sequelize
Version:
Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift and Snowflake’s Data Cloud. It features solid transaction support, relations, eager and lazy loading, read replication and more.
1,266 lines (1,059 loc) • 196 kB
JavaScript
'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.uniqno = 1;
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;
}
}
/**
* Returns the attributes of the model.
*
* @returns {object|any}
*/
static getAttributes() {
return this.rawAttributes;
}
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 (['where', 'having'].includes(key)) {
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.p