lib_factory_model.js

import { sanitizeAll, sanitizeRef } from '../helper.js'
import clearRecord from './model/clear-record.js'
import countRecord from './model/count-record.js'
import createRecord from './model/create-record.js'
import drop from './model/drop.js'
import findAllRecord from './model/find-all-record.js'
import findOneRecord from './model/find-one-record.js'
import findRecord from './model/find-record.js'
import getRecord from './model/get-record.js'
import createHistogram from './model/create-histogram.js'
import createAggregate from './model/create-aggregate.js'
import exists from './model/exists.js'
import build from './model/build.js'
import loadFixtures from './model/load-fixtures.js'
import removeRecord from './model/remove-record.js'
import updateRecord from './model/update-record.js'
import createAttachment from './model/create-attachment.js'
import updateAttachment from './model/update-attachment.js'
import getAttachment from './model/get-attachment.js'
import removeAttachment from './model/remove-attachment.js'
import findAttachment from './model/find-attachment.js'
import sanitizeBody from './model/sanitize-body.js'
import sanitizeRecord from './model/sanitize-record.js'
import sanitizeFixture from './model/sanitize-fixture.js'
import upsertRecord from './model/upsert-record.js'
import bulkCreateRecord from './model/bulk-create-record.js'
import listAttachment from './model/list-attachment.js'
import transaction from './model/transaction.js'
import validate from './model/validate.js'

/**
 * @typedef {Object} TOptions
 * @memberof DoboModel
 * @type {Object}
 * @property {boolean} [noHook=false] - If `true`, no model's hook will be executed
 * @property {boolean} [noModelHook=false] - If `true`, no model's hook will be executed
 * @property {boolean} [noDynHook=false] - If `true`, no dynamic hook will be executed
 * @property {boolean} [noValidation=false] - If `true`, no validation of data payload performed
 * @property {boolean} [noCheckUnique=false] - If `true`, no unique validation for ID performed
 * @property {boolean} [noBodySanitizer=false] - If `true`, accept data payload as is without sanitization
 * @property {boolean} [noRecordSanitizer=false] - If `true`, accept result payload as is without sanitization
 * @property {boolean} [noResult=false] - If `true`, returns nothing
 * @property {boolean} [truncateString=true] - If `true` (default), string is truncated to its model's `maxLength`
 * @property {boolean} [partial] - If `true`, only updated values are saved. Otherwise replace all existing values with given payload. Defaults to `true` for `updateRecord`
 * @property {boolean} [dataOnly=true] - If `true` (default) returns only record's object. Otherwise {@link DoboModel.TResult}
 */

/**
 * Sort order key used in pagination and sorting of records. It can be either:
 * - `1` for ascending order (the default, if none is provided) or
 * - `-1` for descending order.
 * @typedef {string} TSortOrder
 * @memberof DoboModel
 */

/**
 * Key value pairs used as sort order in pagination and sorting of records. It is an object where:
 * - Key represents model's field name
 * - Value represents its sort order
 *
 * @typedef {Object.<string, DoboModel.TSortOrder>} TSort
 * @memberof DoboModel
 * @example
 * // to sort by firstName (ascending) and lastName (descending)
 * const sort = {
 *   firstName: 1,
 *   lastName: -1
 * }
 */

/**
 * @typedef {Object} TPagination
 * @memberof DoboModel
 * @property {number} limit - Number of records per page
 * @property {number} page - Page number
 * @property {number} [skip] - Records to skip. If not provided, it will be calculated based on `limit` and `page`
 */

/**
 * Property value type definition. Used for building property `values` in a model which is an array of this object type.
 * @typedef TPropertyValue
 * @memberof DoboModel
 * @type {Object}
 * @property {string} value - The actual value of the property
 * @property {string} text - The display text for the property value, which can be translated based on the request context
 */

/**
 * Formatted version of a record's property values. It is an object where:
 * - Key represents the property name
 * - Value represents its formatted value according to the property's data type or formatting function. Data type is ALWAYS in string.
 *
 * This record is then wrapped in a `_fmt` property.
 * @typedef TRecordFormatted
 * @memberof DoboModel
 * @type {Object}
 * @property {Object} [_fmt={}] - Wrapper
 * @example
 * {
 *   _fmt: {
 *     id: "1",
 *     item: "Laptop",
 *     createdAt: "Jan 1, 2024, 12:00 AM",
 *     isActive: "True",
 *     price: "2,500.00"
 *   }
 * }
 */

/**
 * Reference record related to a record's result. It is an object where:
 * - Key represents the name of the referenced record. Normally, it is the referenced model's name in camelCase format.
 * - Value represents its reference record. It can be an object or an array of objects, depending on the relationship type.
 *
 * This record is then wrapped in a `_ref` property in the result object.
 * @typedef TRecordRef
 * @memberof DoboModel
 * @type {Object}
 * @property {Object} [_ref={}] - Wrapper
 * @property {Object|Array} [_ref.<string>.<DoboModel.TRecord>] - Reference record
 * @example
 * {
 *   _ref: {
 *     user: {
 *       id: 1,
 *       name: "John Doe"
 *     },
 *     posts: [
 *       {
 *         id: 1,
 *         title: "Post Title"
 *       },
 *       {
 *         id: 2,
 *         title: "Another Post Title"
 *       }
 *     ]
 *   }
 * }
 */

/**
 * The record returned from a model's action. It is an object where:
 * - Key represents the property name
 * - Value represents its value according to the property's data type
 *
 * This record is then merged with {@link DoboModel.TRecordFormatted} and {@link DoboModel.TRecordRef} to form the final record returned from a model's action.
 * @typedef TRecord
 * @memberof DoboModel
 * @type {Object & DoboModel.TRecordFormatted & DoboModel.TRecordRef}
 * @example
 * {
 *   id: 1,
 *   item: "Laptop",
 *   createdAt: "2024-01-01T00:00:00.000Z",
 *   isActive: true,
 *   price: 2500.00,
 *   _fmt: {
 *     id: "1",
 *     item: "Laptop",
 *     createdAt: "Jan 1, 2024, 12:00 AM",
 *     isActive: "True",
 *     price: "2,500.00"
 *   },
 *   _ref: {
 *     user: {
 *       id: 1,
 *       name: "John Doe"
 *     }
 *   }
 * }
 */

/**
 * The result returned from a model's action other than `find*`, `{create|update|remove}Record` if `dataOnly` is set to `false`.
 * @typedef TResultRecord
 * @memberof DoboModel
 * @type {Object}
 * @property {Array<DoboModel.TRecord>} data - The data returned from the action
 */

/**
 * The result returned from a model's `find*` actions if `dataOnly` is set to `false`.
 * @typedef TResultFindRecord
 * @memberof DoboModel
 * @type {Object}
 * @property {Array<DoboModel.TRecord>} data - The data returned from the action
 * @property {number} [count] - The total number of records found, if applicable
 * @property {boolean} [cached] - Indicates if the result was retrieved from cache, if applicable. Only available if `bajoCache` is used
 * @property {Array} [warnings] - Any warnings generated during the action, if applicable
 */

/**
 * The result returned from a model's `getRecord` action if `dataOnly` is set to `false`.
 * @typedef TResultGetRecord
 * @memberof DoboModel
 * @type {Object}
 * @property {DoboModel.TRecord} data - The data returned from the action
 */

/**
 * The result returned from a model's `createRecord` action if `dataOnly` is set to `false`.
 * @typedef TResultCreateRecord
 * @memberof DoboModel
 * @type {Object}
 * @property {DoboModel.TRecord} data - The data returned from the action
 */

/**
 * The result returned from a model's `updateRecord` action if `dataOnly` is set to `false`.
 * @typedef TResultUpdateRecord
 * @memberof DoboModel
 * @type {Object}
 * @property {DoboModel.TRecord} data - The updated data returned from the action
 * @property {DoboModel.TRecord} oldData - The previous data before the update
 */

/**
 * The result returned from a model's `removeRecord` action if `dataOnly` is set to `false`.
 * @typedef TResultRemoveRecord
 * @memberof DoboModel
 * @type {Object}
 * @property {DoboModel.TRecord} oldData - The removed data returned from the action
 */

/**
 * Model factory.
 *
 * @async
 * @returns {Promise<void>} Returns nothing
 */
async function modelFactory () {
  const { Tools } = this.app.baseClass
  const { defaults } = this.app.lib._

  /**
   * Model class defintion.
   *
   * @class
   */
  class DoboModel extends Tools {
    /**
     * Constructor
     */
    constructor (plugin, options) {
      super(plugin)

      defaults(this, options)

      /**
       * Model's adapter name
       * @type {string}
       */
      this.adapter = this.connection.adapter
    }

    /**
     * Invoke a model's action. It will create a new instance of {@link DoboAction} and return it.
     * @method
     * @memberof DoboModel
     * @param {string} name - The name of the action to invoke
     * @param  {...any} args - Arguments to pass to the action
     * @returns {DoboAction} Returns an instance of {@link DoboAction}
     */
    action = (name, ...args) => {
      const action = new this.app.baseClass.DoboAction(this, name, ...args)
      return action
    }

    sanitizeObjectDef = async (obj) => {
      await sanitizeAll.call(this.app.dobo, obj)
    }

    /**
     * Sanitize a reference object based on the model's properties and rules.
     * @method
     * @memberof DoboModel
     * @async
     * @param {*} obj - The reference object to sanitize
     * @param {boolean} [fatal=true] - Whether to throw an error if sanitization fails
     */
    sanitizeRef = async (obj, fatal = true) => {
      await sanitizeRef.call(this.app.dobo, obj, this.app.dobo.models, fatal)
    }

    /**
     * Sanitize the given ID based on the model's properties.
     * @method
     * @memberof DoboModel
     * @param {string|number} id - The ID to sanitize
     * @returns {string|number} Returns the sanitized ID
     */
    sanitizeId = (id) => {
      const prop = this.properties.find(p => p.name === 'id')
      if (prop.type === 'integer') id = parseInt(id)
      return id
    }

    /**
     * Get a property by its name.
     * @method
     * @memberof DoboModel
     * @param {string} name - The name of the property to retrieve
     * @returns {Object|null} Returns the property object if found, otherwise `undefined`
     */
    getProperty = (name) => {
      return this.properties.find(prop => prop.name === name)
    }

    /**
     * Get the model's properties based on the given options.
     * @method
     * @memberof DoboModel
     * @param {Object} [options={}] - Options object
     * @param {boolean} [options.noVirtual=false] - If `true`, exclude virtual properties
     * @param {boolean} [options.namesOnly=false] - If `true`, return only property names instead of full property objects
     * @returns {Array<object|string>} Returns an array of properties or property names based on the options
     */
    getProperties = (options = {}) => {
      const { noVirtual, namesOnly } = options
      const items = noVirtual ? this.properties.filter(prop => !prop.virtual) : this.properties
      return namesOnly ? items.map(item => item.name) : items
    }

    /**
     * Get the model's virtual properties.
     * @method
     * @memberof DoboModel
     * @param {boolean} [namesOnly=false] - If `true`, return only property names instead of full property objects
     * @returns {Array<object|string>} Returns an array of virtual properties or property names based on the `namesOnly` option
     */
    getVirtualProperties = (namesOnly) => {
      const items = this.properties.filter(prop => prop.virtual)
      return namesOnly ? items.map(item => item.name) : items
    }

    /**
     * Get the model's non-virtual properties.
     * @method
     * @memberof DoboModel
     * @param {boolean} [namesOnly=false] - If `true`, return only property names instead of full property objects
     * @returns {Array<object|string>} Returns an array of non-virtual properties or property names based on the `namesOnly` option
     */
    getNonVirtualProperties = (namesOnly) => {
      const items = this.properties.filter(prop => !prop.virtual)
      return namesOnly ? items.map(item => item.name) : items
    }

    /**
     * Returns the model's indexes.
     * @method
     * @memberof DoboModel
     * @returns {Array<object>} Returns an array of index objects defined in the model
     */
    getIndexes = () => {
      return this.indexes
    }

    /**
     * Checks if the model has a property with the given name.
     * @method
     * @memberof DoboModel
     * @param {string} name - The name of the property to check
     * @returns {boolean} Returns `true` if the property exists, otherwise `false`
     */
    hasProperty = (name) => {
      return !!this.getProperty(name)
    }

    syncIdField = (idField) => {
      const { cloneDeep, findIndex } = this.app.lib._
      if (!this.adapter) return
      if (!idField) idField = cloneDeep(this.adapter.idField)
      const idx = findIndex(this.properties, { name: 'id' })
      if (idx === -1) return
      this.properties.splice(idx, 1)
      this.properties.unshift(idField)
    }

    /**
     * Internal method to perform a simple lookup based on the provided value and lookupValue.
     * @method
     * @internal
     * @memberof DoboModel
     * @async
     * @param {string|object|Array} value - The value to look up
     * @param {object} lookupValue - The lookup values to replace in the query
     * @param {object} [options={}] - Additional options for the lookup
     * @returns {*|null|undefined} Returns the result of the lookup or `null` if not found or `undefined` if the value is not a string, object, or array
     */
    _simpleLookup = async (value, lookupValue, options = {}) => {
      const { get, isEmpty, isString, isPlainObject, isArray } = this.app.lib._
      let model
      let field
      let query
      if (isString(value)) {
        [model, field, ...query] = value.split(':')
        query = query.join(':')
      } else if (isPlainObject(value)) ({ model, field, query } = value)
      else if (isArray(value)) [model, field, query] = value
      else return
      for (const key in lookupValue) {
        query = query.replaceAll(`{${key}}`, lookupValue[key])
      }
      if (isEmpty(field)) field = 'id'
      const { getModel } = this.app.dobo
      const ref = getModel(model)
      const opts = { ...options, noCache: true, noMagic: true }
      opts.dataOnly = true
      const rec = await ref.findOneRecord({ query }, opts)
      return get(rec, field, null)
    }

    /**
     * Builds the values for a given property if `prop.values` is set:
     * - If a string, it will be treated as a handler and executed to get the values.
     * - If a function, it will be executed to get the values.
     * - If an array, it will be used as is.
     *
     * Values are returned as an array of {@link DoboModel.TPropertyValues}.
     * If a request object is provided in the options, the `text` will be translated using
     * the request's translation function.
     * @async
     * @method
     * @memberof DoboModel
     * @param {object} prop - The property definition
     * @param {DoboModel.TOptions} opts - Additional options for building the property values
     * @returns {Array<DoboModel.TPropertyValue>} Returns an array of property values
     */
    buildPropValues = async (prop, opts) => {
      const { isString, camelCase, isFunction } = this.app.lib._
      const { callHandler } = this.app.bajo
      const values = isString(prop.values) || isFunction(prop.values) ? await callHandler(this, prop.values, opts) : [...prop.values]
      return values.map(v => {
        if (isString(v)) v = { value: v, text: v }
        if (opts.req) {
          const key = camelCase(`${prop.name} ${v.text}`)
          if (opts.req.te(key)) v.text = opts.req.t(key)
        }
        return v
      })
    }

    build = build
    exists = exists
    drop = drop

    createRecord = createRecord
    getRecord = getRecord
    updateRecord = updateRecord
    upsertRecord = upsertRecord
    removeRecord = removeRecord
    clearRecord = clearRecord
    findRecord = findRecord
    findOneRecord = findOneRecord
    findAllRecord = findAllRecord

    transaction = transaction
    bulkCreateRecord = bulkCreateRecord

    createAggregate = createAggregate
    createHistogram = createHistogram
    countRecord = countRecord

    createAttachment = createAttachment
    getAttachment = getAttachment
    updateAttachment = updateAttachment
    removeAttachment = removeAttachment
    findAttachment = findAttachment
    listAttachment = listAttachment

    loadFixtures = loadFixtures
    sanitizeRecord = sanitizeRecord
    sanitizeBody = sanitizeBody
    sanitizeFixture = sanitizeFixture
    validate = validate

    // aliases
    deleteRecord = removeRecord
    clearRecords = clearRecord
    countRecords = countRecord
    findRecords = findRecord
    findAllRecords = findAllRecord
    listAttachments = listAttachment
    bulkCreateRecords = bulkCreateRecord

    getField = (name) => this.getProperty(name)
    hasField = (name) => this.hasProperty(name)

    dispose = async () => {
      await super.dispose()
      this.connection = null
      this.adapter = null
    }
  }

  this.app.baseClass.DoboModel = DoboModel
}

export default modelFactory