UNPKG

dobo

Version:

DBMS for Bajo Framework

1,057 lines 57.3 kB
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: lib/factory/adapter.js</title><!--[if lt IE 9]>
      <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124    C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124    c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227    c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6    V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474    c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323    c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848    S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328    c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="light"><div class="sidebar-container"><div class="sidebar" id="sidebar"><a href="/" class="sidebar-title sidebar-title-anchor">Dobo API</a><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="Dobo.html">Dobo</a></div><div class="sidebar-section-children"><a href="DoboAction.html">DoboAction</a></div><div class="sidebar-section-children"><a href="DoboAdapter.html">DoboAdapter</a></div><div class="sidebar-section-children"><a href="DoboConnection.html">DoboConnection</a></div><div class="sidebar-section-children"><a href="DoboFeature.html">DoboFeature</a></div><div class="sidebar-section-children"><a href="DoboModel.html">DoboModel</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-Helper.html">Helper</a></div><div class="sidebar-section-children"><a href="module-Helper_Model.html">Helper/Model</a></div><div class="sidebar-section-children"><a href="module-Hook.html">Hook</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-externals"><div>Externals</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="external-Tools.html">Tools</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#actionFactory">actionFactory</a></div><div class="sidebar-section-children"><a href="global.html#adapterFactory">adapterFactory</a></div><div class="sidebar-section-children"><a href="global.html#connectionFactory">connectionFactory</a></div><div class="sidebar-section-children"><a href="global.html#featureFactory">featureFactory</a></div><div class="sidebar-section-children"><a href="global.html#modelFactory">modelFactory</a></div><div class="sidebar-section-children"><a href="global.html#pluginFactory">pluginFactory</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"><div class="navbar-item"><a id="" href="https://www.npmjs.com/package/dobo" target="">NPM</a></div><div class="navbar-item"><a id="" href="https://github.com/ardhi/dobo" target="">Github</a></div><div class="navbar-item"><a id="" href="https://dobo.bajo.app/" target="">Dobo</a></div><div class="navbar-item"><a id="" href="https://bajo.app/" target="">Bajo</a></div></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#dark-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">lib_factory_adapter.js</h1></header><article><pre class="prettyprint source lang-js"><code>import { ulid } from 'ulid'
import { v4 as uuidv4, v7 as uuidv7 } from 'uuid'
import crypto from 'crypto'

const defIdField = {
  name: '_id',
  type: 'string',
  maxLength: 50,
  required: true,
  index: 'primary'
}

/**
 * @external Tools
 * @see {@link https://ardhi.github.io/bajo/Tools.html|Bajo Tools}
 */

/**
 * @typedef TIdField
 * @type {object}
 * @memberof DoboAdapter
 * @property {string} [name='_id'] - The name of the ID field.
 * @property {string} [type='string'] - The data type of the ID field.
 * @property {number} [maxLength=50] - The maximum length of the ID field.
 * @property {boolean} [required=true] - Indicates if the ID field is required.
 * @property {string} [index='primary'] - The index type of the ID field.
 */

/**
 * @typedef TSupport
 * @memberof DoboAdapter
 * @type {object}
 * @property {object} [propType={}] - An object indicating support for various property types.
 * @property {boolean} [propType.object=false] - Indicates if object property type is supported.
 * @property {boolean} [propType.array=false] - Indicates if array property type is supported.
 * @property {boolean} [propType.datetime=true] - Indicates if datetime property type is supported.
 * @property {boolean} [search=false] - Indicates if search functionality is supported.
 * @property {boolean} [uniqueIndex=false] - Indicates if unique index functionality is supported.
 * @property {boolean} [nullableField=true] - Indicates if nullable fields are supported.
 * @property {boolean} [transaction=false] - Indicates if transaction functionality is supported.
 */

/**
 * Adapter factory function.
 *
 * @async
 * @returns {Promise&lt;DoboAdapter>}
 */
async function adapterFactory () {
  const { Tools } = this.app.baseClass
  const { pick, cloneDeep, has, uniq, without, isEmpty, omit, isFunction, camelCase, last } = this.app.lib._
  const { isSet } = this.app.lib.aneka
  const { runHook } = this.app.bajo

  /**
   * DoboAdapter class serves as a base class for all database adapters in the Dobo framework. It provides common functionality for managing models, records, and database operations.
   * Child classes should implement the abstract methods to provide specific database functionality.
   *
   * @class
   * @extends external:Tools
   */
  class DoboAdapter extends Tools {
    /**
     * Constructor.
     */
    constructor (plugin, name, options = {}) {
      super(plugin)

      /**
       * Adapter name
       * @type {string}
       */
      this.name = name

      /**
       * ID field configuration
       * @type {DoboAdapter.TIdField}
       */
      this.idField = cloneDeep(defIdField)
      this.propertyType = {}

      /**
       * Support configuration for the adapter
       * @type {DoboAdapter.TSupport}
       */
      this.support = {
        propType: {
          object: false,
          array: false,
          datetime: true
        },
        search: false,
        uniqueIndex: false,
        nullableField: true,
        transaction: false
      }

      /**
       * Indicates whether to use UTC for datetime fields
       * @type {boolean}
       */
      this.useUtc = false

      /**
       * Maximum chunk size for bulk operations
       * @type {number}
       */
      this.maxChunkSize = 500

      /**
       * Indicates whether the adapter uses in-memory storage
       * @type {boolean}
       */
      this.memory = false

      /**
       * Adapter options
       * @type {object}
       */
      this.options = options
    }

    /**
     * Sanitize connection object
     * @async
     * @method
     * @param {Object} conn - Connection object
     * @returns {Promise&lt;void>}
     */
    async sanitizeConnection (conn) {
      conn.proto = conn.proto ?? 'http' // used by adapter that use url based connection
      conn.memory = false
    }

    /**
     * Sanitizes the body of a record before creating or updating it. It ensures that all required fields
     * are present and have valid values, and converts data types as necessary.
     * @param {DoboModel} model - The model instance for which the body is being sanitized
     * @param {object} body - The body of the record to be sanitized
     * @param {boolean} [partial=false] - Indicates whether to perform a partial update
     * @returns {object} - Sanitized body
     */
    sanitizeBody (model, body = {}, partial) {
      const { keys, pick } = this.app.lib._
      const item = cloneDeep(body)
      let newId = false
      if (has(item, 'id') &amp;&amp; this.idField.name !== 'id') {
        item[this.idField.name] = item.id
        newId = true
      }
      for (const prop of model.getNonVirtualProperties()) {
        if (item[prop.name] === 'null') item[prop.name] = null
        if (!isSet(item[prop.name]) &amp;&amp; !this.support.nullableField) {
          switch (prop.type) {
            case 'datetime': item[prop.name] = new Date(0); break
            case 'float':
            case 'double': item[prop.name] = 0; break
            case 'string':
            case 'text': item[prop.name] = ''; break
            case 'object': item[prop.name] = {}; break
            case 'array': item[prop.name] = []; break
          }
        }
        if (isSet(item[prop.name]) &amp;&amp; !this.support.propType[prop.type]) {
          if (prop.type === 'datetime') item[prop.name] = item[prop.name].toISOString()
          else if (['object', 'array'].includes(prop.type)) item[prop.name] = JSON.stringify(item[prop.name])
        }
      }
      const result = partial ? pick(item, keys(body)) : item
      if (newId) delete result.id
      return result
    }

    /**
     * Sanitizes a record retrieved from the database, converting data types as necessary
     * and ensuring that the record conforms to the model's schema.
     * @param {DoboModel} model - The model instance for which the record is being sanitized
     * @param {object} record - The record retrieved from the database
     * @returns {object} - Sanitized record
     */
    sanitizeRecord (model, record = {}) {
      const { dayjs } = this.app.lib
      const { isString } = this.app.lib._
      const item = { ...record }
      if (has(item, this.idField.name) &amp;&amp; this.idField.name !== 'id') {
        item.id = item[this.idField.name]
        delete item[this.idField.name]
      }
      for (const prop of model.properties) {
        if (isSet(item[prop.name])) {
          if (!this.support.propType[prop.type]) {
            try {
              if (prop.type === 'datetime') {
                const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
                item[prop.name] = dt.toDate()
              } else if (['object', 'array'].includes(prop.type)) item[prop.name] = JSON.parse(item[prop.name])
            } catch (err) {
              item[prop.name] = null
            }
          }
          if (prop.type === 'datetime' &amp;&amp; isString(item[prop.name])) {
            const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
            item[prop.name] = dt.toDate()
          }
          if (prop.type === 'boolean' &amp;&amp; isSet(item[prop.name])) item[prop.name] = Boolean(item[prop.name])
        }
      }
      return item
    }

    /**
     * Utility method to get the real fields of a model, excluding virtual fields.
     * This is useful for operations that require only the actual stored properties of a model.
     * @param {*} model
     * @returns {string[]} - Array of real field names
     */
    getRealFields (model) {
      return model.getProperties({ noVirtual: true, namesOnly: true })
    }

    /**
     * Utility method to get the virtual fields of a model.
     * This is useful for operations that need to work with computed or derived properties.
     * @param {DoboModel} model - The model instance
     * @returns {string[]} - Array of virtual field names
     */
    getVirtualFields (model) {
      return model.getVirtualProperties({ namesOnly: true })
    }

    /**
     * Get returning fields for a model based on the provided options. If the adapter supports returning fields,
     * it will return the specified fields or all model properties. It ensures that the ID field is always
     * included in the returned fields.
     * @param {DoboModel} model - The model instance for which to get the returning fields
     * @param {object} options - Options that may include the fields to return
     * @returns {string[]} - Array of field names to be returned
     */
    _getReturningFields (model, options = {}) {
      const { fields = [] } = options
      if (!this.support.returning) return []
      let items = fields.length > 0 ? [...fields] : model.properties.map(prop => prop.name)
      if (!items.includes(this.idField.name)) items.unshift(this.idField.name)
      if (this.idField.name !== 'id') items = without(items, ['id'])
      return uniq(items)
    }

    /**
     * Attaches hooks to the model for various operations. It runs the appropriate hooks
     * before and after the specified operation, allowing for custom behavior to be injected
     * into the model's lifecycle.
     * @internal
     * @async
     * @method
     * @param {string} name - The name of the hook
     * @param {DoboModel} model - The model instance to which the hook is being attached
     * @param  {...any} args - Additional arguments to be passed to the hook
     */
    async _attachHook (name, model, ...args) {
      const { ns } = this.app.dobo
      const { kebabCase } = this.app.lib._
      const options = last(args)
      if (!options.noAdapterHook) {
        const prefix = kebabCase(name).split('-')[0]
        await runHook(`${ns}.adapter:${prefix}Any`, model, options)
        await runHook(`${ns}.adapter:${name}`, model, ...args)
        await runHook(`${ns}.adapter.${camelCase(model.name)}:${name}`, ...args)
      }
    }

    /**
     * Checks the uniqueness of fields with a unique index.
     * @async
     * @method
     * @internal
     * @param {DoboModel} model - The model instance to check
     * @param {object} body - The data to be checked for uniqueness
     * @param {object} options - Additional options, including the action being performed
     * @returns {Promise&lt;void>} - Resolves if unique, throws an error if not
     */
    _checkUnique = async (model, body = {}, options = {}) => {
      const { isSet } = this.app.lib.aneka
      const { filter, map, isEmpty, forOwn } = this.app.lib._
      const indexes = filter(model.indexes ?? [], idx => idx.type === 'unique')
      for (const index of indexes) {
        const query = {}
        for (const field of index.fields) {
          if (isSet(body[field])) query[field] = body[field]
        }
        if (isEmpty(query)) continue
        const { data } = await model.findOneRecord({ query }, options)
        if (!isEmpty(data)) {
          if (['updateRecord', 'upsertRecord'].includes(options.action)) {
            let eq = true
            forOwn(query, (v, k) => {
              if (data[k] !== v) eq = false
            })
            if (!eq) continue
          }
          const error = this.app.dobo.t('uniqueConstraintError')
          const details = map(index.fields, field => {
            return { field, error }
          })
          throw this.app.dobo.error(error, { details, body })
        }
      }
    }

    // Internal calls that will be called by model

    /**
     * Wrapper for the `modelExists` method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     *
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} options
     * @returns {Promise&lt;boolean>}
     */
    async _modelExists (model, options = {}) {
      return await this.modelExists(model, options)
    }

    /**
     * Wrapper for the `buildModel` method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _buildModel (model, options = {}) {
      return await this.buildModel(model, options)
    }

    /**
     * Wrapper for the `dropModel` method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _dropModel (model, options = {}) {
      return await this.dropModel(model, options)
    }

    /**
     * Prepares the body of a record for creation by populating default values for properties
     * that are not set. It handles various types of default values, including functions,
     * special strings (like 'now', 'uuid', etc.), and static values.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model - The model instance for which the body is being prepared
     * @param {object} body - The data to be prepared for creation
     * @param {object} options - Additional options that may affect the preparation
     * @returns {Promise&lt;object>} - The prepared body with default values populated
     */
    async _prepBodyForCreate (model, body = {}, options = {}) {
      const { callHandler } = this.app.bajo
      const { isSet, generateId } = this.app.lib.aneka
      for (const prop of model.getProperties({ noVirtual: true })) {
        if (isSet(prop.default) &amp;&amp; (!options.noDefault) &amp;&amp; (!isSet(body[prop.name]) || body[prop.name] === prop.default)) {
          if (isFunction(prop.default)) body[prop.name] = await prop.default.call(model)
          else if (typeof prop.default !== 'string') body[prop.name] = prop.default
          else {
            if (['now'].includes(prop.default) &amp;&amp; prop.type === 'datetime') {
              body[prop.name] = new Date()
            } else if (['uuid', 'uuidv4'].includes(prop.default) &amp;&amp; prop.type === 'string') {
              body[prop.name] = uuidv4().slice(0, prop.maxLength)
            } else if (prop.default === 'uuidv7' &amp;&amp; prop.type === 'string') {
              body[prop.name] = uuidv7().slice(0, prop.maxLength)
            } else if (prop.default === 'ulid' &amp;&amp; prop.type === 'string') {
              body[prop.name] = ulid().slice(0, prop.maxLength)
            } else if (prop.default === 'generateid' &amp;&amp; prop.type === 'string') {
              body[prop.name] = generateId()
            } else if (prop.default.startsWith('handler:')) {
              const [, ...args] = prop.default.split(':')
              if (args.length > 0) body[prop.name] = await callHandler(args.join(':'))
            } else if (prop.default.startsWith('md5:') &amp;&amp; prop.type === 'string') {
              const [, field] = prop.default.split(':')
              const fields = field.split(',')
              if (model.properties.filter(item => fields.includes(item.name)).length === fields.length) {
                const values = fields.map(f => body[f])
                body[prop.name] = crypto.createHash('md5').update(values.join(':')).digest('hex')
              }
            } else {
              body[prop.name] = prop.default
            }
          }
        }
      }
      return pick(body, this.getRealFields(model))
    }

    /**
     * Prepares the ID for a record before creation. It generates an ID if it is not set in the body.
     *
     * @internal
     * @async
     * @method
     * @param {DoboModel} model - The model instance for which the ID is being prepared
     * @param {object} body - The data containing the ID
     * @param {object} options - Additional options that may affect ID generation
     * @returns {Promise&lt;void>} - Resolves when the ID has been prepared
     */
    async _prepIdForCreate (model, body = {}, options = {}) {
      const { isSet, generateId } = this.app.lib.aneka
      const { isFunction } = this.app.lib._
      const prop = model.properties.find(p => p.name === 'id')
      if (!isSet(body.id) &amp;&amp; prop.type === 'string') {
        if (this.idGenerator) {
          if (['uuid', 'uuidv4'].includes(this.idGenerator)) body.id = uuidv4()
          else if (['uuidv7'].includes(this.idGenerator)) body.id = uuidv7()
          else if (this.idGenerator === 'generateId') body.id = generateId()
          else if (isFunction(this.idGenerator)) body.id = await this.idGenerator(model, body, options)
        }
        if (!body.id) body.id = ulid()
        body.id = body.id.slice(0, prop.maxLength)
      }
    }

    _injectMeta (result = {}, options = {}) {
      result.warnings = result.warnings ?? []
      result.warnings.push(...(options.warnings ?? []))
    }

    /**
     * Wrapper for the {@link DoboAdapter#createRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} input
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _createRecord (model, input = {}, options = {}) {
      const { isSet } = this.app.lib.aneka
      let body = await this._prepBodyForCreate(model, input, options)
      await this._prepIdForCreate(model, body, options)
      if (!options.noUniqueCheck) {
        if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
      }
      if (!options.noIdCheck &amp;&amp; isSet(body.id)) {
        const resp = await this.getRecord(model, body.id, { noMagic: true })
        if (!isEmpty(resp.data)) throw this.plugin.error('recordExists%s%s', body.id, model.name)
      }
      body = this.sanitizeBody(model, body)

      await this._attachHook('beforeCreateRecord', model, body, options)
      const result = await this.createRecord(model, body, options)
      await this._attachHook('afterCreateRecord', model, body, result, options)

      if (options.noResult) return
      result.data = this.sanitizeRecord(model, result.data)
      this._injectMeta(result, options)
      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#bulkCreateRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {Array&lt;object>} bodies
     * @param {object} options
     * @returns {Promise&lt;void>}
     */
    async _bulkCreateRecord (model, bodies = [], options = {}) {
      const { chunk } = this.app.lib._
      let { chunkSize = this.maxChunkSize } = options
      if (chunkSize > this.maxChunkSize) chunkSize = this.maxChunkSize
      for (const idx in bodies) {
        const body = await this._prepBodyForCreate(model, bodies[idx], options)
        await this._prepIdForCreate(model, body, options)
        bodies[idx] = this.sanitizeBody(model, body)
      }

      await this._attachHook('beforeBulkCreateRecord', model, bodies, options)
      const items = chunk(bodies, chunkSize)
      for (const item of items) {
        await this.bulkCreateRecord(model, item, options)
      }
      await this._attachHook('afterBulkCreateRecord', model, bodies, [], options)
    }

    /**
     * Wrapper for the {@link DoboAdapter#getRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {string|number} id
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _getRecord (model, id, options = {}) {
      await this._attachHook('beforeGetRecord', model, id, options)
      const result = await this.getRecord(model, id, options)
      await this._attachHook('afterGetRecord', model, id, result, options)

      if (isEmpty(result.data) &amp;&amp; options.throwNotFound) throw this.plugin.error('recordNotFound%s%s', id, model.name)
      result.data = this.sanitizeRecord(model, result.data)
      this._injectMeta(result, options)
      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#updateRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {string|number} id
     * @param {object} input
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _updateRecord (model, id, input = {}, options = {}) {
      let body = omit(input, this.getVirtualFields(model))
      if (!options.noUniqueCheck) {
        if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
      }
      if (!options._data) {
        const resp = await this.getRecord(model, id, { noMagic: true })
        if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
        options._data = resp.data
      }
      body = this.sanitizeBody(model, body, true)
      delete body.id

      await this._attachHook('beforeUpdateRecord', model, id, body, options)
      const result = await this.updateRecord(model, id, body, options)
      await this._attachHook('afterUpdateRecord', model, id, body, result, options)

      if (options.noResult) return
      result.oldData = this.sanitizeRecord(model, result.oldData)
      result.data = this.sanitizeRecord(model, result.data)
      this._injectMeta(result, options)
      return result
    }

    /**
     * Upserts a record for the given model.
     * This method will only run if child adapter does not implement {@link DoboAdapter#upsertRecord}.
     *
     * @internal
     * @async
     * @method
     * @param {DoboModel} model - The model instance for which the record is being upserted
     * @param {object} input - The input data for the upsert
     * @param {object} options - Additional options that may affect record upserting
     * @returns {Promise&lt;object>} - The result of the record upsert
     */
    async _upsertRecord (model, input = {}, options = {}) {
      let body = omit(input, this.getVirtualFields(model))
      if (!options.noUniqueCheck) {
        if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
      }
      if (isSet(body.id)) {
        if (!options._data) {
          const resp = await this.getRecord(model, body.id, { noMagic: true })
          if (!resp.data) throw this.plugin.error('recordNotFound%s%s', body.id, model.name)
          options._data = resp.data
        }
      }
      body = this.sanitizeBody(model, body)

      await this._attachHook('beforeUpsertRecord', model, body, options)
      const result = await this.upsertRecord(model, body, options)
      await this._attachHook('afterUpsertRecord', model, body, result, options)

      if (options.noResult) return
      if (result.oldData) result.oldData = this.sanitizeRecord(model, result.oldData)
      result.data = this.sanitizeRecord(model, result.data)
      this._injectMeta(result, options)
      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#removeRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {string|number} id
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _removeRecord (model, id, options = {}) {
      if (!options._data) {
        const resp = await this.getRecord(model, id, { noMagic: true })
        if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
        options._data = resp.data
      }

      await this._attachHook('beforeRemoveRecord', model, id, options)
      const result = await this.removeRecord(model, id, options)
      await this._attachHook('afterRemoveRecord', model, id, result, options)

      if (options.noResult) return
      result.oldData = this.sanitizeRecord(model, result.oldData)
      this._injectMeta(result, options)
      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#clearRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _clearRecord (model, options = {}) {
      await this._attachHook('beforeClearRecord', model, options)
      const result = await this.clearRecord(model, options)
      await this._attachHook('afterClearRecord', model, result, options)

      this._injectMeta(result, options)
      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#findRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} filter
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _findRecord (model, filter = {}, options = {}) {
      let result
      try {
        await this._attachHook('beforeFindRecord', model, filter, options)
        result = await this.findRecord(model, filter, options)
        await this._attachHook('afterFindRecord', model, filter, result, options)
      } catch (err) {
        if (!['_emptyColumnQuery', '_abortAction'].includes(err.message)) throw err
        result = {
          data: [],
          count: 0
          // warnings: [] // TODO: should generate warnings?
        }
      }

      for (const idx in result.data) {
        result.data[idx] = this.sanitizeRecord(model, result.data[idx])
      }
      this._injectMeta(result, options)
      return result
    }

    /**
     * Finds all records for the given model based on the provided filter.
     * This method will only run if child adapter does not implement {@link DoboAdapter#findAllRecord}.
     *
     * @internal
     * @async
     * @method
     * @param {DoboModel} model - The model instance for which the records are being found
     * @param {object} filter - The filter criteria for finding records
     * @param {object} options - Additional options that may affect record finding
     * @returns {Promise&lt;object>} - The result of the record finding
     */
    async _findAllRecord (model, filter = {}, options = {}) {
      let result
      try {
        await this._attachHook('beforeFindAllRecord', model, filter, options)
        result = await this.findAllRecord(model, filter, options)
        await this._attachHook('afterFindAllRecord', model, filter, result, options)
      } catch (err) {
        if (err.message !== '_emptyColumnQuery') throw err
        result = {
          data: [],
          count: 0
          // warnings: [] // TODO: should generate warnings?
        }
      }

      for (const idx in result.data) {
        result.data[idx] = this.sanitizeRecord(model, result.data[idx])
      }
      this._injectMeta(result, options)
      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#countRecord} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} filter
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _countRecord (model, filter = {}, options = {}) {
      let result
      try {
        await this._attachHook('beforeCountRecord', model, filter, options)
        result = await this.countRecord(model, filter, options)
        await this._attachHook('afterCountRecord', model, filter, result, options)
      } catch (err) {
        if (err.message !== '_emptyColumnQuery') throw err
        result = { data: 0 }
      }

      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#createAggregate} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} filter
     * @param {object} params
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _createAggregate (model, filter = {}, params = {}, options = {}) {
      const fieldPropTypes = ['integer', 'smallint', 'float', 'double']
      const groupPropTypes = ['string', ...fieldPropTypes]
      this.app.dobo.checkAggregateParams(params)
      const { group, field } = params

      let prop = model.properties.find(p => p.name === group)
      if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), group)
      if (!groupPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', group, groupPropTypes.join(', '))

      prop = model.properties.find(p => p.name === field)
      if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)
      // if (!fieldPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', field, fieldPropTypes.join(', '))

      let result
      try {
        await this._attachHook('beforeCreateAggregate', model, filter, params, options)
        result = await this.createAggregate(model, filter, params, options)
        await this._attachHook('afterCreateAggregate', model, filter, params, result, options)
      } catch (err) {
        if (err.message !== '_emptyColumnQuery') throw err
        result = { data: [] }
      }

      for (const idx in result.data) {
        result.data[idx] = this.sanitizeRecord(model, result.data[idx])
      }
      this._injectMeta(result, options)
      return result
    }

    /**
     * Wrapper for the {@link DoboAdapter#createHistogram} method, called internally by `model` to make sure
     * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
     * @internal
     * @async
     * @method
     * @param {DoboModel} model
     * @param {object} filter
     * @param {object} params
     * @param {object} options
     * @returns {Promise&lt;object>}
     */
    async _createHistogram (model, filter = {}, params, options = {}) {
      // const fieldPropTypes = ['integer', 'smallint', 'float', 'double']
      const groupPropTypes = ['datetime', 'date']
      this.app.dobo.checkHistogramParams(params)
      const { group, field } = params

      let prop = model.properties.find(p => p.name === group)
      if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), group)
      if (!groupPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', group, groupPropTypes.join(', '))

      prop = model.properties.find(p => p.name === field)
      if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)

      let result
      try {
        await this._attachHook('beforeCreateHistogram', model, filter, params, options)
        result = await this.createHistogram(model, filter, params, options)
        await this._attachHook('afterCreateHistogram', model, filter, params, result, options)
      } catch (err) {
        if (err.message !== '_emptyColumnQuery') throw err
        result = { data: [] }
      }

      for (const idx in result.data) {
        result.data[idx] = this.sanitizeRecord(model, result.data[idx])
      }
      this._injectMeta(result, options)
      return result
    }

    // Public calls that need to be implemented by child adapters

    /**
     * Connects to the database using the provided connection parameters.
     * @param {*} connection - The connection parameters for the database
     * @param {*} noRebuild - Flag indicating whether to skip rebuilding the database schema
     * @returns {Promise&lt;void>} - Resolves when the connection is established
     */
    async connect (connection, noRebuild) {
    }

    /**
     * Checks if the model exists in the database.
     *
     * Must be implemented by child classes to provide specific database functionality for model existence checking,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance to check for existence
     * @param {object} options - Additional options that may affect the existence check
     */
    async modelExists (model, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'modelExists', this.name)
    }

    /**
     * Builds the model in the database.
     *
     * Must be implemented by child classes to provide specific database functionality for model building,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the record is being built
     * @param {object} options - Additional options that may affect model building
     */
    async buildModel (model, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'buildModel', this.name)
    }

    /**
     * Drops the model from the database.
     *
     * Must be implemented by child classes to provide specific database functionality for dropping a model,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the record is being dropped
     * @param {object} options - Additional options that may affect record dropping
     */
    async dropModel (model, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'dropModel', this.name)
    }

    /**
     * Creates a new record for the given model.
     *
     * Must be implemented by child classes to provide specific database functionality for record creation,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the record is being created
     * @param {object} input - The input data for the new record
     * @param {object} options - Additional options that may affect record creation
     * @returns {Promise&lt;object>} - The result of the record creation
     */
    async createRecord (model, body = {}, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'createRecord', this.name)
    }

    /**
     * Retrieves a record for the given model by its ID.
     *
     * Must be implemented by child classes to provide specific database functionality for record retrieval,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the record is being retrieved
     * @param {string|number} id - The ID of the record to retrieve
     * @param {object} options - Additional options that may affect record retrieval
     * @returns {Promise&lt;object>} - The result of the record retrieval
     */
    async getRecord (model, id, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'getRecord', this.name)
    }

    /**
     * Updates a record for the given model by its ID.
     *
     * Must be implemented by child classes to provide specific database functionality for record updating,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the record is being updated
     * @param {string|number} id - The ID of the record to update
     * @param {object} input - The input data for the update
     * @param {object} options - Additional options that may affect record updating
     * @returns {Promise&lt;object>} - The result of the record update
     */
    async updateRecord (model, id, body = {}, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'updateRecord', this.name)
    }

    /**
     * Removes a record for the given model by its ID.
     *
     * Must be implemented by child classes to provide specific database functionality for record removal,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the record is being removed
     * @param {string|number} id - The ID of the record to remove
     * @param {object} options - Additional options that may affect record removal
     * @returns {Promise&lt;object>} - The result of the record removal
     */
    async removeRecord (model, id, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'removeRecord', this.name)
    }

    /**
     * Clears all records for the given model.
     *
     * Must be implemented by child classes to provide specific database functionality for record clearing,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the records are being cleared
     * @param {object} options - Additional options that may affect record clearing
     * @returns {Promise&lt;object>} - The result of the record clearing
     */
    async clearRecord (model, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'clearRecord', this.name)
    }

    /**
     * Finds records for the given model based on the provided filter.
     *
     * Must be implemented by child classes to provide specific database functionality for record finding,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the records are being found
     * @param {object} filter - The filter criteria for finding records
     * @param {object} options - Additional options that may affect record finding
     * @returns {Promise&lt;object>} - The result of the record finding
     */
    async findRecord (model, filter = {}, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'findRecord', this.name)
    }

    /**
     * Bulk creates records for the given model.
     *
     * Must be implemented by child classes to provide specific database functionality for bulk record creation,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the records are being created
     * @param {Array&lt;object>} bodies - The array of input data for the new records
     * @param {object} options - Additional options that may affect record creation
     * @returns {Promise&lt;void>} - Resolves when the records have been created
     */
    async bulkCreateRecord (model, bodies = [], options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'bulkCreateRecord', this.name)
    }

    /**
     * Counts the records for the given model based on the provided filter.
     *
     * Must be implemented by child classes to provide specific database functionality for record counting,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the records are being counted
     * @param {object} filter - The filter criteria for counting records
     * @param {object} options - Additional options that may affect record counting
     * @returns {Promise&lt;object>} - The result of the record counting
     */
    async countRecord (model, filter = {}, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'countRecord', this.name)
    }

    /**
     * Creates an aggregate for the given model based on the provided filter and parameters.
     *
     * Must be implemented by child classes to provide specific database functionality for aggregate creation,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the aggregate is being created
     * @param {object} filter - The filter criteria for creating the aggregate
     * @param {object} params - The parameters for the aggregate creation
     * @param {object} options - Additional options that may affect aggregate creation
     * @returns {Promise&lt;object>} - The result of the aggregate creation
     */
    async createAggregate (model, filter = {}, params = {}, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'createAggregate', this.name)
    }

    /**
     * Creates a histogram for the given model based on the provided filter and parameters.
     *
     * Must be implemented by child classes to provide specific database functionality for histogram creation,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the histogram is being created
     * @param {object} filter - The filter criteria for creating the histogram
     * @param {object} params - The parameters for the histogram creation
     * @param {object} options - Additional options that may affect histogram creation
     * @returns {Promise&lt;object>} - The result of the histogram creation
     */
    async createHistogram (model, filter = {}, params = {}, options = {}) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'createHistogram', this.name)
    }

    /**
     * Executes a transaction for the given model.
     *
     * Must be implemented by child classes to provide specific database functionality for transactions,
     * or throw an error if the operation is not supported by the adapter.
     *
     * @param {DoboModel} model - The model instance for which the transaction is being executed
     * @param {Function} handler - The transaction handler function
     * @param  {...any} args - Additional arguments for the transaction handler
     */
    async transaction (model, handler, ...args) {
      throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'transaction', this.name)
    }

    /**
     * Disposes of the adapter, performing any necessary cleanup operations.
     *
     * @returns {Promise&lt;void>} - Resolves when the adapter has been disposed
     */
    async dispose () {
      await super.dispose()
    }
  }

  class DoboNullAdapter extends DoboAdapter {
    constructor (plugin, name = 'null', options = {}) {
      super(plugin, name, options)
      this.memory = true
    }
  }

  this.app.baseClass.DoboAdapter = DoboAdapter
  this.app.baseClass.DoboNullAdapter = DoboNullAdapter
}

export default adapterFactory
</code></pre></article></section></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><a href="/" class="sidebar-title sidebar-title-anchor">Dobo API</a><div class="mobile-nav-links"><div class="navbar-item"><a id="" href="https://www.npmjs.com/package/dobo" target="">NPM</a></div><div class="navbar-item"><a id="" href="https://github.com/ardhi/dobo" target="">Github</a></div><div class="navbar-item"><a id="" href="https://dobo.bajo.app/" target="">Dobo</a></div><div class="navbar-item"><a id="" href="https://bajo.app/" target="">Bajo</a></div></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="Dobo.html">Dobo</a></div><div class="sidebar-section-children"><a href="DoboAction.html">DoboAction</a></div><div class="sidebar-section-children"><a href="DoboAdapter.html">DoboAdapter</a></div><div class="sidebar-section-children"><a href="DoboConnection.html">DoboConnection</a></div><div class="sidebar-section-children"><a href="DoboFeature.html">DoboFeature</a></div><div class="sidebar-section-children"><a href="DoboModel.html">DoboModel</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-modules"><div>Modules</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="module-Helper.html">Helper</a></div><div class="sidebar-section-children"><a href="module-Helper_Model.html">Helper/Model</a></div><div class="sidebar-section-children"><a href="module-Hook.html">Hook</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-externals"><div>Externals</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="external-Tools.html">Tools</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#actionFactory">actionFactory</a></div><div class="sidebar-section-children"><a href="global.html#adapterFactory">adapterFactory</a></div><div class="sidebar-section-children"><a href="global.html#connectionFactory">connectionFactory</a></div><div class="sidebar-section-children"><a href="global.html#featureFactory">featureFactory</a></div><div class="sidebar-section-children"><a href="global.html#modelFactory">modelFactory</a></div><div class="sidebar-section-children"><a href="global.html#pluginFactory">pluginFactory</a></div></div></div><div class="mobile-navbar-actions"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#dark-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div></div></div><script type="text/javascript" src="scripts/core.min.js"></script><script src="scripts/search.min.js" defer="defer"></script><script src="scripts/third-party/fuse.js" defer="defer"></script><script type="text/javascript">var tocbotInstance=tocbot.init({tocSelector:"#eed4d2a0bfd64539bb9df78095dec881",contentSelector:".main-content",headingSelector:"h1, h2, h3",hasInnerContainers:!0,scrollContainer:".main-content",headingsOffset:130,onClick:bringLinkToView})</script></body></html>