sequelize-ibmi
Version:
Multi dialect ORM for Node.JS
1,261 lines (1,054 loc) • 194 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._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);
}