UNPKG

sequelize-ibmi

Version:

Multi dialect ORM for Node.JS

1,261 lines (1,054 loc) 194 kB
'use strict'; const assert = require('assert'); const _ = require('lodash'); const Dottie = require('dottie'); const Utils = require('./utils'); const { logger } = require('./utils/logger'); const BelongsTo = require('./associations/belongs-to'); const BelongsToMany = require('./associations/belongs-to-many'); const InstanceValidator = require('./instance-validator'); const QueryTypes = require('./query-types'); const sequelizeErrors = require('./errors'); const Association = require('./associations/base'); const HasMany = require('./associations/has-many'); const DataTypes = require('./data-types'); const Hooks = require('./hooks'); const associationsMixin = require('./associations/mixin'); const Op = require('./operators'); const { noDoubleNestedGroup } = require('./utils/deprecations'); // This list will quickly become dated, but failing to maintain this list just means // we won't throw a warning when we should. At least most common cases will forever be covered // so we stop throwing erroneous warnings when we shouldn't. const validQueryKeywords = new Set(['where', 'attributes', 'paranoid', 'include', 'order', 'limit', 'offset', 'transaction', 'lock', 'raw', 'logging', 'benchmark', 'having', 'searchPath', 'rejectOnEmpty', 'plain', 'scope', 'group', 'through', 'defaults', 'distinct', 'primary', 'exception', 'type', 'hooks', 'force', 'name']); // List of attributes that should not be implicitly passed into subqueries/includes. const nonCascadingOptions = ['include', 'attributes', 'originalAttributes', 'order', 'where', 'limit', 'offset', 'plain', 'group', 'having']; /** * A Model represents a table in the database. Instances of this class represent a database row. * * Model instances operate with the concept of a `dataValues` property, which stores the actual values represented by the instance. * By default, the values from dataValues can also be accessed directly from the Instance, that is: * ```js * instance.field * // is the same as * instance.get('field') * // is the same as * instance.getDataValue('field') * ``` * However, if getters and/or setters are defined for `field` they will be invoked, instead of returning the value from `dataValues`. * Accessing properties directly or using `get` is preferred for regular use, `getDataValue` should only be used for custom getters. * * @see * {@link Sequelize#define} for more information about getters and setters * @mixes Hooks */ class Model { static get queryInterface() { return this.sequelize.getQueryInterface(); } static get queryGenerator() { return this.queryInterface.queryGenerator; } /** * A reference to the sequelize instance * * @see * {@link Sequelize} * * @property sequelize * * @returns {Sequelize} */ get sequelize() { return this.constructor.sequelize; } /** * Builds a new model instance. * * @param {object} [values={}] an object of key value pairs * @param {object} [options] instance construction options * @param {boolean} [options.raw=false] If set to true, values will ignore field and virtual setters. * @param {boolean} [options.isNewRecord=true] Is this a new record * @param {Array} [options.include] an array of include options - Used to build prefetched/included model instances. See `set` */ constructor(values = {}, options = {}) { options = { isNewRecord: true, _schema: this.constructor._schema, _schemaDelimiter: this.constructor._schemaDelimiter, ...options }; if (options.attributes) { options.attributes = options.attributes.map(attribute => Array.isArray(attribute) ? attribute[1] : attribute); } if (!options.includeValidated) { this.constructor._conformIncludes(options, this.constructor); if (options.include) { this.constructor._expandIncludeAll(options); this.constructor._validateIncludedElements(options); } } this.dataValues = {}; this._previousDataValues = {}; this._changed = new Set(); this._options = options || {}; /** * Returns true if this instance has not yet been persisted to the database * * @property isNewRecord * @returns {boolean} */ this.isNewRecord = options.isNewRecord; this._initValues(values, options); } _initValues(values, options) { let defaults; let key; values = { ...values }; if (options.isNewRecord) { defaults = {}; if (this.constructor._hasDefaultValues) { defaults = _.mapValues(this.constructor._defaultValues, valueFn => { const value = valueFn(); return value && value instanceof Utils.SequelizeMethod ? value : _.cloneDeep(value); }); } // set id to null if not passed as value, a newly created dao has no id // removing this breaks bulkCreate // do after default values since it might have UUID as a default value if (this.constructor.primaryKeyAttributes.length) { this.constructor.primaryKeyAttributes.forEach(primaryKeyAttribute => { if (!Object.prototype.hasOwnProperty.call(defaults, primaryKeyAttribute)) { defaults[primaryKeyAttribute] = null; } }); } if (this.constructor._timestampAttributes.createdAt && defaults[this.constructor._timestampAttributes.createdAt]) { this.dataValues[this.constructor._timestampAttributes.createdAt] = Utils.toDefaultValue(defaults[this.constructor._timestampAttributes.createdAt], this.sequelize.options.dialect); delete defaults[this.constructor._timestampAttributes.createdAt]; } if (this.constructor._timestampAttributes.updatedAt && defaults[this.constructor._timestampAttributes.updatedAt]) { this.dataValues[this.constructor._timestampAttributes.updatedAt] = Utils.toDefaultValue(defaults[this.constructor._timestampAttributes.updatedAt], this.sequelize.options.dialect); delete defaults[this.constructor._timestampAttributes.updatedAt]; } if (this.constructor._timestampAttributes.deletedAt && defaults[this.constructor._timestampAttributes.deletedAt]) { this.dataValues[this.constructor._timestampAttributes.deletedAt] = Utils.toDefaultValue(defaults[this.constructor._timestampAttributes.deletedAt], this.sequelize.options.dialect); delete defaults[this.constructor._timestampAttributes.deletedAt]; } for (key in defaults) { if (values[key] === undefined) { this.set(key, Utils.toDefaultValue(defaults[key], this.sequelize.options.dialect), { raw: true }); delete values[key]; } } } this.set(values, options); } // validateIncludedElements should have been called before this method static _paranoidClause(model, options = {}) { // Apply on each include // This should be handled before handling where conditions because of logic with returns // otherwise this code will never run on includes of a already conditionable where if (options.include) { for (const include of options.include) { this._paranoidClause(include.model, include); } } // apply paranoid when groupedLimit is used if (_.get(options, 'groupedLimit.on.options.paranoid')) { const throughModel = _.get(options, 'groupedLimit.on.through.model'); if (throughModel) { options.groupedLimit.through = this._paranoidClause(throughModel, options.groupedLimit.through); } } if (!model.options.timestamps || !model.options.paranoid || options.paranoid === false) { // This model is not paranoid, nothing to do here; return options; } const deletedAtCol = model._timestampAttributes.deletedAt; const deletedAtAttribute = model.rawAttributes[deletedAtCol]; const deletedAtObject = {}; let deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; deletedAtDefaultValue = deletedAtDefaultValue || { [Op.eq]: null }; deletedAtObject[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue; if (Utils.isWhereEmpty(options.where)) { options.where = deletedAtObject; } else { options.where = { [Op.and]: [deletedAtObject, options.where] }; } return options; } static _addDefaultAttributes() { const tail = {}; let head = {}; // Add id if no primary key was manually added to definition // Can't use this.primaryKeys here, since this function is called before PKs are identified if (!_.some(this.rawAttributes, 'primaryKey')) { if ('id' in this.rawAttributes) { // Something is fishy here! throw new Error(`A column called 'id' was added to the attributes of '${this.tableName}' but not marked with 'primaryKey: true'`); } head = { id: { type: new DataTypes.INTEGER(), allowNull: false, primaryKey: true, autoIncrement: true, _autoGenerated: true } }; } if (this._timestampAttributes.createdAt) { tail[this._timestampAttributes.createdAt] = { type: DataTypes.DATE, allowNull: false, _autoGenerated: true }; } if (this._timestampAttributes.updatedAt) { tail[this._timestampAttributes.updatedAt] = { type: DataTypes.DATE, allowNull: false, _autoGenerated: true }; } if (this._timestampAttributes.deletedAt) { tail[this._timestampAttributes.deletedAt] = { type: DataTypes.DATE, _autoGenerated: true }; } if (this._versionAttribute) { tail[this._versionAttribute] = { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, _autoGenerated: true }; } const newRawAttributes = { ...head, ...this.rawAttributes }; _.each(tail, (value, attr) => { if (newRawAttributes[attr] === undefined) { newRawAttributes[attr] = value; } }); this.rawAttributes = newRawAttributes; if (!Object.keys(this.primaryKeys).length) { this.primaryKeys.id = this.rawAttributes.id; } } static _findAutoIncrementAttribute() { this.autoIncrementAttribute = null; for (const name in this.rawAttributes) { if (Object.prototype.hasOwnProperty.call(this.rawAttributes, name)) { const definition = this.rawAttributes[name]; if (definition && definition.autoIncrement) { if (this.autoIncrementAttribute) { throw new Error('Invalid Instance definition. Only one autoincrement field allowed.'); } this.autoIncrementAttribute = name; } } } } static _conformIncludes(options, self) { if (!options.include) return; // if include is not an array, wrap in an array if (!Array.isArray(options.include)) { options.include = [options.include]; } else if (!options.include.length) { delete options.include; return; } // convert all included elements to { model: Model } form options.include = options.include.map(include => this._conformInclude(include, self)); } static _transformStringAssociation(include, self) { if (self && typeof include === 'string') { if (!Object.prototype.hasOwnProperty.call(self.associations, include)) { throw new Error(`Association with alias "${include}" does not exist on ${self.name}`); } return self.associations[include]; } return include; } static _conformInclude(include, self) { if (include) { let model; if (include._pseudo) return include; include = this._transformStringAssociation(include, self); if (include instanceof Association) { if (self && include.target.name === self.name) { model = include.source; } else { model = include.target; } return { model, association: include, as: include.as }; } if (include.prototype && include.prototype instanceof Model) { return { model: include }; } if (_.isPlainObject(include)) { if (include.association) { include.association = this._transformStringAssociation(include.association, self); if (self && include.association.target.name === self.name) { model = include.association.source; } else { model = include.association.target; } if (!include.model) include.model = model; if (!include.as) include.as = include.association.as; this._conformIncludes(include, model); return include; } if (include.model) { this._conformIncludes(include, include.model); return include; } if (include.all) { this._conformIncludes(include); return include; } } } throw new Error('Include unexpected. Element has to be either a Model, an Association or an object.'); } static _expandIncludeAllElement(includes, include) { // check 'all' attribute provided is valid let all = include.all; delete include.all; if (all !== true) { if (!Array.isArray(all)) { all = [all]; } const validTypes = { BelongsTo: true, HasOne: true, HasMany: true, One: ['BelongsTo', 'HasOne'], Has: ['HasOne', 'HasMany'], Many: ['HasMany'] }; for (let i = 0; i < all.length; i++) { const type = all[i]; if (type === 'All') { all = true; break; } const types = validTypes[type]; if (!types) { throw new sequelizeErrors.EagerLoadingError(`include all '${type}' is not valid - must be BelongsTo, HasOne, HasMany, One, Has, Many or All`); } if (types !== true) { // replace type placeholder e.g. 'One' with its constituent types e.g. 'HasOne', 'BelongsTo' all.splice(i, 1); i--; for (let j = 0; j < types.length; j++) { if (!all.includes(types[j])) { all.unshift(types[j]); i++; } } } } } // add all associations of types specified to includes const nested = include.nested; if (nested) { delete include.nested; if (!include.include) { include.include = []; } else if (!Array.isArray(include.include)) { include.include = [include.include]; } } const used = []; (function addAllIncludes(parent, includes) { _.forEach(parent.associations, association => { if (all !== true && !all.includes(association.associationType)) { return; } // check if model already included, and skip if so const model = association.target; const as = association.options.as; const predicate = { model }; if (as) { // We only add 'as' to the predicate if it actually exists predicate.as = as; } if (_.some(includes, predicate)) { return; } // skip if recursing over a model already nested if (nested && used.includes(model)) { return; } used.push(parent); // include this model const thisInclude = Utils.cloneDeep(include); thisInclude.model = model; if (as) { thisInclude.as = as; } includes.push(thisInclude); // run recursively if nested if (nested) { addAllIncludes(model, thisInclude.include); if (thisInclude.include.length === 0) delete thisInclude.include; } }); used.pop(); })(this, includes); } static _validateIncludedElements(options, tableNames) { if (!options.model) options.model = this; tableNames = tableNames || {}; options.includeNames = []; options.includeMap = {}; /* Legacy */ options.hasSingleAssociation = false; options.hasMultiAssociation = false; if (!options.parent) { options.topModel = options.model; options.topLimit = options.limit; } options.include = options.include.map(include => { include = this._conformInclude(include); include.parent = options; include.topLimit = options.topLimit; this._validateIncludedElement.call(options.model, include, tableNames, options); if (include.duplicating === undefined) { include.duplicating = include.association.isMultiAssociation; } include.hasDuplicating = include.hasDuplicating || include.duplicating; include.hasRequired = include.hasRequired || include.required; options.hasDuplicating = options.hasDuplicating || include.hasDuplicating; options.hasRequired = options.hasRequired || include.required; options.hasWhere = options.hasWhere || include.hasWhere || !!include.where; return include; }); for (const include of options.include) { include.hasParentWhere = options.hasParentWhere || !!options.where; include.hasParentRequired = options.hasParentRequired || !!options.required; if (include.subQuery !== false && options.hasDuplicating && options.topLimit) { if (include.duplicating) { include.subQuery = include.subQuery || false; include.subQueryFilter = include.hasRequired; } else { include.subQuery = include.hasRequired; include.subQueryFilter = false; } } else { include.subQuery = include.subQuery || false; if (include.duplicating) { include.subQueryFilter = include.subQuery; } else { include.subQueryFilter = false; include.subQuery = include.subQuery || include.hasParentRequired && include.hasRequired && !include.separate; } } options.includeMap[include.as] = include; options.includeNames.push(include.as); // Set top level options if (options.topModel === options.model && options.subQuery === undefined && options.topLimit) { if (include.subQuery) { options.subQuery = include.subQuery; } else if (include.hasDuplicating) { options.subQuery = true; } } /* Legacy */ options.hasIncludeWhere = options.hasIncludeWhere || include.hasIncludeWhere || !!include.where; options.hasIncludeRequired = options.hasIncludeRequired || include.hasIncludeRequired || !!include.required; if (include.association.isMultiAssociation || include.hasMultiAssociation) { options.hasMultiAssociation = true; } if (include.association.isSingleAssociation || include.hasSingleAssociation) { options.hasSingleAssociation = true; } } if (options.topModel === options.model && options.subQuery === undefined) { options.subQuery = false; } return options; } static _validateIncludedElement(include, tableNames, options) { tableNames[include.model.getTableName()] = true; if (include.attributes && !options.raw) { include.model._expandAttributes(include); include.originalAttributes = include.model._injectDependentVirtualAttributes(include.attributes); include = Utils.mapFinderOptions(include, include.model); if (include.attributes.length) { _.each(include.model.primaryKeys, (attr, key) => { // Include the primary key if it's not already included - take into account that the pk might be aliased (due to a .field prop) if (!include.attributes.some(includeAttr => { if (attr.field !== key) { return Array.isArray(includeAttr) && includeAttr[0] === attr.field && includeAttr[1] === key; } return includeAttr === key; })) { include.attributes.unshift(key); } }); } } else { include = Utils.mapFinderOptions(include, include.model); } // pseudo include just needed the attribute logic, return if (include._pseudo) { if (!include.attributes) { include.attributes = Object.keys(include.model.tableAttributes); } return Utils.mapFinderOptions(include, include.model); } // check if the current Model is actually associated with the passed Model - or it's a pseudo include const association = include.association || this._getIncludedAssociation(include.model, include.as); include.association = association; include.as = association.as; // If through, we create a pseudo child include, to ease our parsing later on if (include.association.through && Object(include.association.through.model) === include.association.through.model) { if (!include.include) include.include = []; const through = include.association.through; include.through = _.defaults(include.through || {}, { model: through.model, as: through.model.name, association: { isSingleAssociation: true }, _pseudo: true, parent: include }); if (through.scope) { include.through.where = include.through.where ? { [Op.and]: [include.through.where, through.scope] } : through.scope; } include.include.push(include.through); tableNames[through.tableName] = true; } // include.model may be the main model, while the association target may be scoped - thus we need to look at association.target/source let model; if (include.model.scoped === true) { // If the passed model is already scoped, keep that model = include.model; } else { // Otherwise use the model that was originally passed to the association model = include.association.target.name === include.model.name ? include.association.target : include.association.source; } model._injectScope(include); // This check should happen after injecting the scope, since the scope may contain a .attributes if (!include.attributes) { include.attributes = Object.keys(include.model.tableAttributes); } include = Utils.mapFinderOptions(include, include.model); if (include.required === undefined) { include.required = !!include.where; } if (include.association.scope) { include.where = include.where ? { [Op.and]: [include.where, include.association.scope] } : include.association.scope; } if (include.limit && include.separate === undefined) { include.separate = true; } if (include.separate === true) { if (!(include.association instanceof HasMany)) { throw new Error('Only HasMany associations support include.separate'); } include.duplicating = false; if ( options.attributes && options.attributes.length && !_.flattenDepth(options.attributes, 2).includes(association.sourceKey) ) { options.attributes.push(association.sourceKey); } if ( include.attributes && include.attributes.length && !_.flattenDepth(include.attributes, 2).includes(association.foreignKey) ) { include.attributes.push(association.foreignKey); } } // Validate child includes if (Object.prototype.hasOwnProperty.call(include, 'include')) { this._validateIncludedElements.call(include.model, include, tableNames); } return include; } static _getIncludedAssociation(targetModel, targetAlias) { const associations = this.getAssociations(targetModel); let association = null; if (associations.length === 0) { throw new sequelizeErrors.EagerLoadingError(`${targetModel.name} is not associated to ${this.name}!`); } if (associations.length === 1) { association = this.getAssociationForAlias(targetModel, targetAlias); if (association) { return association; } if (targetAlias) { const existingAliases = this.getAssociations(targetModel).map(association => association.as); throw new sequelizeErrors.EagerLoadingError(`${targetModel.name} is associated to ${this.name} using an alias. ` + `You've included an alias (${targetAlias}), but it does not match the alias(es) defined in your association (${existingAliases.join(', ')}).`); } throw new sequelizeErrors.EagerLoadingError(`${targetModel.name} is associated to ${this.name} using an alias. ` + 'You must use the \'as\' keyword to specify the alias within your include statement.'); } association = this.getAssociationForAlias(targetModel, targetAlias); if (!association) { throw new sequelizeErrors.EagerLoadingError(`${targetModel.name} is associated to ${this.name} multiple times. ` + 'To identify the correct association, you must use the \'as\' keyword to specify the alias of the association you want to include.'); } return association; } static _expandIncludeAll(options) { const includes = options.include; if (!includes) { return; } for (let index = 0; index < includes.length; index++) { const include = includes[index]; if (include.all) { includes.splice(index, 1); index--; this._expandIncludeAllElement(includes, include); } } includes.forEach(include => { this._expandIncludeAll.call(include.model, include); }); } static _conformIndex(index) { if (!index.fields) { throw new Error('Missing "fields" property for index definition'); } index = _.defaults(index, { type: '', parser: null }); if (index.type && index.type.toLowerCase() === 'unique') { index.unique = true; delete index.type; } return index; } static _uniqIncludes(options) { if (!options.include) return; options.include = _(options.include) .groupBy(include => `${include.model && include.model.name}-${include.as}`) .map(includes => this._assignOptions(...includes)) .value(); } static _baseMerge(...args) { _.assignWith(...args); this._conformIncludes(args[0], this); this._uniqIncludes(args[0]); return args[0]; } static _mergeFunction(objValue, srcValue, key) { if (Array.isArray(objValue) && Array.isArray(srcValue)) { return _.union(objValue, srcValue); } if (key === 'where' || key === 'having') { if (srcValue instanceof Utils.SequelizeMethod) { srcValue = { [Op.and]: srcValue }; } if (_.isPlainObject(objValue) && _.isPlainObject(srcValue)) { return Object.assign(objValue, srcValue); } } else if (key === 'attributes' && _.isPlainObject(objValue) && _.isPlainObject(srcValue)) { return _.assignWith(objValue, srcValue, (objValue, srcValue) => { if (Array.isArray(objValue) && Array.isArray(srcValue)) { return _.union(objValue, srcValue); } }); } // If we have a possible object/array to clone, we try it. // Otherwise, we return the original value when it's not undefined, // or the resulting object in that case. if (srcValue) { return Utils.cloneDeep(srcValue, true); } return srcValue === undefined ? objValue : srcValue; } static _assignOptions(...args) { return this._baseMerge(...args, this._mergeFunction); } static _defaultsOptions(target, opts) { return this._baseMerge(target, opts, (srcValue, objValue, key) => { return this._mergeFunction(objValue, srcValue, key); }); } /** * Initialize a model, representing a table in the DB, with attributes and options. * * The table columns are defined by the hash that is given as the first argument. * Each attribute of the hash represents a column. * * @example * Project.init({ * columnA: { * type: Sequelize.BOOLEAN, * validate: { * is: ['[a-z]','i'], // will only allow letters * max: 23, // only allow values <= 23 * isIn: { * args: [['en', 'zh']], * msg: "Must be English or Chinese" * } * }, * field: 'column_a' * // Other attributes here * }, * columnB: Sequelize.STRING, * columnC: 'MY VERY OWN COLUMN TYPE' * }, {sequelize}) * * sequelize.models.modelName // The model will now be available in models under the class name * * @see * <a href="/master/manual/model-basics.html">Model Basics</a> guide * * @see * <a href="/master/manual/model-basics.html">Hooks</a> guide * * @see * <a href="/master/manual/validations-and-constraints.html"/>Validations & Constraints</a> guide * * @param {object} attributes An object, where each attribute is a column of the table. Each column can be either a DataType, a string or a type-description object, with the properties described below: * @param {string|DataTypes|object} attributes.column The description of a database column * @param {string|DataTypes} attributes.column.type A string or a data type * @param {boolean} [attributes.column.allowNull=true] If false, the column will have a NOT NULL constraint, and a not null validation will be run before an instance is saved. * @param {any} [attributes.column.defaultValue=null] A literal default value, a JavaScript function, or an SQL function (see `sequelize.fn`) * @param {string|boolean} [attributes.column.unique=false] If true, the column will get a unique constraint. If a string is provided, the column will be part of a composite unique index. If multiple columns have the same string, they will be part of the same unique index * @param {boolean} [attributes.column.primaryKey=false] If true, this attribute will be marked as primary key * @param {string} [attributes.column.field=null] If set, sequelize will map the attribute name to a different name in the database * @param {boolean} [attributes.column.autoIncrement=false] If true, this column will be set to auto increment * @param {boolean} [attributes.column.autoIncrementIdentity=false] If true, combined with autoIncrement=true, will use Postgres `GENERATED BY DEFAULT AS IDENTITY` instead of `SERIAL`. Postgres 10+ only. * @param {string} [attributes.column.comment=null] Comment for this column * @param {string|Model} [attributes.column.references=null] An object with reference configurations * @param {string|Model} [attributes.column.references.model] If this column references another table, provide it here as a Model, or a string * @param {string} [attributes.column.references.key='id'] The column of the foreign table that this column references * @param {string} [attributes.column.onUpdate] What should happen when the referenced key is updated. One of CASCADE, RESTRICT, SET DEFAULT, SET NULL or NO ACTION * @param {string} [attributes.column.onDelete] What should happen when the referenced key is deleted. One of CASCADE, RESTRICT, SET DEFAULT, SET NULL or NO ACTION * @param {Function} [attributes.column.get] Provide a custom getter for this column. Use `this.getDataValue(String)` to manipulate the underlying values. * @param {Function} [attributes.column.set] Provide a custom setter for this column. Use `this.setDataValue(String, Value)` to manipulate the underlying values. * @param {object} [attributes.column.validate] An object of validations to execute for this column every time the model is saved. Can be either the name of a validation provided by validator.js, a validation function provided by extending validator.js (see the `DAOValidator` property for more details), or a custom validation function. Custom validation functions are called with the value of the field and the instance itself as the `this` binding, and can possibly take a second callback argument, to signal that they are asynchronous. If the validator is sync, it should throw in the case of a failed validation; if it is async, the callback should be called with the error text. * @param {object} options These options are merged with the default define options provided to the Sequelize constructor * @param {object} options.sequelize Define the sequelize instance to attach to the new Model. Throw error if none is provided. * @param {string} [options.modelName] Set name of the model. By default its same as Class name. * @param {object} [options.defaultScope={}] Define the default search scope to use for this model. Scopes have the same form as the options passed to find / findAll * @param {object} [options.scopes] More scopes, defined in the same way as defaultScope above. See `Model.scope` for more information about how scopes are defined, and what you can do with them * @param {boolean} [options.omitNull] Don't persist null values. This means that all columns with null values will not be saved * @param {boolean} [options.timestamps=true] Adds createdAt and updatedAt timestamps to the model. * @param {boolean} [options.paranoid=false] Calling `destroy` will not delete the model, but instead set a `deletedAt` timestamp if this is true. Needs `timestamps=true` to work * @param {boolean} [options.underscored=false] Add underscored field to all attributes, this covers user defined attributes, timestamps and foreign keys. Will not affect attributes with explicitly set `field` option * @param {boolean} [options.freezeTableName=false] If freezeTableName is true, sequelize will not try to alter the model name to get the table name. Otherwise, the model name will be pluralized * @param {object} [options.name] An object with two attributes, `singular` and `plural`, which are used when this model is associated to others. * @param {string} [options.name.singular=Utils.singularize(modelName)] Singular name for model * @param {string} [options.name.plural=Utils.pluralize(modelName)] Plural name for model * @param {Array<object>} [options.indexes] indexes definitions * @param {string} [options.indexes[].name] The name of the index. Defaults to model name + _ + fields concatenated * @param {string} [options.indexes[].type] Index type. Only used by mysql. One of `UNIQUE`, `FULLTEXT` and `SPATIAL` * @param {string} [options.indexes[].using] The method to create the index by (`USING` statement in SQL). BTREE and HASH are supported by mysql and postgres, and postgres additionally supports GIST and GIN. * @param {string} [options.indexes[].operator] Specify index operator. * @param {boolean} [options.indexes[].unique=false] Should the index by unique? Can also be triggered by setting type to `UNIQUE` * @param {boolean} [options.indexes[].concurrently=false] PostgresSQL will build the index without taking any write locks. Postgres only * @param {Array<string|object>} [options.indexes[].fields] An array of the fields to index. Each field can either be a string containing the name of the field, a sequelize object (e.g `sequelize.fn`), or an object with the following attributes: `attribute` (field name), `length` (create a prefix index of length chars), `order` (the direction the column should be sorted in), `collate` (the collation (sort order) for the column) * @param {string|boolean} [options.createdAt] Override the name of the createdAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting. * @param {string|boolean} [options.updatedAt] Override the name of the updatedAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting. * @param {string|boolean} [options.deletedAt] Override the name of the deletedAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting. * @param {string} [options.tableName] Defaults to pluralized model name, unless freezeTableName is true, in which case it uses model name verbatim * @param {string} [options.schema='public'] schema * @param {string} [options.engine] Specify engine for model's table * @param {string} [options.charset] Specify charset for model's table * @param {string} [options.comment] Specify comment for model's table * @param {string} [options.collate] Specify collation for model's table * @param {string} [options.initialAutoIncrement] Set the initial AUTO_INCREMENT value for the table in MySQL. * @param {object} [options.hooks] An object of hook function that are called before and after certain lifecycle events. The possible hooks are: beforeValidate, afterValidate, validationFailed, beforeBulkCreate, beforeBulkDestroy, beforeBulkUpdate, beforeCreate, beforeDestroy, beforeUpdate, afterCreate, beforeSave, afterDestroy, afterUpdate, afterBulkCreate, afterSave, afterBulkDestroy and afterBulkUpdate. See Hooks for more information about hook functions and their signatures. Each property can either be a function, or an array of functions. * @param {object} [options.validate] An object of model wide validations. Validations have access to all model values via `this`. If the validator function takes an argument, it is assumed to be async, and is called with a callback that accepts an optional error. * * @returns {Model} */ static init(attributes, options = {}) { if (!options.sequelize) { throw new Error('No Sequelize instance passed'); } this.sequelize = options.sequelize; const globalOptions = this.sequelize.options; options = Utils.merge(_.cloneDeep(globalOptions.define), options); if (!options.modelName) { options.modelName = this.name; } options = Utils.merge({ name: { plural: Utils.pluralize(options.modelName), singular: Utils.singularize(options.modelName) }, indexes: [], omitNull: globalOptions.omitNull, schema: globalOptions.schema }, options); this.sequelize.runHooks('beforeDefine', attributes, options); if (options.modelName !== this.name) { Object.defineProperty(this, 'name', { value: options.modelName }); } delete options.modelName; this.options = { timestamps: true, validate: {}, freezeTableName: false, underscored: false, paranoid: false, rejectOnEmpty: false, whereCollection: null, schema: null, schemaDelimiter: '', defaultScope: {}, scopes: {}, indexes: [], ...options }; // if you call "define" multiple times for the same modelName, do not clutter the factory if (this.sequelize.isDefined(this.name)) { this.sequelize.modelManager.removeModel(this.sequelize.modelManager.getModel(this.name)); } this.associations = {}; this._setupHooks(options.hooks); this.underscored = this.options.underscored; if (!this.options.tableName) { this.tableName = this.options.freezeTableName ? this.name : Utils.underscoredIf(Utils.pluralize(this.name), this.underscored); } else { this.tableName = this.options.tableName; } this._schema = this.options.schema; this._schemaDelimiter = this.options.schemaDelimiter; // error check options _.each(options.validate, (validator, validatorType) => { if (Object.prototype.hasOwnProperty.call(attributes, validatorType)) { throw new Error(`A model validator function must not have the same name as a field. Model: ${this.name}, field/validation name: ${validatorType}`); } if (typeof validator !== 'function') { throw new Error(`Members of the validate option must be functions. Model: ${this.name}, error with validate member ${validatorType}`); } }); this.rawAttributes = _.mapValues(attributes, (attribute, name) => { attribute = this.sequelize.normalizeAttribute(attribute); if (attribute.type === undefined) { throw new Error(`Unrecognized datatype for attribute "${this.name}.${name}"`); } if (attribute.allowNull !== false && _.get(attribute, 'validate.notNull')) { throw new Error(`Invalid definition for "${this.name}.${name}", "notNull" validator is only allowed with "allowNull:false"`); } if (_.get(attribute, 'references.model.prototype') instanceof Model) { attribute.references.model = attribute.references.model.getTableName(); } return attribute; }); const tableName = this.getTableName(); this._indexes = this.options.indexes .map(index => Utils.nameIndex(this._conformIndex(index), tableName)); this.primaryKeys = {}; this._readOnlyAttributes = new Set(); this._timestampAttributes = {}; // setup names of timestamp attributes if (this.options.timestamps) { for (const key of ['createdAt', 'updatedAt', 'deletedAt']) { if (!['undefined', 'string', 'boolean'].includes(typeof this.options[key])) { throw new Error(`Value for "${key}" option must be a string or a boolean, got ${typeof this.options[key]}`); } if (this.options[key] === '') { throw new Error(`Value for "${key}" option cannot be an empty string`); } } if (this.options.createdAt !== false) { this._timestampAttributes.createdAt = typeof this.options.createdAt === 'string' ? this.options.createdAt : 'createdAt'; this._readOnlyAttributes.add(this._timestampAttributes.createdAt); } if (this.options.updatedAt !== false) { this._timestampAttributes.updatedAt = typeof this.options.updatedAt === 'string' ? this.options.updatedAt : 'updatedAt'; this._readOnlyAttributes.add(this._timestampAttributes.updatedAt); } if (this.options.paranoid && this.options.deletedAt !== false) { this._timestampAttributes.deletedAt = typeof this.options.deletedAt === 'string' ? this.options.deletedAt : 'deletedAt'; this._readOnlyAttributes.add(this._timestampAttributes.deletedAt); } } // setup name for version attribute if (this.options.version) { this._versionAttribute = typeof this.options.version === 'string' ? this.options.version : 'version'; this._readOnlyAttributes.add(this._versionAttribute); } this._hasReadOnlyAttributes = this._readOnlyAttributes.size > 0; // Add head and tail default attributes (id, timestamps) this._addDefaultAttributes(); this.refreshAttributes(); this._findAutoIncrementAttribute(); this._scope = this.options.defaultScope; this._scopeNames = ['defaultScope']; this.sequelize.modelManager.addModel(this); this.sequelize.runHooks('afterDefine', this); return this; } static refreshAttributes() { const attributeManipulation = {}; this.prototype._customGetters = {}; this.prototype._customSetters = {}; ['get', 'set'].forEach(type => { const opt = `${type}terMethods`; const funcs = { ...this.options[opt] }; const _custom = type === 'get' ? this.prototype._customGetters : this.prototype._customSetters; _.each(funcs, (method, attribute) => { _custom[attribute] = method; if (type === 'get') { funcs[attribute] = function() { return this.get(attribute); }; } if (type === 'set') { funcs[attribute] = function(value) { return this.set(attribute, value); }; } }); _.each(this.rawAttributes, (options, attribute) => { if (Object.prototype.hasOwnProperty.call(options, type)) { _custom[attribute] = options[type]; } if (type === 'get') { funcs[attribute] = function() { return this.get(attribute); }; } if (type === 'set') { funcs[attribute] = function(value) { return this.set(attribute, value); }; } }); _.each(funcs, (fct, name) => { if (!attributeManipulation[name]) { attributeManipulation[name] = { configurable: true }; } attributeManipulation[name][type] = fct; }); }); this._dataTypeChanges = {}; this._dataTypeSanitizers = {}; this._hasBooleanAttributes = false; this._hasDateAttributes = false; this._jsonAttributes = new Set(); this._virtualAttributes = new Set(); this._defaultValues = {}; this.prototype.validators = {}; this.fieldRawAttributesMap = {}; this.primaryKeys = {}; this.uniqueKeys = {}; _.each(this.rawAttributes, (definition, name) => { definition.type = this.sequelize.normalizeDataType(definition.type); definition.Model = this; definition.fieldName = name; definition._modelAttribute = true; if (definition.field === undefined) { definition.field = Utils.underscoredIf(name, this.underscored); } if (definition.primaryKey === true) { this.primaryKeys[name] = definition; } this.fieldRawAttributesMap[definition.field] = definition; if (definition.type._sanitize) { this._dataTypeSanitizers[name] = definition.type._sanitize; } if (definition.type._isChanged) { this._dataTypeChanges[name] = definition.type._isChanged; } if (definition.type instanceof DataTypes.BOOLEAN) { this._hasBooleanAttributes = true; } else if (definition.type instanceof DataTypes.DATE || definition.type instanceof DataTypes.DATEONLY) { this._hasDateAttributes = true; } else if (definition.type instanceof DataTypes.JSON) { this._jsonAttributes.add(name); } else if (definition.type instanceof DataTypes.VIRTUAL) { this._virtualAttributes.add(name); } if (Object.prototype.hasOwnProperty.call(definition, 'defaultValue')) { this._defaultValues[name] = () => Utils.toDefaultValue(definition.defaultValue, this.sequelize.options.dialect); } if (Object.prototype.hasOwnProperty.call(definition, 'unique') && definition.unique) { let idxName; if ( typeof definition.unique === 'object' && Object.prototype.hasOwnProperty.call(definition.unique, 'name') ) { idxName = definition.unique.name; } else if (typeof definition.unique === 'string') { idxName = definition.unique; } else { idxName = `${this.tableName}_${name}_unique`; } const idx = this.uniqueKeys[idxName] || { fields: [] }; idx.fields.push(definition.field); idx.msg = idx.msg || definition.unique.msg || null; idx.name = idxName || false; idx.column = name; idx.customIndex = definition.unique !== true; this.uniqueKeys[idxName] = idx; } if (Object.prototype.hasOwnProperty.call(definition, 'validate')) { this.prototype.validators[name] = definition.validate; } if (definition.index === true && definition.type instanceof DataTypes.JSONB) { this._indexes.push( Utils.nameIndex( this._conformIndex({ fields: [definition.field || name], using: 'gin' }), this.getTableName() ) ); delete definition.index; } }); // Create a map of field to attribute names this.fieldAttributeMap = _.reduce(this.fieldRawAttributesMap, (map, value, key) => { if (key !== value.fieldName) { map[key] = value.fieldName; } return map; }, {}); this._hasJsonAttributes = !!this._jsonAttributes.size; this._hasVirtualAttributes = !!this._virtualAttributes.size; this._hasDefaultValues = !_.isEmpty(this._defaultValues); this.tableAttributes = _.omitBy(this.rawAttributes, (_a, key) => this._virtualAttributes.has(key)); this.prototype._hasCustomGetters = Object.keys(this.prototype._customGetters).length; this.prototype._hasCustomSetters = Object.keys(this.prototype._customSetters).length; for (const key of Object.keys(attributeManipulation)) { if (Object.prototype.hasOwnProperty.call(Model.prototype, key)) { this.sequelize.log(`Not overriding built-in method from model attribute: ${key}`); continue; } Object.defineProperty(this.prototype, key, attributeManipulation[key]); } this.prototype.rawAttributes = this.rawAttributes; this.prototype._isAttribute = key => Object.prototype.hasOwnProperty.call(this.prototype.rawAttributes, key); // Primary key convenience constiables this.primaryKeyAttributes = Object.keys(this.primaryKeys); this.primaryKeyAttribute = this.primaryKeyAttributes[0]; if (this.primaryKeyAttribute) { this.primaryKeyField = this.rawAttributes[this.primaryKeyAttribute].field || this.primaryKeyAttribute; } this._hasPrimaryKeys = this.primaryKeyAttributes.length > 0; this._isPrimaryKey = key => this.primaryKeyAttributes.includes(key); }