From f57e5a0293ea48e077939deeb6529aa4772ff4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=A9?= Date: Sun, 18 Dec 2022 14:31:55 +0100 Subject: [PATCH] feat: migrate model definitions to TypeScript (#15431) BREAKING CHANGE: Type `ModelAttributeColumnOptions` has been renamed `AttributeOptions` BREAKING CHANGE: `setterMethods` and `getterMethods` model options, which were deprecated in a previous major version, have been removed. See https://sequelize.org/docs/v6/core-concepts/getters-setters-virtuals/#deprecated-in-sequelize-v7-gettermethods-and-settermethods for alternatives. BREAKING CHANGE: In the "references" attribute option, the "model" option has been split in two options: "table" and "model". "table" references a database table, and "model" references a Sequelize model. BREAKING CHANGE: Sequelize will now throw if you try to define an attribute that conflicts (i.e. its options are incompatible) with one of the automatic timestamp or version attributes that are added on Models by Sequelize. BREAKING CHANGE: `Model#validators` and many private state and methods have been removed from models and moved to `Model.modelDefinition` --- .eslintrc.js | 4 + src/associations/base.ts | 7 +- src/associations/belongs-to-many.ts | 29 +- src/associations/belongs-to.ts | 61 +- src/associations/has-many.ts | 9 + src/associations/has-one.ts | 2 + src/associations/helpers.ts | 28 +- src/decorators/legacy/attribute-utils.ts | 8 +- src/decorators/legacy/attribute.ts | 8 +- src/decorators/legacy/index.mjs | 2 + src/decorators/legacy/model-hooks.ts | 16 +- src/decorators/legacy/table.ts | 1 - src/decorators/shared/model.ts | 53 +- src/dialects/abstract/data-types.ts | 8 +- .../abstract/query-generator-typescript.ts | 4 + src/dialects/abstract/query-generator.d.ts | 18 +- src/dialects/abstract/query-generator.js | 101 +- src/dialects/abstract/query-interface.d.ts | 27 +- src/dialects/abstract/query-interface.js | 69 +- src/dialects/abstract/query.js | 40 +- src/dialects/db2/query-generator.js | 44 +- src/dialects/db2/query-interface.js | 18 +- src/dialects/db2/query.js | 32 +- src/dialects/ibmi/query-generator.js | 43 +- src/dialects/ibmi/query.js | 7 +- src/dialects/mariadb/query.js | 14 +- src/dialects/mssql/query-generator.js | 73 +- src/dialects/mssql/query-interface.js | 5 +- src/dialects/mssql/query.js | 8 +- src/dialects/mysql/query-generator.js | 13 +- src/dialects/mysql/query-interface.js | 8 +- src/dialects/mysql/query.js | 12 +- src/dialects/postgres/query-generator.js | 25 +- src/dialects/postgres/query-interface.js | 23 +- src/dialects/postgres/query.js | 30 +- src/dialects/snowflake/query-generator.js | 12 +- src/dialects/snowflake/query-interface.js | 4 +- src/dialects/snowflake/query.js | 24 +- src/dialects/sqlite/query-generator.js | 41 +- src/dialects/sqlite/query-interface.js | 50 +- src/dialects/sqlite/query.js | 59 +- src/hooks-legacy.ts | 45 +- src/hooks.ts | 14 +- src/instance-validator.js | 65 +- src/model-definition.ts | 893 ++++++++++++ src/model-hooks.ts | 135 ++ src/model-internals.ts | 16 + src/model-manager.d.ts | 5 +- src/model-manager.js | 32 +- src/model-repository.ts | 38 + src/model-typescript.ts | 470 +++++-- src/model.d.ts | 280 +--- src/model.js | 1247 +++++------------ src/sequelize-typescript.ts | 4 +- src/sequelize.d.ts | 11 +- src/sequelize.js | 26 +- src/utils/deprecations.ts | 2 + src/utils/format.ts | 32 +- src/utils/immutability.ts | 106 ++ src/utils/iterators.ts | 36 + src/utils/model-utils.ts | 8 +- src/utils/object.ts | 29 +- src/utils/types.ts | 2 + test/integration/als.test.ts | 2 +- .../associations/belongs-to-many.test.js | 72 +- .../associations/belongs-to.test.js | 45 +- .../integration/associations/has-many.test.js | 45 +- test/integration/associations/has-one.test.js | 22 +- test/integration/associations/self.test.js | 6 +- test/integration/data-types/methods.test.ts | 2 +- .../dialects/mariadb/dao-factory.test.js | 20 +- .../dialects/mysql/dao-factory.test.js | 20 +- .../dialects/postgres/query.test.js | 2 +- test/integration/hooks/hooks.test.js | 2 +- test/integration/include.test.js | 2 +- test/integration/include/schema.test.js | 2 +- test/integration/index.test.ts | 12 +- test/integration/instance/save.test.js | 8 - test/integration/instance/update.test.js | 2 - test/integration/instance/values.test.js | 130 -- test/integration/model.test.js | 118 +- .../model/attributes/field.test.js | 7 +- test/integration/model/bulk-create.test.js | 5 - .../model/bulk-create/include.test.js | 2 +- test/integration/model/create.test.js | 6 +- test/integration/model/create/include.test.js | 2 +- test/integration/model/json.test.js | 5 +- test/integration/model/paranoid.test.js | 1 - test/integration/model/schema.test.js | 4 +- test/integration/model/sync.test.js | 11 +- test/integration/model/upsert.test.js | 2 +- test/integration/query-interface.test.js | 8 +- .../query-interface/changeColumn.test.js | 42 +- .../query-interface/removeColumn.test.js | 6 +- test/integration/sequelize.test.js | 61 +- test/integration/sequelize/deferrable.test.js | 6 +- test/integration/sequelize/drop.test.ts | 2 - test/smoke/smoketests.test.js | 2 +- test/types/hooks.ts | 2 +- test/types/model.ts | 5 - test/types/models/user.ts | 10 - test/types/query-interface.ts | 11 +- .../unit/associations/belongs-to-many.test.ts | 120 +- test/unit/associations/belongs-to.test.ts | 7 +- .../associations/dont-modify-options.test.js | 8 +- test/unit/associations/has-one.test.ts | 41 +- test/unit/data-types/_utils.ts | 2 +- test/unit/data-types/misc-data-types.test.ts | 2 +- test/unit/decorators/attribute.test.ts | 10 +- test/unit/decorators/hooks.test.ts | 24 +- test/unit/decorators/table.test.ts | 54 +- .../unit/dialects/db2/query-generator.test.js | 12 +- .../dialects/mariadb/query-generator.test.js | 24 +- .../dialects/mysql/query-generator.test.js | 24 +- .../dialects/postgres/query-generator.test.js | 20 +- .../snowflake/query-generator.test.js | 48 +- .../dialects/sqlite/query-generator.test.js | 12 +- test/unit/hooks.test.js | 45 +- test/unit/instance/build.test.js | 7 +- test/unit/instance/decrement.test.js | 10 +- test/unit/instance/reload.test.js | 10 +- test/unit/instance/restore.test.js | 4 +- test/unit/model/define.test.js | 55 +- test/unit/model/get-attributes.test.ts | 4 +- test/unit/model/indexes.test.js | 77 - test/unit/model/indexes.test.ts | 164 +++ test/unit/model/init.test.ts | 4 - test/unit/model/remove-attribute.test.ts | 4 +- test/unit/model/schema.test.ts | 28 +- test/unit/model/underscored.test.js | 40 +- test/unit/model/validation.test.js | 6 +- .../unit/query-generator/insert-query.test.ts | 6 +- test/unit/sql/add-column.test.js | 2 +- test/unit/sql/change-column.test.js | 2 +- test/unit/sql/create-table.test.js | 6 +- test/unit/sql/enum.test.js | 8 +- test/unit/sql/insert.test.js | 16 +- test/unit/sql/order.test.js | 20 - test/unit/sql/select.test.js | 6 +- test/unit/sql/update.test.js | 4 +- test/unit/utils/utils.test.ts | 12 +- 141 files changed, 3364 insertions(+), 2832 deletions(-) create mode 100644 src/model-definition.ts create mode 100644 src/model-hooks.ts create mode 100644 src/model-repository.ts create mode 100644 src/utils/immutability.ts delete mode 100644 test/unit/model/indexes.test.js create mode 100644 test/unit/model/indexes.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 8bf6eec3bb4b..6a3897bc7aa8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,9 +38,13 @@ module.exports = { 'prefer-object-has-own': 'off', 'unicorn/prefer-at': 'off', 'unicorn/prefer-string-replace-all': 'off', + 'consistent-return': 'off', // This rule is incompatible with DataTypes 'babel/new-cap': 'off', + + // Too opinionated + 'unicorn/prefer-set-has': 'off', }, overrides: [{ files: ['**/*.{js,mjs,cjs}'], diff --git a/src/associations/base.ts b/src/associations/base.ts index a638eb7e1f39..7e9c53d17a72 100644 --- a/src/associations/base.ts +++ b/src/associations/base.ts @@ -1,5 +1,5 @@ import type { Optional } from '../index.js'; -import type { Model, ModelStatic, Hookable, AttributeNames, ModelAttributeColumnOptions } from '../model'; +import type { Model, ModelStatic, Hookable, AttributeNames, AttributeOptions } from '../model'; import { cloneDeep } from '../utils/object.js'; import type { AllowArray } from '../utils/types.js'; import type { NormalizeBaseAssociationOptions } from './helpers'; @@ -221,6 +221,7 @@ export abstract class MultiAssociation< } const tmpInstance = Object.create(null); + // @ts-expect-error -- TODO: what if the target has no primary key? tmpInstance[this.target.primaryKeyAttribute] = element; return this.target.build(tmpInstance, { isNewRecord: false }); @@ -248,11 +249,11 @@ export type MultiAssociationAccessors = { }; /** Foreign Key Options */ -export interface ForeignKeyOptions extends Optional { +export interface ForeignKeyOptions extends Optional { /** * The name of the foreign key attribute. * - * Not to be confused with {@link ModelAttributeColumnOptions#field} which controls the name of the foreign key Column. + * Not to be confused with {@link AttributeOptions#columnName} which controls the name of the foreign key Column. */ name?: ForeignKey; diff --git a/src/associations/belongs-to-many.ts b/src/associations/belongs-to-many.ts index df57b378b570..8011c1ff1dc9 100644 --- a/src/associations/belongs-to-many.ts +++ b/src/associations/belongs-to-many.ts @@ -339,9 +339,13 @@ export class BelongsToMany< #makeFkPairUnique() { let hasPrimaryKey = false; + const throughModelDefinition = this.throughModel.modelDefinition; + // remove any PKs previously defined by sequelize // but ignore any keys that are part of this association (#5865) - each(this.through.model.rawAttributes, (attribute, attributeName) => { + const { rawAttributes: throughRawAttributes } = throughModelDefinition; + + each(throughRawAttributes, (attribute, attributeName) => { if (!attribute.primaryKey) { return; } @@ -351,7 +355,7 @@ export class BelongsToMany< } if (attribute._autoGenerated) { - delete this.through.model.rawAttributes[attributeName]; + delete throughRawAttributes[attributeName]; return; } @@ -365,22 +369,22 @@ export class BelongsToMany< Add your own primary key to the through model, on different attributes than the foreign keys, to be able to use this option.`); } - this.throughModel.rawAttributes[this.foreignKey].primaryKey = true; - this.throughModel.rawAttributes[this.otherKey].primaryKey = true; + throughRawAttributes[this.foreignKey].primaryKey = true; + throughRawAttributes[this.otherKey].primaryKey = true; } else if (this.through.unique !== false) { let uniqueKey; if (typeof this.through.unique === 'string' && this.through.unique !== '') { uniqueKey = this.through.unique; } else { const keys = [this.foreignKey, this.otherKey].sort(); - uniqueKey = [this.through.model.tableName, ...keys, 'unique'].join('_'); + uniqueKey = [this.through.model.table.tableName, ...keys, 'unique'].join('_'); } - this.throughModel.rawAttributes[this.foreignKey].unique = [{ name: uniqueKey }]; - this.throughModel.rawAttributes[this.otherKey].unique = [{ name: uniqueKey }]; + throughRawAttributes[this.foreignKey].unique = [{ name: uniqueKey }]; + throughRawAttributes[this.otherKey].unique = [{ name: uniqueKey }]; } - this.throughModel.refreshAttributes(); + throughModelDefinition.refreshAttributes(); } static associate< @@ -843,17 +847,16 @@ function normalizeThroughOptions( } else if (sequelize.isDefined(through.model)) { model = sequelize.model(through.model); } else { + const sourceTable = source.table; + model = sequelize.define(through.model, {} as ModelAttributes, removeUndefined({ tableName: through.model, indexes: [], // we don't want indexes here (as referenced in #2416) paranoid: through.paranoid || false, // Default to non-paranoid join (referenced in #11991) validate: {}, // Don't propagate model-level validations timestamps: through.timestamps, - - // @ts-expect-error -- TODO: make 'schema' a public property on Model once the method has been removed (sequelize 8) - schema: source._schema, - // @ts-expect-error -- TODO: either remove or make a public readonly property - schemaDelimiter: source._schemaDelimiter, + schema: sourceTable.schema, + schemaDelimiter: sourceTable.delimiter, })); } diff --git a/src/associations/belongs-to.ts b/src/associations/belongs-to.ts index ebe776e08ce2..00804167af9c 100644 --- a/src/associations/belongs-to.ts +++ b/src/associations/belongs-to.ts @@ -1,4 +1,5 @@ import assert from 'node:assert'; +import isEqual from 'lodash/isEqual'; import isObject from 'lodash/isObject.js'; import upperFirst from 'lodash/upperFirst'; import { cloneDataType } from '../dialects/abstract/data-types-utils.js'; @@ -12,7 +13,9 @@ import type { SaveOptions, AttributeNames, Attributes, + AttributeReferencesOptions, } from '../model'; +import { normalizeReference } from '../model-definition.js'; import { Op } from '../operators'; import { getColumnName } from '../utils/format.js'; import { isSameInitialModel } from '../utils/model-utils.js'; @@ -24,7 +27,6 @@ import { HasMany } from './has-many.js'; import { HasOne } from './has-one.js'; import type { NormalizeBaseAssociationOptions } from './helpers'; import { - addForeignKeyConstraints, defineAssociation, mixinMethods, normalizeBaseAssociationOptions, } from './helpers'; @@ -65,6 +67,7 @@ export class BelongsTo< /** * The column name of the foreign key */ + // TODO: rename to foreignKeyColumnName identifierField: string; /** @@ -77,6 +80,7 @@ export class BelongsTo< /** * The column name of the target key */ + // TODO: rename to targetKeyColumnName readonly targetKeyField: string; readonly targetKeyIsPrimary: boolean; @@ -100,7 +104,9 @@ export class BelongsTo< // TODO: throw is source model has a composite primary key. const targetKey = options?.targetKey || (target.primaryKeyAttribute as TargetKey); - if (!target.getAttributes()[targetKey]) { + const targetAttributes = target.modelDefinition.attributes; + + if (!targetAttributes.has(targetKey)) { throw new Error(`Unknown attribute "${options.targetKey}" passed as targetKey, define this attribute on model "${target.name}" first`); } @@ -115,10 +121,9 @@ export class BelongsTo< // For Db2 server, a reference column of a FOREIGN KEY must be unique // else, server throws SQL0573N error. Hence, setting it here explicitly // for non primary columns. - if (target.sequelize.options.dialect === 'db2' && this.target.getAttributes()[this.targetKey].primaryKey !== true) { + if (target.sequelize.options.dialect === 'db2' && targetAttributes.get(this.targetKey)!.primaryKey !== true) { // TODO: throw instead - // @ts-expect-error -- pending removal, not worth typing - this.target.getAttributes()[this.targetKey].unique = true; + this.target.modelDefinition.rawAttributes[this.targetKey].unique = true; } let foreignKey: string | undefined; @@ -139,16 +144,52 @@ export class BelongsTo< this.foreignKey = foreignKey as SourceKey; + this.targetKeyField = getColumnName(targetAttributes.get(this.targetKey)!); + this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute; + + const targetAttribute = targetAttributes.get(this.targetKey)!; + + const existingForeignKey = source.modelDefinition.rawAttributes[this.foreignKey]; const newForeignKeyAttribute = removeUndefined({ - type: cloneDataType(this.target.rawAttributes[this.targetKey].type), + type: cloneDataType(targetAttribute.type), ...foreignKeyAttributeOptions, - allowNull: this.source.rawAttributes[this.foreignKey]?.allowNull ?? foreignKeyAttributeOptions?.allowNull, + allowNull: existingForeignKey?.allowNull ?? foreignKeyAttributeOptions?.allowNull, }); - this.targetKeyField = getColumnName(this.target.getAttributes()[this.targetKey]); - this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute; + // FK constraints are opt-in: users must either set `foreignKeyConstraints` + // on the association, or request an `onDelete` or `onUpdate` behavior + if (options.foreignKeyConstraints !== false) { + const existingReference = existingForeignKey?.references + ? (normalizeReference(existingForeignKey.references) ?? existingForeignKey.references) as AttributeReferencesOptions + : undefined; + + const queryGenerator = this.source.sequelize.getQueryInterface().queryGenerator; + + const existingReferencedTable = existingReference?.table + ? queryGenerator.extractTableDetails(existingReference.table) + : undefined; - addForeignKeyConstraints(newForeignKeyAttribute, this.target, this.options, this.targetKeyField); + const newReferencedTable = queryGenerator.extractTableDetails(this.target); + + const newReference: AttributeReferencesOptions = {}; + if (existingReferencedTable) { + if (!isEqual(existingReferencedTable, newReferencedTable)) { + throw new Error(`Foreign key ${this.foreignKey} on ${this.source.name} already references ${queryGenerator.quoteTable(existingReferencedTable)}, but this association needs to make it reference ${queryGenerator.quoteTable(newReferencedTable)} instead.`); + } + } else { + newReference.table = newReferencedTable; + } + + if (existingReference?.key && existingReference.key !== this.targetKeyField) { + throw new Error(`Foreign key ${this.foreignKey} on ${this.source.name} already references column ${existingReference.key}, but this association needs to make it reference ${this.targetKeyField} instead.`); + } + + newReference.key = this.targetKeyField; + + newForeignKeyAttribute.references = newReference; + newForeignKeyAttribute.onDelete ??= newForeignKeyAttribute.allowNull !== false ? 'SET NULL' : 'CASCADE'; + newForeignKeyAttribute.onUpdate ??= newForeignKeyAttribute.onUpdate ?? 'CASCADE'; + } this.source.mergeAttributesDefault({ [this.foreignKey]: newForeignKeyAttribute, diff --git a/src/associations/has-many.ts b/src/associations/has-many.ts index 33b85ccb5c3b..7c32b0ec502a 100644 --- a/src/associations/has-many.ts +++ b/src/associations/has-many.ts @@ -313,6 +313,7 @@ export class HasMany< } return { + // @ts-expect-error -- TODO: what if the target has no primary key? [this.target.primaryKeyAttribute]: instance, }; }), @@ -321,6 +322,7 @@ export class HasMany< const findOptions: HasManyGetAssociationsMixinOptions = { ...options, scope: false, + // @ts-expect-error -- TODO: what if the target has no primary key? attributes: [this.target.primaryKeyAttribute], raw: true, // @ts-expect-error -- TODO: current WhereOptions typings do not allow having 'WhereOptions' inside another 'WhereOptions' @@ -379,7 +381,9 @@ export class HasMany< } as UpdateValues; const updateWhere = { + // @ts-expect-error -- TODO: what if the target has no primary key? [this.target.primaryKeyAttribute]: unassociatedObjects.map(unassociatedObject => { + // @ts-expect-error -- TODO: what if the target has no primary key? return unassociatedObject.get(this.target.primaryKeyAttribute); }), }; @@ -421,7 +425,9 @@ export class HasMany< } as UpdateValues; const where = { + // @ts-expect-error -- TODO: what if the target has no primary key? [this.target.primaryKeyAttribute]: targetInstances.map(unassociatedObject => { + // @ts-expect-error -- TODO: what if the target has no primary key? return unassociatedObject.get(this.target.primaryKeyAttribute); }), }; @@ -460,12 +466,15 @@ export class HasMany< const where = { [this.foreignKey]: sourceInstance.get(this.sourceKey), + // @ts-expect-error -- TODO: what if the target has no primary key? [this.target.primaryKeyAttribute]: targetInstances.map(targetInstance => { if (targetInstance instanceof this.target) { + // @ts-expect-error -- TODO: what if the target has no primary key? return (targetInstance as T).get(this.target.primaryKeyAttribute); } // raw entity + // @ts-expect-error -- TODO: what if the target has no primary key? if (isPlainObject(targetInstance) && this.target.primaryKeyAttribute in targetInstance) { // @ts-expect-error -- implicit any, can't be fixed return targetInstance[this.target.primaryKeyAttribute]; diff --git a/src/associations/has-one.ts b/src/associations/has-one.ts index feb2d9f9901a..61f16cd0719d 100644 --- a/src/associations/has-one.ts +++ b/src/associations/has-one.ts @@ -259,6 +259,7 @@ because, as this is a hasOne association, the foreign key we need to update is l const alreadyAssociated = !oldInstance || !associatedInstanceOrPk ? false : associatedInstanceOrPk instanceof Model ? associatedInstanceOrPk.equals(oldInstance) + // @ts-expect-error -- TODO: what if the target has no primary key? : oldInstance.get(this.target.primaryKeyAttribute) === associatedInstanceOrPk; if (alreadyAssociated) { @@ -286,6 +287,7 @@ because, as this is a hasOne association, the foreign key we need to update is l associatedInstance = associatedInstanceOrPk as T; } else { const tmpInstance = Object.create(null); + // @ts-expect-error -- TODO: what if the target has no primary key? tmpInstance[this.target.primaryKeyAttribute] = associatedInstanceOrPk; associatedInstance = this.target.build(tmpInstance, { isNewRecord: false, diff --git a/src/associations/helpers.ts b/src/associations/helpers.ts index 6203a8cf47cb..cfb6750b469b 100644 --- a/src/associations/helpers.ts +++ b/src/associations/helpers.ts @@ -6,7 +6,7 @@ import lowerFirst from 'lodash/lowerFirst'; import omit from 'lodash/omit'; import type { Class } from 'type-fest'; import { AssociationError } from '../errors/index.js'; -import type { Model, ModelAttributeColumnOptions, ModelStatic } from '../model'; +import type { Model, ModelStatic } from '../model'; import type { Sequelize } from '../sequelize'; import * as deprecations from '../utils/deprecations.js'; import { isModelStatic, isSameInitialModel } from '../utils/model-utils.js'; @@ -25,32 +25,6 @@ export function checkNamingCollision(source: ModelStatic, associationName: } } -export function addForeignKeyConstraints( - newAttribute: ModelAttributeColumnOptions, - source: ModelStatic, - options: AssociationOptions, - key: string, -): void { - // FK constraints are opt-in: users must either set `foreignKeyConstraints` - // on the association, or request an `onDelete` or `onUpdate` behavior - - if (options.foreignKeyConstraints !== false) { - // Find primary keys: composite keys not supported with this approach - const primaryKeys = Object.keys(source.primaryKeys) - .map(primaryKeyAttribute => source.getAttributes()[primaryKeyAttribute].field || primaryKeyAttribute); - - if (primaryKeys.length === 1 || !primaryKeys.includes(key)) { - newAttribute.references = { - model: source.getTableName(), - key: key || primaryKeys[0], - }; - - newAttribute.onDelete = newAttribute.onDelete ?? (newAttribute.allowNull !== false ? 'SET NULL' : 'CASCADE'); - newAttribute.onUpdate = newAttribute.onUpdate ?? 'CASCADE'; - } - } -} - /** * Mixin (inject) association methods to model prototype * diff --git a/src/decorators/legacy/attribute-utils.ts b/src/decorators/legacy/attribute-utils.ts index a8949f6e20a4..e74602d7e7a9 100644 --- a/src/decorators/legacy/attribute-utils.ts +++ b/src/decorators/legacy/attribute-utils.ts @@ -1,4 +1,4 @@ -import type { ModelAttributeColumnOptions, ModelStatic } from '../../model.js'; +import type { AttributeOptions, ModelStatic } from '../../model.js'; import { Model } from '../../model.js'; import { registerModelAttributeOptions } from '../shared/model.js'; import type { @@ -25,7 +25,7 @@ export function createRequiredAttributeOptionsDecorator( target: Object, propertyName: string | symbol, propertyDescriptor: PropertyDescriptor | undefined, - ) => Partial, + ) => Partial, ): RequiredParameterizedPropertyDecorator { return createOptionalAttributeOptionsDecorator(decoratorName, DECORATOR_NO_DEFAULT, callback); } @@ -45,7 +45,7 @@ export function createOptionalAttributeOptionsDecorator( target: Object, propertyName: string | symbol, propertyDescriptor: PropertyDescriptor | undefined, - ) => Partial, + ) => Partial, ): OptionalParameterizedPropertyDecorator { return createOptionallyParameterizedPropertyDecorator( decoratorName, @@ -63,7 +63,7 @@ function annotate( target: Object, propertyName: string | symbol, propertyDescriptor: PropertyDescriptor | undefined, - options: Partial, + options: Partial, ): void { if (typeof propertyName === 'symbol') { throw new TypeError('Symbol Model Attributes are not currently supported. We welcome a PR that implements this feature.'); diff --git a/src/decorators/legacy/attribute.ts b/src/decorators/legacy/attribute.ts index 46beef8f2e2c..33b8424b1fef 100644 --- a/src/decorators/legacy/attribute.ts +++ b/src/decorators/legacy/attribute.ts @@ -1,11 +1,11 @@ import { isDataType } from '../../dialects/abstract/data-types-utils.js'; import type { DataType } from '../../dialects/abstract/data-types.js'; -import type { ModelAttributeColumnOptions } from '../../model.js'; +import type { AttributeOptions } from '../../model.js'; import { columnToAttribute } from '../../utils/deprecations.js'; import { createOptionalAttributeOptionsDecorator, createRequiredAttributeOptionsDecorator } from './attribute-utils.js'; import type { PropertyOrGetterDescriptor } from './decorator-utils.js'; -type AttributeDecoratorOption = DataType | Partial; +type AttributeDecoratorOption = DataType | Partial; export const Attribute = createRequiredAttributeOptionsDecorator('Attribute', attrOptionOrDataType => { if (isDataType(attrOptionOrDataType)) { @@ -21,13 +21,13 @@ export const Attribute = createRequiredAttributeOptionsDecorator; +type UniqueOptions = NonNullable; /** * Configures the unique option of the attribute. diff --git a/src/decorators/legacy/index.mjs b/src/decorators/legacy/index.mjs index 1334bc8b96fb..3da996bb0d75 100644 --- a/src/decorators/legacy/index.mjs +++ b/src/decorators/legacy/index.mjs @@ -86,3 +86,5 @@ export const BeforeUpdate = Pkg.BeforeUpdate; export const BeforeUpsert = Pkg.BeforeUpsert; export const BeforeValidate = Pkg.BeforeValidate; export const ValidationFailed = Pkg.ValidationFailed; +export const BeforeDefinitionRefresh = Pkg.BeforeDefinitionRefresh; +export const AfterDefinitionRefresh = Pkg.AfterDefinitionRefresh; diff --git a/src/decorators/legacy/model-hooks.ts b/src/decorators/legacy/model-hooks.ts index 0fb73b0706a5..43213e2390c2 100644 --- a/src/decorators/legacy/model-hooks.ts +++ b/src/decorators/legacy/model-hooks.ts @@ -1,7 +1,8 @@ import upperFirst from 'lodash/upperFirst.js'; -import type { ModelHooks } from '../../model-typescript.js'; +import type { ModelHooks } from '../../model-hooks.js'; import { Model } from '../../model.js'; import { isModelStatic } from '../../utils/model-utils.js'; +import { registerModelOptions } from '../shared/model.js'; import { createOptionallyParameterizedPropertyDecorator, throwMustBeMethod, throwMustBeModel, @@ -45,7 +46,15 @@ function createHookDecorator(hookType: keyof ModelHooks) { ); } - targetModel.hooks.addListener(hookType, targetMethod.bind(targetModel), options?.name); + const callback = targetMethod.bind(targetModel); + + registerModelOptions(targetModel, { + hooks: { + [hookType]: options?.name + ? { name: options?.name, callback } + : callback, + }, + }); }, ); } @@ -96,3 +105,6 @@ export const AfterUpsert = createHookDecorator('afterUpsert'); export const BeforeValidate = createHookDecorator('beforeValidate'); export const AfterValidate = createHookDecorator('afterValidate'); export const ValidationFailed = createHookDecorator('validationFailed'); + +export const BeforeDefinitionRefresh = createHookDecorator('beforeDefinitionRefresh'); +export const AfterDefinitionRefresh = createHookDecorator('afterDefinitionRefresh'); diff --git a/src/decorators/legacy/table.ts b/src/decorators/legacy/table.ts index be718c14631b..c13b68997e75 100644 --- a/src/decorators/legacy/table.ts +++ b/src/decorators/legacy/table.ts @@ -12,7 +12,6 @@ export function Table(arg: any): undefined | ClassDecorator { const options: ModelOptions = { ...arg }; - // eslint-disable-next-line consistent-return -- decorators return (target: any) => annotate(target, options); } diff --git a/src/decorators/shared/model.ts b/src/decorators/shared/model.ts index 662b01c64c17..b9394e2c8188 100644 --- a/src/decorators/shared/model.ts +++ b/src/decorators/shared/model.ts @@ -1,10 +1,12 @@ -import type { ModelAttributeColumnOptions, ModelAttributes, ModelOptions, ModelStatic } from '../../model.js'; +import { mergeModelOptions } from '../../model-definition.js'; +import { initModel } from '../../model-typescript.js'; +import type { AttributeOptions, ModelAttributes, ModelOptions, ModelStatic } from '../../model.js'; import type { Sequelize } from '../../sequelize.js'; import { getAllOwnEntries } from '../../utils/object.js'; interface RegisteredOptions { model: ModelOptions; - attributes: { [key: string]: Partial }; + attributes: { [key: string]: Partial }; } const registeredOptions = new WeakMap(); @@ -27,43 +29,14 @@ export function registerModelOptions( return; } - // merge-able: scopes, indexes, setterMethods, getterMethods + // merge-able: scopes, indexes const existingModelOptions = registeredOptions.get(model)!.model; - for (const [optionName, optionValue] of Object.entries(options)) { - if (!(optionName in existingModelOptions)) { - // @ts-expect-error -- runtime type checking is enforced by model - existingModelOptions[optionName] = optionValue; - continue; - } - - // These are objects. We merge their properties, unless the same key is used in both values. - if (optionName === 'scopes' || optionName === 'setterMethods' || optionName === 'getterMethods' || optionName === 'validate') { - for (const [subOptionName, subOptionValue] of getAllOwnEntries(optionValue)) { - if (subOptionName in existingModelOptions[optionName]!) { - throw new Error(`Multiple decorators are attempting to register option ${optionName}[${JSON.stringify(subOptionName)}] on model ${model.name}.`); - } - - // @ts-expect-error -- runtime type checking is enforced by model - existingModelOptions[optionName][subOptionName] = subOptionValue; - } - - continue; - } - - // This is an array. Simple array merge. - if (optionName === 'indexes') { - existingModelOptions.indexes = [...existingModelOptions.indexes!, ...optionValue]; - - continue; - } - - // @ts-expect-error -- dynamic type, not worth typing - if (optionValue === existingModelOptions[optionName]) { - continue; - } - - throw new Error(`Multiple decorators are attempting to set different values for the option ${optionName} on model ${model.name}.`); + try { + mergeModelOptions(existingModelOptions, options, false); + } catch (error) { + // TODO [TS 4.8]: remove this "as Error" cast once support for TS < 4.8 is dropped, as the typing of "cause" has been fixed in TS 4.8 + throw new Error(`Multiple decorators are trying to register conflicting options on model ${model.name}`, { cause: error as Error }); } } @@ -78,7 +51,7 @@ export function registerModelOptions( export function registerModelAttributeOptions( model: ModelStatic, attributeName: string, - options: Partial, + options: Partial, ): void { if (!registeredOptions.has(model)) { registeredOptions.set(model, { @@ -151,9 +124,7 @@ export function registerModelAttributeOptions( export function initDecoratedModel(model: ModelStatic, sequelize: Sequelize): void { const { model: modelOptions, attributes: attributeOptions } = registeredOptions.get(model) ?? {}; - // model.init will ensure all required attributeOptions have been specified. - // @ts-expect-error -- secret method - model._internalInit(attributeOptions as ModelAttributes, { + initModel(model, attributeOptions as ModelAttributes, { ...modelOptions, sequelize, }); diff --git a/src/dialects/abstract/data-types.ts b/src/dialects/abstract/data-types.ts index e5165cc2a397..75f126c90e8a 100644 --- a/src/dialects/abstract/data-types.ts +++ b/src/dialects/abstract/data-types.ts @@ -9,7 +9,7 @@ import { ValidationErrorItem } from '../../errors'; import type { Falsy } from '../../generic/falsy'; import type { GeoJson, GeoJsonType } from '../../geo-json.js'; import { assertIsGeoJson } from '../../geo-json.js'; -import type { BuiltModelAttributeColumnOptions, ModelStatic, Rangable, RangePart } from '../../model.js'; +import type { NormalizedAttributeOptions, ModelStatic, Rangable, RangePart } from '../../model.js'; import type { Sequelize } from '../../sequelize.js'; import { makeBufferFromTypedArray } from '../../utils/buffer.js'; import { isPlainObject, isString } from '../../utils/check.js'; @@ -70,7 +70,7 @@ export interface StringifyOptions { dialect: AbstractDialect; operation?: string; timezone?: string | undefined; - field?: BuiltModelAttributeColumnOptions; + field?: NormalizedAttributeOptions; } export interface BindParamOptions extends StringifyOptions { @@ -275,6 +275,10 @@ export abstract class AbstractDataType< assertDataTypeSupported(dialect, this); } + belongsToDialect(dialect: AbstractDialect): boolean { + return this.#dialect === dialect; + } + /** * Returns this DataType, using its dialect-specific subclass. * diff --git a/src/dialects/abstract/query-generator-typescript.ts b/src/dialects/abstract/query-generator-typescript.ts index 7e67006c8f9c..66d021fa91b8 100644 --- a/src/dialects/abstract/query-generator-typescript.ts +++ b/src/dialects/abstract/query-generator-typescript.ts @@ -65,6 +65,7 @@ export class AbstractQueryGeneratorTypeScript { throw new Error(`removeIndexQuery has not been implemented in ${this.dialect.name}.`); } + // TODO: rename to "normalizeTable" & move to sequelize class extractTableDetails( tableNameOrModel: TableNameOrModel, options?: { schema?: string, delimiter?: string }, @@ -77,6 +78,9 @@ export class AbstractQueryGeneratorTypeScript { throw new Error(`Invalid input received, got ${NodeUtil.inspect(tableNameOrModel)}, expected a Model Class, a TableNameWithSchema object, or a table name string`); } + // @ts-expect-error -- TODO: this is added by getTableName on model, and must be removed + delete tableNameObject.toString; + return { ...tableNameObject, schema: options?.schema || tableNameObject.schema || this.options.schema || this.dialect.getDefaultSchema(), diff --git a/src/dialects/abstract/query-generator.d.ts b/src/dialects/abstract/query-generator.d.ts index 03d6e328d06a..aa6b1fc8924b 100644 --- a/src/dialects/abstract/query-generator.d.ts +++ b/src/dialects/abstract/query-generator.d.ts @@ -1,10 +1,10 @@ // TODO: complete me - this file is a stub that will be completed when query-generator.ts is migrated to TS import type { - BuiltModelAttributeColumnOptions, + NormalizedAttributeOptions, FindOptions, Model, - ModelAttributeColumnOptions, + AttributeOptions, ModelStatic, SearchPathable, WhereOptions, @@ -67,7 +67,7 @@ export type WhereItemsQueryOptions = ParameterOptions & { model?: ModelStatic, type?: QueryTypes, prefix?: string | Literal, - field?: ModelAttributeColumnOptions, + field?: AttributeOptions, }; type HandleSequelizeMethodOptions = ParameterOptions & { @@ -118,8 +118,8 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { generateTransactionId(): string; whereQuery(where: object, options?: ParameterOptions): string; whereItemsQuery(where: WhereOptions, options: WhereItemsQueryOptions, binding?: string): string; - validate(value: unknown, field?: BuiltModelAttributeColumnOptions): void; - escape(value: unknown, field?: BuiltModelAttributeColumnOptions, options?: EscapeOptions): string; + validate(value: unknown, field?: NormalizedAttributeOptions): void; + escape(value: unknown, field?: NormalizedAttributeOptions, options?: EscapeOptions): string; quoteIdentifiers(identifiers: string): string; handleSequelizeMethod( smth: SequelizeMethod, @@ -145,20 +145,20 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { insertQuery( table: TableName, valueHash: object, - columnDefinitions?: { [columnName: string]: BuiltModelAttributeColumnOptions }, + columnDefinitions?: { [columnName: string]: NormalizedAttributeOptions }, options?: InsertOptions ): { query: string, bind?: unknown[] }; bulkInsertQuery( tableName: TableName, newEntries: object[], options?: BulkInsertOptions, - columnDefinitions?: { [columnName: string]: BuiltModelAttributeColumnOptions } + columnDefinitions?: { [columnName: string]: NormalizedAttributeOptions } ): string; addColumnQuery( table: TableName, columnName: string, - columnDefinition: ModelAttributeColumnOptions | DataType, + columnDefinition: AttributeOptions | DataType, options?: AddColumnQueryOptions, ): string; @@ -173,7 +173,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { attrValueHash: object, where: WhereOptions, options?: UpdateOptions, - columnDefinitions?: { [columnName: string]: BuiltModelAttributeColumnOptions }, + columnDefinitions?: { [columnName: string]: NormalizedAttributeOptions }, ): { query: string, bind?: unknown[] }; deleteQuery( diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index dc5007a58124..c96cbaeca6ff 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -1,6 +1,7 @@ 'use strict'; import NodeUtil from 'node:util'; +import { conformIndex } from '../../model-internals'; import { getTextDataTypeForDialect } from '../../sql-string'; import { rejectInvalidOptions, isNullish, canTreatArrayAsAnd, isColString } from '../../utils/check'; import { TICK_CHAR } from '../../utils/dialect'; @@ -24,7 +25,6 @@ const util = require('node:util'); const _ = require('lodash'); const crypto = require('node:crypto'); -const deprecations = require('../../utils/deprecations'); const SqlString = require('../../sql-string'); const DataTypes = require('../../data-types'); const { Model } = require('../../model'); @@ -639,7 +639,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { options = nameIndex(options, options.prefix); } - options = Model._conformIndex(options); + options = conformIndex(options); if (!this.dialect.supports.index.type) { delete options.type; @@ -915,23 +915,25 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { // see if this is an order if (index > 0 && orderIndex !== -1) { item = this.sequelize.literal(` ${validOrderOptions[orderIndex]}`); - } else if (previousModel && isModelStatic(previousModel)) { - // only go down this path if we have preivous model and check only once - if (previousModel.associations !== undefined && previousModel.associations[item]) { + } else if (isModelStatic(previousModel)) { + const { modelDefinition: previousModelDefinition } = previousModel; + + // only go down this path if we have previous model and check only once + if (previousModel.associations?.[item]) { // convert the item to an association item = previousModel.associations[item]; - } else if (previousModel.rawAttributes !== undefined && previousModel.rawAttributes[item] && item !== previousModel.rawAttributes[item].field) { + } else if (previousModelDefinition.attributes.has(item)) { // convert the item attribute from its alias - item = previousModel.rawAttributes[item].field; + item = previousModelDefinition.attributes.get(item).columnName; } else if ( item.includes('.') - && previousModel.rawAttributes !== undefined ) { const itemSplit = item.split('.'); - if (previousModel.rawAttributes[itemSplit[0]].type instanceof DataTypes.JSON) { + const jsonAttribute = previousModelDefinition.attributes.get(itemSplit[0]); + if (jsonAttribute.type instanceof DataTypes.JSON) { // just quote identifiers for now - const identifier = this.quoteIdentifiers(`${previousModel.name}.${previousModel.rawAttributes[itemSplit[0]].field}`); + const identifier = this.quoteIdentifiers(`${previousModel.name}.${jsonAttribute.columnName}`); // get path const path = itemSplit.slice(1); @@ -1038,34 +1040,39 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { * Escape a value (e.g. a string, number or date) * * @param {unknown} value - * @param {object} field + * @param {object} attribute * @param {object} options * @private */ - escape(value, field, options = {}) { + escape(value, attribute, options = {}) { if (value instanceof SequelizeMethod) { return this.handleSequelizeMethod(value, undefined, undefined, { replacements: options.replacements }); } - if (value == null || field?.type == null || typeof field.type === 'string') { + if (value == null || attribute?.type == null || typeof attribute.type === 'string') { // use default escape mechanism instead of the DataType's. return SqlString.escape(value, this.options.timezone, this.dialect); } - field.type = field.type.toDialectDataType(this.dialect); + if (!attribute.type.belongsToDialect(this.dialect)) { + attribute = { + ...attribute, + type: attribute.type.toDialectDataType(this.dialect), + }; + } if (options.isList && Array.isArray(value)) { const escapeOptions = { ...options, isList: false }; return `(${value.map(valueItem => { - return this.escape(valueItem, field, escapeOptions); + return this.escape(valueItem, attribute, escapeOptions); }).join(', ')})`; } - this.validate(value, field, options); + this.validate(value, attribute, options); - return field.type.escape(value, { - field, + return attribute.type.escape(value, { + field: attribute, timezone: this.options.timezone, operation: options.operation, dialect: this.dialect, @@ -1208,11 +1215,15 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { return Array.isArray(t) ? this.quoteTable(t[0], t[1]) : this.quoteTable(t, true); }).join(', '); + const mainModelDefinition = mainTable.model?.modelDefinition; + const mainModelAttributes = mainModelDefinition?.attributes; + if (subQuery && attributes.main) { - for (const keyAtt of mainTable.model.primaryKeyAttributes) { + for (const pkAttrName of mainModelDefinition.primaryKeysAttributeNames) { // Check if mainAttributes contain the primary key of the model either as a field or an aliased field - if (!attributes.main.some(attr => keyAtt === attr || keyAtt === attr[0] || keyAtt === attr[1])) { - attributes.main.push(mainTable.model.rawAttributes[keyAtt].field ? [keyAtt, mainTable.model.rawAttributes[keyAtt].field] : keyAtt); + if (!attributes.main.some(attr => pkAttrName === attr || pkAttrName === attr[0] || pkAttrName === attr[1])) { + const attribute = mainModelAttributes.get(pkAttrName); + attributes.main.push(attribute.columnName !== pkAttrName ? [pkAttrName, attribute.columnName] : pkAttrName); } } } @@ -1736,18 +1747,21 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { let joinWhere; /* Attributes for the left side */ const left = association.source; - const attrLeft = association instanceof BelongsTo - ? association.identifier - : association.sourceKeyAttribute || left.primaryKeyAttribute; - const fieldLeft = association instanceof BelongsTo + const leftAttributes = left.modelDefinition.attributes; + + const attrNameLeft = association instanceof BelongsTo + ? association.foreignKey + : association.sourceKeyAttribute; + const columnNameLeft = association instanceof BelongsTo ? association.identifierField - : left.rawAttributes[association.sourceKeyAttribute || left.primaryKeyAttribute].field; + : leftAttributes.get(association.sourceKeyAttribute).columnName; let asLeft; /* Attributes for the right side */ const right = include.model; + const rightAttributes = right.modelDefinition.attributes; const tableRight = right.getTableName(); const fieldRight = association instanceof BelongsTo - ? right.rawAttributes[association.targetIdentifier || right.primaryKeyAttribute].field + ? rightAttributes.get(association.targetKey).columnName : association.identifierField; let asRight = include.as; @@ -1765,7 +1779,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { asRight = `${asLeft}->${asRight}`; } - let joinOn = `${this.quoteTable(asLeft)}.${this.quoteIdentifier(fieldLeft)}`; + let joinOn = `${this.quoteTable(asLeft)}.${this.quoteIdentifier(columnNameLeft)}`; const subqueryAttributes = []; if (topLevelInfo.options.groupedLimit && parentIsTop || topLevelInfo.subQuery && include.parent.subQuery && !include.subQuery) { @@ -1775,14 +1789,14 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { const quotedTableName = this.quoteTable(tableName); // Check for potential aliased JOIN condition - joinOn = this._getAliasForField(tableName, attrLeft, topLevelInfo.options) || `${quotedTableName}.${this.quoteIdentifier(attrLeft)}`; + joinOn = this._getAliasForField(tableName, attrNameLeft, topLevelInfo.options) || `${quotedTableName}.${this.quoteIdentifier(attrNameLeft)}`; if (topLevelInfo.subQuery) { - const dbIdentifier = `${quotedTableName}.${this.quoteIdentifier(fieldLeft)}`; - subqueryAttributes.push(dbIdentifier !== joinOn ? `${dbIdentifier} AS ${this.quoteIdentifier(attrLeft)}` : dbIdentifier); + const dbIdentifier = `${quotedTableName}.${this.quoteIdentifier(columnNameLeft)}`; + subqueryAttributes.push(dbIdentifier !== joinOn ? `${dbIdentifier} AS ${this.quoteIdentifier(attrNameLeft)}` : dbIdentifier); } } else { - const joinSource = `${asLeft.replace(/->/g, '.')}.${attrLeft}`; + const joinSource = `${asLeft.replace(/->/g, '.')}.${attrNameLeft}`; // Check for potential aliased JOIN condition joinOn = this._getAliasForField(asLeft, joinSource, topLevelInfo.options) || this.quoteIdentifier(joinSource); @@ -1904,6 +1918,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { const throughTable = through.model.getTableName(); const throughAs = `${includeAs.internalAs}->${through.as}`; const externalThroughAs = `${includeAs.externalAs}.${through.as}`; + const throughAttributes = through.attributes.map(attr => { let alias = `${externalThroughAs}.${Array.isArray(attr) ? attr[1] : attr}`; @@ -2154,8 +2169,8 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { && !(typeof order[0] === 'string' && model && model.associations !== undefined && model.associations[order[0]]) ) { // TODO - refactor this.quote() to not change the first argument - const field = model.rawAttributes[order[0]]?.field || order[0]; - const subQueryAlias = this._getAliasForField(model.name, field, options); + const columnName = model.modelDefinition.getColumnNameLoose(order[0]); + const subQueryAlias = this._getAliasForField(model.name, columnName, options); let parent = null; let orderToQuote = []; @@ -2425,12 +2440,13 @@ Only named replacements (:name) are allowed in literal() because we cannot guara if (typeof key === 'string' && key.includes('.') && options.model) { const keyParts = key.split('.'); - if (options.model.rawAttributes[keyParts[0]] && options.model.rawAttributes[keyParts[0]].type instanceof DataTypes.JSON) { + const { attributes } = options.model.modelDefinition; + const attribute = attributes.get(keyParts[0]); + if (attribute?.type instanceof DataTypes.JSON) { const tmp = {}; - const field = options.model.rawAttributes[keyParts[0]]; _.set(tmp, keyParts.slice(1), value); - return this.whereItemQuery(field.field || keyParts[0], tmp, { field, ...options }); + return this.whereItemQuery(attribute.columnName, tmp, { field: attribute, ...options }); } } @@ -2536,12 +2552,15 @@ Only named replacements (:name) are allowed in literal() because we cannot guara return options.field; } - if (options.model && options.model.rawAttributes && options.model.rawAttributes[key]) { - return options.model.rawAttributes[key]; + const modelDefinition = options.model?.modelDefinition; + const attribute = modelDefinition?.attributes.get(key); + if (attribute) { + return attribute; } - if (options.model && options.model.fieldRawAttributesMap && options.model.fieldRawAttributesMap[key]) { - return options.model.fieldRawAttributesMap[key]; + const column = modelDefinition?.columns.get(key); + if (column) { + return column; } } diff --git a/src/dialects/abstract/query-interface.d.ts b/src/dialects/abstract/query-interface.d.ts index 6e4f8aa1ef05..d9f10e6de0c7 100644 --- a/src/dialects/abstract/query-interface.d.ts +++ b/src/dialects/abstract/query-interface.d.ts @@ -3,18 +3,19 @@ import type { Deferrable } from '../../deferrable'; import type { Logging, Model, - ModelAttributeColumnOptions, + AttributeOptions, ModelAttributes, WhereOptions, Filterable, ModelStatic, CreationAttributes, Attributes, - BuiltModelAttributeColumnOptions, + NormalizedAttributeOptions, } from '../../model'; import type { Sequelize, QueryRawOptions, QueryRawOptionsWithModel } from '../../sequelize'; import type { Transaction } from '../../transaction'; import type { Fn, Literal, Col } from '../../utils/sequelize-method.js'; +import type { AllowLowercase } from '../../utils/types.js'; import type { DataType } from './data-types.js'; import type { RemoveIndexQueryOptions, TableNameOrModel } from './query-generator-typescript'; import type { AbstractQueryGenerator, AddColumnQueryOptions, RemoveColumnQueryOptions } from './query-generator.js'; @@ -67,12 +68,7 @@ export interface QueryInterfaceCreateTableOptions extends QueryRawOptions, Colla /** * Used for compound unique keys. */ - uniqueKeys?: { - [keyName: string]: { - fields: string[], - customIndex?: boolean, - }, - }; + uniqueKeys?: { [indexName: string]: { fields: string[] } }; } export interface QueryInterfaceDropTableOptions extends QueryRawOptions { @@ -92,7 +88,7 @@ export interface TableNameWithSchema { export type TableName = string | TableNameWithSchema; -export type IndexType = 'UNIQUE' | 'FULLTEXT' | 'SPATIAL'; +export type IndexType = AllowLowercase<'UNIQUE' | 'FULLTEXT' | 'SPATIAL'>; export type IndexMethod = 'BTREE' | 'HASH' | 'GIST' | 'SPGIST' | 'GIN' | 'BRIN' | string; export interface IndexField { @@ -143,6 +139,11 @@ export interface IndexOptions { */ unique?: boolean; + /** + * The message to display if the unique constraint is violated. + */ + msg?: string; + /** * PostgreSQL will build the index without taking any write locks. Postgres only. * @@ -389,7 +390,7 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { addColumn( table: TableName, key: string, - attribute: ModelAttributeColumnOptions | DataType, + attribute: AttributeOptions | DataType, options?: AddColumnOptions ): Promise; @@ -408,7 +409,7 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { changeColumn( tableName: TableName, attributeName: string, - dataTypeOrOptions?: DataType | ModelAttributeColumnOptions, + dataTypeOrOptions?: DataType | AttributeOptions, options?: QiOptionsWithReplacements ): Promise; @@ -507,7 +508,7 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { tableName: TableName, records: object[], options?: QiOptionsWithReplacements, - attributes?: Record + attributes?: Record ): Promise; /** @@ -529,7 +530,7 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { values: object, where: WhereOptions, options?: QiOptionsWithReplacements, - columnDefinitions?: { [columnName: string]: BuiltModelAttributeColumnOptions }, + columnDefinitions?: { [columnName: string]: NormalizedAttributeOptions }, ): Promise; /** diff --git a/src/dialects/abstract/query-interface.js b/src/dialects/abstract/query-interface.js index d41f7fa003a3..6efd39b743a8 100644 --- a/src/dialects/abstract/query-interface.js +++ b/src/dialects/abstract/query-interface.js @@ -1,6 +1,7 @@ 'use strict'; -import { cloneDeep } from '../../utils/object'; +import { map } from '../../utils/iterators'; +import { cloneDeep, getObjectFromMap } from '../../utils/object'; import { noSchemaParameter, noSchemaDelimiterParameter } from '../../utils/deprecations'; import { assertNoReservedBind, combineBinds } from '../../utils/sql'; import { AbstractDataType } from './data-types'; @@ -192,18 +193,8 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { */ // TODO: remove "schema" option from the option bag, it must be passed as part of "tableName" instead async createTable(tableName, attributes, options, model) { - let sql = ''; - options = { ...options }; - if (options && options.uniqueKeys) { - _.forOwn(options.uniqueKeys, uniqueKey => { - if (uniqueKey.customIndex === undefined) { - uniqueKey.customIndex = true; - } - }); - } - if (model) { options.uniqueKeys = options.uniqueKeys || model.uniqueKeys; } @@ -216,12 +207,14 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { // Postgres requires special SQL commands for ENUM/ENUM[] await this.ensureEnums(tableName, attributes, options, model); + const modelTable = model?.table; + if ( !tableName.schema - && (options.schema || Boolean(model) && model._schema) + && (options.schema || modelTable?.schema) ) { tableName = this.queryGenerator.extractTableDetails(tableName); - tableName.schema = model?._schema || options.schema; + tableName.schema = modelTable?.schema || options.schema; } attributes = this.queryGenerator.attributesToSQL(attributes, { @@ -231,7 +224,8 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { // schema override for multi-tenancy schema: options.schema, }); - sql = this.queryGenerator.createTableQuery(tableName, attributes, options); + + const sql = this.queryGenerator.createTableQuery(tableName, attributes, options); return await this.sequelize.queryRaw(sql, options); } @@ -841,8 +835,15 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { } options = cloneDeep(options); - options.hasTrigger = instance && instance.constructor.options.hasTrigger; - const { query, bind } = this.queryGenerator.insertQuery(tableName, values, instance && instance.constructor.rawAttributes, options); + const modelDefinition = instance?.constructor.modelDefinition; + + options.hasTrigger = modelDefinition?.options.hasTrigger; + const { query, bind } = this.queryGenerator.insertQuery( + tableName, + values, + modelDefinition && getObjectFromMap(modelDefinition.attributes), + options, + ); options.type = QueryTypes.INSERT; options.instance = instance; @@ -882,25 +883,22 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { options = { ...options }; const model = options.model; + const modelDefinition = model.modelDefinition; options.type = QueryTypes.UPSERT; options.updateOnDuplicate = Object.keys(updateValues); options.upsertKeys = options.conflictFields || []; if (options.upsertKeys.length === 0) { - const primaryKeys = Object.values(model.primaryKeys).map(item => item.field); - const uniqueKeys = Object.values(model.uniqueKeys).filter(c => c.fields.length > 0).map(c => c.fields); - const indexKeys = Object.values(model.getIndexes()).filter(c => c.unique && c.fields.length > 0).map(c => c.fields); + const primaryKeys = Array.from( + map(modelDefinition.primaryKeysAttributeNames, pkAttrName => modelDefinition.attributes.get(pkAttrName).columnName), + ); + + const uniqueColumnNames = Object.values(model.getIndexes()).filter(c => c.unique && c.fields.length > 0).map(c => c.fields); // For fields in updateValues, try to find a constraint or unique index // that includes given field. Only first matching upsert key is used. for (const field of options.updateOnDuplicate) { - const uniqueKey = uniqueKeys.find(fields => fields.includes(field)); - if (uniqueKey) { - options.upsertKeys = uniqueKey; - break; - } - - const indexKey = indexKeys.find(fields => fields.includes(field)); + const indexKey = uniqueColumnNames.find(fields => fields.includes(field)); if (indexKey) { options.upsertKeys = indexKey; break; @@ -918,7 +916,12 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { options.upsertKeys = _.uniq(options.upsertKeys); } - const { bind, query } = this.queryGenerator.insertQuery(tableName, insertValues, model.rawAttributes, options); + const { bind, query } = this.queryGenerator.insertQuery( + tableName, + insertValues, + getObjectFromMap(modelDefinition.attributes), + options, + ); // unlike bind, replacements are handled by QueryGenerator, not QueryRaw delete options.replacement; @@ -966,10 +969,18 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript { assertNoReservedBind(options.bind); } + const modelDefinition = instance?.constructor.modelDefinition; + options = { ...options }; - options.hasTrigger = instance && instance.constructor.options.hasTrigger; + options.hasTrigger = modelDefinition?.options.hasTrigger; - const { query, bind } = this.queryGenerator.updateQuery(tableName, values, where, options, instance.constructor.rawAttributes); + const { query, bind } = this.queryGenerator.updateQuery( + tableName, + values, + where, + options, + modelDefinition && getObjectFromMap(modelDefinition.attributes), + ); options.type = QueryTypes.UPDATE; options.instance = instance; diff --git a/src/dialects/abstract/query.js b/src/dialects/abstract/query.js index 24e1d480be9f..ae898e5bd645 100644 --- a/src/dialects/abstract/query.js +++ b/src/dialects/abstract/query.js @@ -110,13 +110,23 @@ export class AbstractQuery { } getUniqueConstraintErrorMessage(field) { - let message = field ? `${field} must be unique` : 'Must be unique'; + if (!field) { + return 'Must be unique'; + } - if (field && this.model) { - for (const key of Object.keys(this.model.uniqueKeys)) { - if (this.model.uniqueKeys[key].fields.includes(field.replace(/"/g, '')) && this.model.uniqueKeys[key].msg) { - message = this.model.uniqueKeys[key].msg; - } + const message = `${field} must be unique`; + + if (!this.model) { + return message; + } + + for (const index of this.model.getIndexes()) { + if (!index.unique) { + continue; + } + + if (index.fields.includes(field.replace(/"/g, '')) && index.msg) { + return index.msg; } } @@ -155,16 +165,16 @@ export class AbstractQuery { } handleInsertQuery(results, metaData) { - if (this.instance) { - // add the inserted row id to the instance - const autoIncrementAttribute = this.model.autoIncrementAttribute; - let id = null; + if (!this.instance) { + return; + } - id = id || results && results[this.getInsertIdField()]; - id = id || metaData && metaData[this.getInsertIdField()]; + const autoIncrementAttribute = this.model.modelDefinition.autoIncrementAttributeName; + const id = results?.[this.getInsertIdField()] + ?? metaData?.[this.getInsertIdField()] + ?? null; - this.instance[autoIncrementAttribute] = id; - } + this.instance[autoIncrementAttribute] = id; } isShowTablesQuery() { @@ -309,7 +319,7 @@ export class AbstractQuery { continue; } - const attribute = model?.rawAttributes[key]; + const attribute = model?.modelDefinition.attributes.get(key); values[key] = this._parseDatabaseValue(values[key], attribute?.type); } diff --git a/src/dialects/db2/query-generator.js b/src/dialects/db2/query-generator.js index b066e8e463ac..3f6ada0d1ae6 100644 --- a/src/dialects/db2/query-generator.js +++ b/src/dialects/db2/query-generator.js @@ -158,13 +158,11 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { if (options && options.uniqueKeys) { _.each(options.uniqueKeys, (columns, indexName) => { - if (columns.customIndex) { - if (!_.isString(indexName)) { - indexName = `uniq_${tableName}_${columns.fields.join('_')}`; - } - - values.attributes += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; + if (!_.isString(indexName)) { + indexName = `uniq_${tableName}_${columns.fields.join('_')}`; } + + values.attributes += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; }); } @@ -447,23 +445,20 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { upsertQuery(tableName, insertValues, updateValues, where, model, options) { const targetTableAlias = this.quoteTable(`${tableName}_target`); const sourceTableAlias = this.quoteTable(`${tableName}_source`); - const primaryKeysAttrs = []; - const identityAttrs = []; + const primaryKeysColumns = []; + const identityColumns = []; const uniqueAttrs = []; const tableNameQuoted = this.quoteTable(tableName); // Obtain primaryKeys, uniquekeys and identity attrs from rawAttributes as model is not passed - for (const key in model.rawAttributes) { - if (model.rawAttributes[key].primaryKey) { - primaryKeysAttrs.push(model.rawAttributes[key].field || key); - } - - if (model.rawAttributes[key].unique) { - uniqueAttrs.push(model.rawAttributes[key].field || key); + const attributes = model.modelDefinition.attributes; + for (const attribute of attributes.values()) { + if (attribute.primaryKey) { + primaryKeysColumns.push(attribute.columnName); } - if (model.rawAttributes[key].autoIncrement) { - identityAttrs.push(model.rawAttributes[key].field || key); + if (attribute.autoIncrement) { + identityColumns.push(attribute.columnName); } } @@ -472,7 +467,8 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { if (index.unique && index.fields) { for (const field of index.fields) { const fieldName = typeof field === 'string' ? field : field.name || field.attribute; - if (!uniqueAttrs.includes(fieldName) && model.rawAttributes[fieldName]) { + // TODO: "index.fields" are column names, not an attribute name. This is a bug. + if (!uniqueAttrs.includes(fieldName) && attributes.has(fieldName)) { uniqueAttrs.push(fieldName); } } @@ -520,8 +516,8 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { // Search for primary key attribute in clauses -- Model can have two separate unique keys for (const key in clauses) { const keys = Object.keys(clauses[key]); - if (primaryKeysAttrs.includes(keys[0])) { - joinCondition = getJoinSnippet(primaryKeysAttrs).join(' AND '); + if (primaryKeysColumns.includes(keys[0])) { + joinCondition = getJoinSnippet(primaryKeysColumns).join(' AND '); break; } } @@ -533,7 +529,7 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { // Remove the IDENTITY_INSERT Column from update const filteredUpdateClauses = updateKeys.filter(key => { - if (!identityAttrs.includes(key)) { + if (!identityColumns.includes(key)) { return true; } @@ -655,7 +651,7 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { template += `, CONSTRAINT ${fkName} FOREIGN KEY (${attrName})`; } - template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`; + template += ` REFERENCES ${this.quoteTable(attribute.references.table)}`; if (attribute.references.key) { template += ` (${this.quoteIdentifier(attribute.references.key)})`; @@ -701,7 +697,7 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { if (attribute.references) { - if (existingConstraints.includes(attribute.references.model.toString())) { + if (existingConstraints.includes(this.quoteTable(attribute.references.table))) { // no cascading constraints to a table more than once attribute.onDelete = ''; attribute.onUpdate = ''; @@ -709,7 +705,7 @@ export class Db2QueryGenerator extends Db2QueryGeneratorTypeScript { attribute.onDelete = ''; attribute.onUpdate = ''; } else { - existingConstraints.push(attribute.references.model.toString()); + existingConstraints.push(this.quoteTable(attribute.references.table)); } } diff --git a/src/dialects/db2/query-interface.js b/src/dialects/db2/query-interface.js index a98d499b77f7..89322948e7e0 100644 --- a/src/dialects/db2/query-interface.js +++ b/src/dialects/db2/query-interface.js @@ -42,9 +42,7 @@ export class Db2QueryInterface extends AbstractQueryInterface { } // Lets combine unique keys and indexes into one - const indexes = _.map(model.uniqueKeys, value => { - return value.fields; - }); + const indexes = []; for (const value of model.getIndexes()) { if (value.unique) { @@ -146,14 +144,6 @@ export class Db2QueryInterface extends AbstractQueryInterface { options = { ...options }; - if (options && options.uniqueKeys) { - _.forOwn(options.uniqueKeys, uniqueKey => { - if (uniqueKey.customIndex === undefined) { - uniqueKey.customIndex = true; - } - }); - } - if (model) { options.uniqueKeys = options.uniqueKeys || model.uniqueKeys; } @@ -163,12 +153,14 @@ export class Db2QueryInterface extends AbstractQueryInterface { attribute => this.sequelize.normalizeAttribute(attribute), ); + const modelTable = model?.table; + if ( !tableName.schema - && (options.schema || Boolean(model) && model._schema) + && (options.schema || modelTable?.schema) ) { tableName = this.queryGenerator.extractTableDetails(tableName); - tableName.schema = Boolean(model) && model._schema || options.schema || tableName.schema; + tableName.schema = modelTable?.schema || options.schema || tableName.schema; } attributes = this.queryGenerator.attributesToSQL(attributes, { table: tableName, context: 'createTable', withoutForeignKeyConstraints: options.withoutForeignKeyConstraints }); diff --git a/src/dialects/db2/query.js b/src/dialects/db2/query.js index c34c7762d90e..657f9960ec38 100644 --- a/src/dialects/db2/query.js +++ b/src/dialects/db2/query.js @@ -324,7 +324,7 @@ export class Db2Query extends AbstractQuery { } if (this.model && Boolean(uniqueIndexName)) { - uniqueKey = this.model.uniqueKeys[uniqueIndexName]; + uniqueKey = this.model.getIndexes().find(index => index.unique && index.name === uniqueIndexName); } if (!uniqueKey && this.options.fields) { @@ -332,6 +332,7 @@ export class Db2Query extends AbstractQuery { } if (uniqueKey) { + // TODO: DB2 uses a custom "column" property, but it should use "fields" instead, so column can be removed if (this.options.where && this.options.where[uniqueKey.column] !== undefined) { fields[uniqueKey.column] = this.options.where[uniqueKey.column]; @@ -457,22 +458,21 @@ export class Db2Query extends AbstractQuery { } handleInsertQuery(results, metaData) { - if (this.instance) { - // add the inserted row id to the instance - const autoIncrementAttribute = this.model.autoIncrementAttribute; - let id = null; - let autoIncrementAttributeAlias = null; - - if (Object.prototype.hasOwnProperty.call(this.model.rawAttributes, autoIncrementAttribute) - && this.model.rawAttributes[autoIncrementAttribute].field !== undefined) { - autoIncrementAttributeAlias = this.model.rawAttributes[autoIncrementAttribute].field; - } + if (!this.instance) { + return; + } - id = id || results && results[0][this.getInsertIdField()]; - id = id || metaData && metaData[this.getInsertIdField()]; - id = id || results && results[0][autoIncrementAttribute]; - id = id || autoIncrementAttributeAlias && results && results[0][autoIncrementAttributeAlias]; - this.instance[autoIncrementAttribute] = id; + const modelDefinition = this.model.modelDefinition; + if (!modelDefinition.autoIncrementAttributeName) { + return; } + + const autoIncrementAttribute = modelDefinition.attributes.get(modelDefinition.autoIncrementAttributeName); + + const id = (results?.[0][this.getInsertIdField()]) + ?? (metaData?.[this.getInsertIdField()]) + ?? (results?.[0][autoIncrementAttribute.columnName]); + + this.instance[autoIncrementAttribute.attributeName] = id; } } diff --git a/src/dialects/ibmi/query-generator.js b/src/dialects/ibmi/query-generator.js index 6af7fa98b881..8679264a6dc3 100644 --- a/src/dialects/ibmi/query-generator.js +++ b/src/dialects/ibmi/query-generator.js @@ -1,6 +1,7 @@ 'use strict'; import { underscore } from 'inflection'; +import { conformIndex } from '../../model-internals'; import { rejectInvalidOptions } from '../../utils/check'; import { addTicks } from '../../utils/dialect'; import { Cast, Json, SequelizeMethod } from '../../utils/sequelize-method'; @@ -109,13 +110,11 @@ export class IBMiQueryGenerator extends IBMiQueryGeneratorTypeScript { return true; } - if (columns.customIndex) { - if (typeof indexName !== 'string') { - indexName = `uniq_${tableName}_${columns.fields.join('_')}`; - } - - attributesClause += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; + if (typeof indexName !== 'string') { + indexName = `uniq_${tableName}_${columns.fields.join('_')}`; } + + attributesClause += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; }); } @@ -308,34 +307,39 @@ export class IBMiQueryGenerator extends IBMiQueryGeneratorTypeScript { return super.handleSequelizeMethod(smth, tableName, factory, options, prepend); } - escape(value, field, options) { + escape(value, attribute, options) { if (value instanceof SequelizeMethod) { return this.handleSequelizeMethod(value, undefined, undefined, { replacements: options.replacements }); } - if (value == null || field?.type == null || typeof field.type === 'string') { + if (value == null || attribute?.type == null || typeof attribute.type === 'string') { const format = (value === null && options.where); // use default escape mechanism instead of the DataType's. return SqlString.escape(value, this.options.timezone, this.dialect, format); } - field.type = field.type.toDialectDataType(this.dialect); + if (!attribute.type.belongsToDialect(this.dialect)) { + attribute = { + ...attribute, + type: attribute.type.toDialectDataType(this.dialect), + }; + } if (options.isList && Array.isArray(value)) { const escapeOptions = { ...options, isList: false }; return `(${value.map(valueItem => { - return this.escape(valueItem, field, escapeOptions); + return this.escape(valueItem, attribute, escapeOptions); }).join(', ')})`; } - this.validate(value, field); + this.validate(value, attribute); - return field.type.escape(value, { + return attribute.type.escape(value, { // Users shouldn't have to worry about these args - just give them a function that takes a single arg escape: this.simpleEscape, - field, + field: attribute, timezone: this.options.timezone, operation: options.operation, dialect: this.dialect, @@ -417,7 +421,7 @@ export class IBMiQueryGenerator extends IBMiQueryGeneratorTypeScript { options = nameIndex(options, options.prefix); } - options = Model._conformIndex(options); + options = conformIndex(options); if (!this.dialect.supports.index.type) { delete options.type; @@ -683,7 +687,7 @@ export class IBMiQueryGenerator extends IBMiQueryGeneratorTypeScript { template += ` ADD CONSTRAINT ${fkName} FOREIGN KEY (${attrName})`; } - template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`; + template += ` REFERENCES ${this.quoteTable(attribute.references.table)}`; if (attribute.references.key) { template += ` (${this.quoteIdentifier(attribute.references.key)})`; @@ -706,9 +710,12 @@ export class IBMiQueryGenerator extends IBMiQueryGeneratorTypeScript { attributesToSQL(attributes, options) { const result = Object.create(null); - for (const key in attributes) { - const attribute = attributes[key]; - attribute.field = attribute.field || key; + for (const key of Object.keys(attributes)) { + const attribute = { + ...attributes[key], + field: attributes[key].field || key, + }; + result[attribute.field || key] = this.attributeToSQL(attribute, options); } diff --git a/src/dialects/ibmi/query.js b/src/dialects/ibmi/query.js index e1e5582301bb..f645d041dcec 100644 --- a/src/dialects/ibmi/query.js +++ b/src/dialects/ibmi/query.js @@ -1,5 +1,7 @@ 'use strict'; +import { find } from '../../utils/iterators'; + const _ = require('lodash'); const { AbstractQuery } = require('../abstract/query'); const sequelizeErrors = require('../../errors'); @@ -76,9 +78,10 @@ export class IBMiQuery extends AbstractQuery { if (Object.prototype.hasOwnProperty.call(data[0], key)) { const record = data[0][key]; - const attr = _.find(this.model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key); + const attributes = this.model.modelDefinition.attributes; + const attr = find(attributes.values(), attribute => attribute.attributeName === key || attribute.columnName === key); - this.instance.dataValues[attr && attr.fieldName || key] = record; + this.instance.dataValues[attr?.attributeName || key] = record; } } } diff --git a/src/dialects/mariadb/query.js b/src/dialects/mariadb/query.js index a3dea18732be..1ab7ac66a523 100644 --- a/src/dialects/mariadb/query.js +++ b/src/dialects/mariadb/query.js @@ -98,20 +98,20 @@ export class MariaDbQuery extends AbstractQuery { this.handleInsertQuery(data); if (!this.instance) { + const modelDefinition = this.model?.modelDefinition; + // handle bulkCreate AI primary key if ( - this.model - && this.model.autoIncrementAttribute - && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute - && this.model.rawAttributes[this.model.primaryKeyAttribute] + modelDefinition?.autoIncrementAttributeName + && modelDefinition?.autoIncrementAttributeName === this.model.primaryKeyAttribute ) { // ONLY TRUE IF @auto_increment_increment is set to 1 !! // Doesn't work with GALERA => each node will reserve increment (x for first server, x+1 for next node...) const startId = data[this.getInsertIdField()]; result = new Array(data.affectedRows); - const pkField = this.model.rawAttributes[this.model.primaryKeyAttribute].field; + const pkColumnName = modelDefinition.attributes.get(this.model.primaryKeyAttribute).columnName; for (let i = 0; i < data.affectedRows; i++) { - result[i] = { [pkField]: startId + i }; + result[i] = { [pkColumnName]: startId + i }; } return [result, data.affectedRows]; @@ -219,7 +219,7 @@ export class MariaDbQuery extends AbstractQuery { const values = match ? match[1].split('-') : undefined; const fieldKey = match ? match[2] : undefined; const fieldVal = match ? match[1] : undefined; - const uniqueKey = this.model && this.model.uniqueKeys[fieldKey]; + const uniqueKey = this.model && this.model.getIndexes().find(index => index.unique && index.name === fieldKey); if (uniqueKey) { if (uniqueKey.msg) { diff --git a/src/dialects/mssql/query-generator.js b/src/dialects/mssql/query-generator.js index 1123cce75733..4aca252eb895 100644 --- a/src/dialects/mssql/query-generator.js +++ b/src/dialects/mssql/query-generator.js @@ -196,17 +196,15 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { if (options.uniqueKeys) { _.each(options.uniqueKeys, (columns, indexName) => { - if (columns.customIndex) { - if (typeof indexName !== 'string') { - indexName = generateIndexName(tableName, columns); - } - - attributesClauseParts.push(`CONSTRAINT ${ - this.quoteIdentifier(indexName) - } UNIQUE (${ - columns.fields.map(field => this.quoteIdentifier(field)).join(', ') - })`); + if (typeof indexName !== 'string') { + indexName = generateIndexName(tableName, columns); } + + attributesClauseParts.push(`CONSTRAINT ${ + this.quoteIdentifier(indexName) + } UNIQUE (${ + columns.fields.map(field => this.quoteIdentifier(field)).join(', ') + })`); }); } @@ -460,24 +458,21 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { upsertQuery(tableName, insertValues, updateValues, where, model, options) { const targetTableAlias = this.quoteTable(`${tableName}_target`); const sourceTableAlias = this.quoteTable(`${tableName}_source`); - const primaryKeysAttrs = []; - const identityAttrs = []; - const uniqueAttrs = []; + const primaryKeysColumns = []; + const identityColumns = []; + const uniqueColumns = []; const tableNameQuoted = this.quoteTable(tableName); let needIdentityInsertWrapper = false; + const modelDefinition = model.modelDefinition; // Obtain primaryKeys, uniquekeys and identity attrs from rawAttributes as model is not passed - for (const key in model.rawAttributes) { - if (model.rawAttributes[key].primaryKey) { - primaryKeysAttrs.push(model.rawAttributes[key].field || key); + for (const attribute of modelDefinition.attributes.values()) { + if (attribute.primaryKey) { + primaryKeysColumns.push(attribute.columnName); } - if (model.rawAttributes[key].unique) { - uniqueAttrs.push(model.rawAttributes[key].field || key); - } - - if (model.rawAttributes[key].autoIncrement) { - identityAttrs.push(model.rawAttributes[key].field || key); + if (attribute.autoIncrement) { + identityColumns.push(attribute.columnName); } } @@ -485,9 +480,10 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { for (const index of model.getIndexes()) { if (index.unique && index.fields) { for (const field of index.fields) { - const fieldName = typeof field === 'string' ? field : field.name || field.attribute; - if (!uniqueAttrs.includes(fieldName) && model.rawAttributes[fieldName]) { - uniqueAttrs.push(fieldName); + const columnName = typeof field === 'string' ? field : field.name || field.attribute; + // TODO: columnName can't be used to get an attribute from modelDefinition.attributes, this is a bug + if (!uniqueColumns.includes(columnName) && modelDefinition.attributes.has(columnName)) { + uniqueColumns.push(columnName); } } } @@ -501,7 +497,7 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { let joinCondition; // IDENTITY_INSERT Condition - for (const key of identityAttrs) { + for (const key of identityColumns) { if (insertValues[key] && insertValues[key] !== null) { needIdentityInsertWrapper = true; /* @@ -545,19 +541,19 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { // Search for primary key attribute in clauses -- Model can have two separate unique keys for (const key in clauses) { const keys = Object.keys(clauses[key]); - if (primaryKeysAttrs.includes(keys[0])) { - joinCondition = getJoinSnippet(primaryKeysAttrs).join(' AND '); + if (primaryKeysColumns.includes(keys[0])) { + joinCondition = getJoinSnippet(primaryKeysColumns).join(' AND '); break; } } if (!joinCondition) { - joinCondition = getJoinSnippet(uniqueAttrs).join(' AND '); + joinCondition = getJoinSnippet(uniqueColumns).join(' AND '); } } // Remove the IDENTITY_INSERT Column from update - const filteredUpdateClauses = updateKeys.filter(key => !identityAttrs.includes(key)) + const filteredUpdateClauses = updateKeys.filter(key => !identityColumns.includes(key)) .map(key => { const value = this.escape(updateValues[key], undefined, options); key = this.quoteIdentifier(key); @@ -609,7 +605,7 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { } // handle self-referential constraints - if (attribute.references && attribute.Model && this.isSameTable(attribute.Model.tableName, attribute.references.model)) { + if (attribute.references && attribute.Model && this.isSameTable(attribute.Model.tableName, attribute.references.table)) { this.sequelize.log('MSSQL does not support self-referential constraints, ' + 'we will remove it but we recommend restructuring your query'); attribute.onDelete = ''; @@ -655,7 +651,7 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { } if ((!options || !options.withoutForeignKeyConstraints) && attribute.references) { - template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`; + template += ` REFERENCES ${this.quoteTable(attribute.references.table)}`; if (attribute.references.key) { template += ` (${this.quoteIdentifier(attribute.references.key)})`; @@ -680,28 +676,25 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript { } attributesToSQL(attributes, options) { - const result = {}; + const result = Object.create(null); const existingConstraints = []; - let key; - let attribute; - for (key in attributes) { - attribute = attributes[key]; + for (const key of Object.keys(attributes)) { + const attribute = { ...attributes[key] }; if (attribute.references) { - if (existingConstraints.includes(attribute.references.model.toString())) { + if (existingConstraints.includes(this.quoteTable(attribute.references.table))) { // no cascading constraints to a table more than once attribute.onDelete = ''; attribute.onUpdate = ''; } else { - existingConstraints.push(attribute.references.model.toString()); + existingConstraints.push(this.quoteTable(attribute.references.table)); // NOTE: this really just disables cascading updates for all // definitions. Can be made more robust to support the // few cases where MSSQL actually supports them attribute.onUpdate = ''; } - } if (key && !attribute.field) { diff --git a/src/dialects/mssql/query-interface.js b/src/dialects/mssql/query-interface.js index 0918a50b65dc..0ba451921ff1 100644 --- a/src/dialects/mssql/query-interface.js +++ b/src/dialects/mssql/query-interface.js @@ -68,11 +68,10 @@ export class MsSqlQueryInterface extends AbstractQueryInterface { } // Lets combine unique keys and indexes into one - let indexes = Object.values(model.uniqueKeys).map(item => item.fields); - indexes = indexes.concat(Object.values(model.getIndexes()).filter(item => item.unique).map(item => item.fields)); + const uniqueColumnNames = Object.values(model.getIndexes()).filter(c => c.unique && c.fields.length > 0).map(c => c.fields); const attributes = Object.keys(insertValues); - for (const index of indexes) { + for (const index of uniqueColumnNames) { if (_.intersection(attributes, index).length === index.length) { where = {}; for (const field of index) { diff --git a/src/dialects/mssql/query.js b/src/dialects/mssql/query.js index 10bb1cf8971c..03a192b5054f 100644 --- a/src/dialects/mssql/query.js +++ b/src/dialects/mssql/query.js @@ -308,11 +308,13 @@ export class MsSqlQuery extends AbstractQuery { // TODO: err can be an AggregateError. When that happens, we must throw an AggregateError too instead of throwing only the second error, // or we lose important information - match = err.message.match(/Violation of (?:UNIQUE|PRIMARY) KEY constraint '([^']*)'. Cannot insert duplicate key in object '.*'.(:? The duplicate key value is \((.*)\).)?/); - match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/); + match = err.message.match(/Violation of (?:UNIQUE|PRIMARY) KEY constraint '([^']*)'. Cannot insert duplicate key in object '.*'\.(:? The duplicate key value is \((.*)\).)?/s); + match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'\.(:? The duplicate key value is \((.*)\).)?/s); + if (match && match.length > 1) { let fields = {}; - const uniqueKey = this.model && this.model.uniqueKeys[match[1]]; + const uniqueKey = this.model && this.model.getIndexes().find(index => index.unique && index.name === match[1]); + let message = 'Validation error'; if (uniqueKey && Boolean(uniqueKey.msg)) { diff --git a/src/dialects/mysql/query-generator.js b/src/dialects/mysql/query-generator.js index 0baf8cdb287f..aabd5c959010 100644 --- a/src/dialects/mysql/query-generator.js +++ b/src/dialects/mysql/query-generator.js @@ -134,13 +134,12 @@ export class MySqlQueryGenerator extends MySqlQueryGeneratorTypeScript { if (options.uniqueKeys) { _.each(options.uniqueKeys, (columns, indexName) => { - if (columns.customIndex) { - if (typeof indexName !== 'string') { - indexName = `uniq_${tableName}_${columns.fields.join('_')}`; - } - - attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; + if (typeof indexName !== 'string') { + indexName = `uniq_${tableName}_${columns.fields.join('_')}`; } + + attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)) + .join(', ')})`; }); } @@ -427,7 +426,7 @@ export class MySqlQueryGenerator extends MySqlQueryGeneratorTypeScript { template += `, ADD CONSTRAINT ${fkName} FOREIGN KEY (${this.quoteIdentifier(options.foreignKey)})`; } - template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`; + template += ` REFERENCES ${this.quoteTable(attribute.references.table)}`; if (attribute.references.key) { template += ` (${this.quoteIdentifier(attribute.references.key)})`; diff --git a/src/dialects/mysql/query-interface.js b/src/dialects/mysql/query-interface.js index fec7eba149a8..3766068ced51 100644 --- a/src/dialects/mysql/query-interface.js +++ b/src/dialects/mysql/query-interface.js @@ -1,5 +1,6 @@ 'use strict'; +import { getObjectFromMap } from '../../utils/object'; import { assertNoReservedBind, combineBinds } from '../../utils/sql'; const sequelizeErrors = require('../../errors'); @@ -48,14 +49,15 @@ export class MySqlQueryInterface extends AbstractQueryInterface { assertNoReservedBind(options.bind); } + const modelDefinition = options.model.modelDefinition; + options = { ...options }; options.type = QueryTypes.UPSERT; options.updateOnDuplicate = Object.keys(updateValues); - options.upsertKeys = Object.values(options.model.primaryKeys).map(item => item.field); + options.upsertKeys = Array.from(modelDefinition.primaryKeysAttributeNames, pkAttrName => modelDefinition.getColumnName(pkAttrName)); - const model = options.model; - const { query, bind } = this.queryGenerator.insertQuery(tableName, insertValues, model.rawAttributes, options); + const { query, bind } = this.queryGenerator.insertQuery(tableName, insertValues, getObjectFromMap(modelDefinition.attributes), options); // unlike bind, replacements are handled by QueryGenerator, not QueryRaw delete options.replacements; diff --git a/src/dialects/mysql/query.js b/src/dialects/mysql/query.js index 1bc9009b40b3..7acb41ecf3d9 100644 --- a/src/dialects/mysql/query.js +++ b/src/dialects/mysql/query.js @@ -100,18 +100,18 @@ export class MySqlQuery extends AbstractQuery { this.handleInsertQuery(data); if (!this.instance) { + const modelDefinition = this.model?.modelDefinition; + // handle bulkCreate AI primary key if ( data.constructor.name === 'ResultSetHeader' - && this.model - && this.model.autoIncrementAttribute - && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute - && this.model.rawAttributes[this.model.primaryKeyAttribute] + && modelDefinition?.autoIncrementAttributeName + && modelDefinition?.autoIncrementAttributeName === this.model.primaryKeyAttribute ) { const startId = data[this.getInsertIdField()]; result = []; for (let i = BigInt(startId); i < BigInt(startId) + BigInt(data.affectedRows); i = i + 1n) { - result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: typeof startId === 'string' ? i.toString() : Number(i) }); + result.push({ [modelDefinition.getColumnName(this.model.primaryKeyAttribute)]: typeof startId === 'string' ? i.toString() : Number(i) }); } } else { result = data[this.getInsertIdField()]; @@ -197,7 +197,7 @@ export class MySqlQuery extends AbstractQuery { const values = match ? match[1].split('-') : undefined; const fieldKey = match ? match[2].split('.').pop() : undefined; const fieldVal = match ? match[1] : undefined; - const uniqueKey = this.model && this.model.uniqueKeys[fieldKey]; + const uniqueKey = this.model && this.model.getIndexes().find(index => index.unique && index.name === fieldKey); if (uniqueKey) { if (uniqueKey.msg) { diff --git a/src/dialects/postgres/query-generator.js b/src/dialects/postgres/query-generator.js index 1a133cad9e77..54abdd2342e2 100644 --- a/src/dialects/postgres/query-generator.js +++ b/src/dialects/postgres/query-generator.js @@ -124,18 +124,17 @@ export class PostgresQueryGenerator extends PostgresQueryGeneratorTypeScript { let attributesClause = attrStr.join(', '); if (options.uniqueKeys) { - _.each(options.uniqueKeys, (columns, indexName) => { - if (columns.customIndex) { - if (typeof indexName !== 'string') { - indexName = generateIndexName(tableName, columns); - } - - attributesClause += `, CONSTRAINT ${ - this.quoteIdentifier(indexName) - } UNIQUE (${ - columns.fields.map(field => this.quoteIdentifier(field)).join(', ') - })`; + _.each(options.uniqueKeys, (index, indexName) => { + if (typeof indexName !== 'string') { + indexName = generateIndexName(tableName, index); } + + attributesClause += `, CONSTRAINT ${ + this.quoteIdentifier(indexName) + } UNIQUE (${ + index.fields.map(field => this.quoteIdentifier(field)) + .join(', ') + })`; }); } @@ -508,14 +507,14 @@ export class PostgresQueryGenerator extends PostgresQueryGeneratorTypeScript { if (options.schema) { schema = options.schema; } else if ( - (!attribute.references.model || typeof attribute.references.model === 'string') + (!attribute.references.table || typeof attribute.references.table === 'string') && options.table && options.table.schema ) { schema = options.table.schema; } - const referencesTable = this.extractTableDetails(attribute.references.model, { schema }); + const referencesTable = this.extractTableDetails(attribute.references.table, { schema }); let referencesKey; diff --git a/src/dialects/postgres/query-interface.js b/src/dialects/postgres/query-interface.js index 8f5a6aa4faf2..23efd9b34209 100644 --- a/src/dialects/postgres/query-interface.js +++ b/src/dialects/postgres/query-interface.js @@ -233,24 +233,29 @@ export class PostgresQueryInterface extends AbstractQueryInterface { async dropTable(tableName, options) { await super.dropTable(tableName, options); const promises = []; - const instanceTable = this.sequelize.modelManager.getModel(tableName, { attribute: 'tableName' }); + // TODO: we support receiving the model class instead of getting it from modelManager. More than one model can use the same table. + const model = this.sequelize.modelManager.getModel(tableName, { attribute: 'tableName' }); - if (!instanceTable) { + if (!model) { // Do nothing when model is not available return; } const getTableName = (!options || !options.schema || options.schema === 'public' ? '' : `${options.schema}_`) + tableName; - const keys = Object.keys(instanceTable.rawAttributes); - const keyLen = keys.length; + const attributes = model.modelDefinition.attributes; - for (let i = 0; i < keyLen; i++) { - if (instanceTable.rawAttributes[keys[i]].type instanceof DataTypes.ENUM) { - const sql = this.queryGenerator.pgEnumDrop(getTableName, keys[i]); - options.supportsSearchPath = false; - promises.push(this.sequelize.queryRaw(sql, { ...options, raw: true })); + for (const attribute of attributes.values()) { + if (!(attribute.type instanceof DataTypes.ENUM)) { + continue; } + + const sql = this.queryGenerator.pgEnumDrop(getTableName, attribute.attributeName); + promises.push(this.sequelize.queryRaw(sql, { + ...options, + raw: true, + supportsSearchPath: false, + })); } await Promise.all(promises); diff --git a/src/dialects/postgres/query.js b/src/dialects/postgres/query.js index 08f439543bd4..90dbce38ca0c 100644 --- a/src/dialects/postgres/query.js +++ b/src/dialects/postgres/query.js @@ -188,11 +188,12 @@ export class PostgresQuery extends AbstractQuery { // Postgres will treat tables as case-insensitive, so fix the case // of the returned values to match attributes if (this.options.raw === false && this.sequelize.options.quoteIdentifiers === false) { - const attrsMap = _.reduce(this.model.rawAttributes, (m, v, k) => { - m[k.toLowerCase()] = k; + const attrsMap = Object.create(null); + + for (const attrName of this.model.modelDefinition.attributes.keys()) { + attrsMap[attrName.toLowerCase()] = attrName; + } - return m; - }, {}); result = rows.map(row => { return _.mapKeys(row, (value, key) => { const targetAttr = attrsMap[key]; @@ -274,10 +275,10 @@ export class PostgresQuery extends AbstractQuery { if (rows[0]) { for (const attributeOrColumnName of Object.keys(rows[0])) { - const attribute = _.find(this.model.rawAttributes, attribute => { - // TODO: this should not be searching in both column names & attribute names. It will lead to collisions. Use only one or the other. - return attribute.fieldName === attributeOrColumnName || attribute.field === attributeOrColumnName; - }); + const modelDefinition = this.model.modelDefinition; + + // TODO: this should not be searching in both column names & attribute names. It will lead to collisions. Use only one or the other. + const attribute = modelDefinition.attributes.get(attributeOrColumnName) ?? modelDefinition.columns.get(attributeOrColumnName); const updatedValue = this._parseDatabaseValue(rows[0][attributeOrColumnName], attribute?.type); @@ -355,14 +356,13 @@ export class PostgresQuery extends AbstractQuery { )); }); - if (this.model && this.model.uniqueKeys) { - _.forOwn(this.model.uniqueKeys, constraint => { - if (_.isEqual(constraint.fields, Object.keys(fields)) && Boolean(constraint.msg)) { - message = constraint.msg; - - return false; + if (this.model) { + for (const index of this.model.getIndexes()) { + if (index.unique && _.isEqual(index.fields, Object.keys(fields)) && index.msg) { + message = index.msg; + break; } - }); + } } return new sequelizeErrors.UniqueConstraintError({ message, errors, cause: err, fields, stack: errStack }); diff --git a/src/dialects/snowflake/query-generator.js b/src/dialects/snowflake/query-generator.js index 440ee7b6f2d7..a62158bf1d70 100644 --- a/src/dialects/snowflake/query-generator.js +++ b/src/dialects/snowflake/query-generator.js @@ -172,13 +172,11 @@ export class SnowflakeQueryGenerator extends SnowflakeQueryGeneratorTypeScript { if (options.uniqueKeys) { _.each(options.uniqueKeys, (columns, indexName) => { - if (columns.customIndex) { - if (typeof indexName !== 'string') { - indexName = `uniq_${tableName}_${columns.fields.join('_')}`; - } - - attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; + if (typeof indexName !== 'string') { + indexName = `uniq_${tableName}_${columns.fields.join('_')}`; } + + attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; }); } @@ -499,7 +497,7 @@ export class SnowflakeQueryGenerator extends SnowflakeQueryGeneratorTypeScript { template += `, ADD CONSTRAINT ${fkName} FOREIGN KEY (${attrName})`; } - template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`; + template += ` REFERENCES ${this.quoteTable(attribute.references.table)}`; if (attribute.references.key) { template += ` (${this.quoteIdentifier(attribute.references.key)})`; diff --git a/src/dialects/snowflake/query-interface.js b/src/dialects/snowflake/query-interface.js index 0010d54762e3..cf5fc817f9c8 100644 --- a/src/dialects/snowflake/query-interface.js +++ b/src/dialects/snowflake/query-interface.js @@ -1,5 +1,6 @@ 'use strict'; +import { getObjectFromMap } from '../../utils/object'; import { assertNoReservedBind, combineBinds } from '../../utils/sql'; const sequelizeErrors = require('../../errors'); @@ -53,7 +54,8 @@ export class SnowflakeQueryInterface extends AbstractQueryInterface { options.updateOnDuplicate = Object.keys(updateValues); const model = options.model; - const { query, bind } = this.queryGenerator.insertQuery(tableName, insertValues, model.rawAttributes, options); + const modelDefinition = model.modelDefinition; + const { query, bind } = this.queryGenerator.insertQuery(tableName, insertValues, getObjectFromMap(modelDefinition.attributes), options); delete options.replacements; options.bind = combineBinds(options.bind, bind); diff --git a/src/dialects/snowflake/query.js b/src/dialects/snowflake/query.js index c414b580b2ed..93ec8361e473 100644 --- a/src/dialects/snowflake/query.js +++ b/src/dialects/snowflake/query.js @@ -90,18 +90,18 @@ export class SnowflakeQuery extends AbstractQuery { this.handleInsertQuery(data); if (!this.instance) { + const modelDefinition = this.model?.modelDefinition; + // handle bulkCreate AI primary key if ( data.constructor.name === 'ResultSetHeader' - && this.model - && this.model.autoIncrementAttribute - && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute - && this.model.rawAttributes[this.model.primaryKeyAttribute] + && modelDefinition?.autoIncrementAttributeName + && modelDefinition?.autoIncrementAttributeName === this.model.primaryKeyAttribute ) { const startId = data[this.getInsertIdField()]; result = []; for (let i = startId; i < startId + data.affectedRows; i++) { - result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i }); + result.push({ [modelDefinition.getColumnName(this.model.primaryKeyAttribute)]: i }); } } else { result = data[this.getInsertIdField()]; @@ -113,15 +113,15 @@ export class SnowflakeQuery extends AbstractQuery { // Snowflake will treat tables as case-insensitive, so fix the case // of the returned values to match attributes if (this.options.raw === false && this.sequelize.options.quoteIdentifiers === false) { - const sfAttrMap = _.reduce(this.model.rawAttributes, (m, v, k) => { - m[k.toUpperCase()] = k; + const attrsMap = Object.create(null); - return m; - }, {}); + for (const attrName of this.model.modelDefinition.attributes.keys()) { + attrsMap[attrName.toLowerCase()] = attrName; + } data = data.map(data => _.reduce(data, (prev, value, key) => { - if (value !== undefined && sfAttrMap[key]) { - prev[sfAttrMap[key]] = value; + if (value !== undefined && attrsMap[key]) { + prev[attrsMap[key]] = value; delete prev[key]; } @@ -208,7 +208,7 @@ export class SnowflakeQuery extends AbstractQuery { const values = match ? match[1].split('-') : undefined; const fieldKey = match ? match[2] : undefined; const fieldVal = match ? match[1] : undefined; - const uniqueKey = this.model && this.model.uniqueKeys[fieldKey]; + const uniqueKey = this.model && this.model.getIndexes().find(index => index.unique && index.name === fieldKey); if (uniqueKey) { if (uniqueKey.msg) { diff --git a/src/dialects/sqlite/query-generator.js b/src/dialects/sqlite/query-generator.js index d2d4f8227dbe..8340004df1bd 100644 --- a/src/dialects/sqlite/query-generator.js +++ b/src/dialects/sqlite/query-generator.js @@ -307,60 +307,59 @@ export class SqliteQueryGenerator extends SqliteQueryGeneratorTypeScript { attributesToSQL(attributes, options) { const result = {}; for (const name in attributes) { - const dataType = attributes[name]; - const fieldName = dataType.field || name; + const attribute = attributes[name]; + const columnName = attribute.field || attribute.columnName || name; - if (_.isObject(dataType)) { - let sql = dataType.type.toString(); + if (_.isObject(attribute)) { + let sql = attribute.type.toString(); - if (dataType.allowNull === false) { + if (attribute.allowNull === false) { sql += ' NOT NULL'; } - if (defaultValueSchemable(dataType.defaultValue)) { + if (defaultValueSchemable(attribute.defaultValue)) { // TODO thoroughly check that DataTypes.NOW will properly // get populated on all databases as DEFAULT value // i.e. mysql requires: DEFAULT CURRENT_TIMESTAMP - sql += ` DEFAULT ${this.escape(dataType.defaultValue, dataType, options)}`; + sql += ` DEFAULT ${this.escape(attribute.defaultValue, attribute, options)}`; } - if (dataType.unique === true) { + if (attribute.unique === true) { sql += ' UNIQUE'; } - if (dataType.primaryKey) { + if (attribute.primaryKey) { sql += ' PRIMARY KEY'; - if (dataType.autoIncrement) { + if (attribute.autoIncrement) { sql += ' AUTOINCREMENT'; } } - if (dataType.references) { - const referencesTable = this.quoteTable(dataType.references.model); + if (attribute.references) { + const referencesTable = this.quoteTable(attribute.references.table); let referencesKey; - if (dataType.references.key) { - referencesKey = this.quoteIdentifier(dataType.references.key); + if (attribute.references.key) { + referencesKey = this.quoteIdentifier(attribute.references.key); } else { referencesKey = this.quoteIdentifier('id'); } sql += ` REFERENCES ${referencesTable} (${referencesKey})`; - if (dataType.onDelete) { - sql += ` ON DELETE ${dataType.onDelete.toUpperCase()}`; + if (attribute.onDelete) { + sql += ` ON DELETE ${attribute.onDelete.toUpperCase()}`; } - if (dataType.onUpdate) { - sql += ` ON UPDATE ${dataType.onUpdate.toUpperCase()}`; + if (attribute.onUpdate) { + sql += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`; } - } - result[fieldName] = sql; + result[columnName] = sql; } else { - result[fieldName] = dataType; + result[columnName] = attribute; } } diff --git a/src/dialects/sqlite/query-interface.js b/src/dialects/sqlite/query-interface.js index 36dc26a775a3..73981da2dfc6 100644 --- a/src/dialects/sqlite/query-interface.js +++ b/src/dialects/sqlite/query-interface.js @@ -36,13 +36,20 @@ export class SqliteQueryInterface extends AbstractQueryInterface { * * @override */ - async changeColumn(tableName, attributeName, dataTypeOrOptions, options) { + async changeColumn(tableName, columnName, dataTypeOrOptions, options) { options = options || {}; - const fields = await this.describeTable(tableName, options); - Object.assign(fields[attributeName], this.normalizeAttribute(dataTypeOrOptions)); + const columns = await this.describeTable(tableName, options); + for (const column of Object.values(columns)) { + // This is handled by copying indexes over, + // we don't use "unique" because it creates an index with a name + // we can't control + delete column.unique; + } - return this.alterTableInternal(tableName, fields, options); + Object.assign(columns[columnName], this.normalizeAttribute(dataTypeOrOptions)); + + return this.alterTableInternal(tableName, columns, options); } /** @@ -241,7 +248,7 @@ export class SqliteQueryInterface extends AbstractQueryInterface { const foreignKeys = await this.getForeignKeyReferencesForTable(tableName, options); for (const foreignKey of foreignKeys) { data[foreignKey.columnName].references = { - model: foreignKey.referencedTableName, + table: foreignKey.referencedTableName, key: foreignKey.referencedColumnName, }; @@ -267,17 +274,35 @@ export class SqliteQueryInterface extends AbstractQueryInterface { * Workaround for sqlite's limited alter table support. * * @param {string} tableName - The table's name - * @param {ColumnsDescription} fields - The table's description + * @param {ColumnsDescription} columns - The table's description * @param {QueryOptions} options - Query options * @private */ - async alterTableInternal(tableName, fields, options) { + async alterTableInternal(tableName, columns, options) { return this.withForeignKeysOff(options, async () => { const savepointName = this.getSavepointName(); await this.sequelize.query(`SAVEPOINT ${savepointName};`, options); try { - const sql = this.queryGenerator.removeColumnQuery(tableName, fields); + const indexes = await this.showIndex(tableName, options); + for (const index of indexes) { + // This index is reserved by SQLite, we can't add it through addIndex and must use "UNIQUE" on the column definition instead. + if (!index.constraintName.startsWith('sqlite_autoindex_')) { + continue; + } + + if (!index.unique) { + continue; + } + + for (const field of index.fields) { + if (columns[field.attribute]) { + columns[field.attribute].unique = true; + } + } + } + + const sql = this.queryGenerator.removeColumnQuery(tableName, columns); const subQueries = sql.split(';').filter(q => q !== ''); for (const subQuery of subQueries) { @@ -298,6 +323,15 @@ export class SqliteQueryInterface extends AbstractQueryInterface { }); } + await Promise.all(indexes.map(async index => { + // This index is reserved by SQLite, we can't add it through addIndex and must use "UNIQUE" on the column definition instead. + if (index.constraintName.startsWith('sqlite_autoindex_')) { + return; + } + + return this.addIndex(tableName, index); + })); + await this.sequelize.query(`RELEASE ${savepointName};`, options); } catch (error) { await this.sequelize.query(`ROLLBACK TO ${savepointName};`, options); diff --git a/src/dialects/sqlite/query.js b/src/dialects/sqlite/query.js index 9dcea021b78b..117007ad7606 100644 --- a/src/dialects/sqlite/query.js +++ b/src/dialects/sqlite/query.js @@ -61,18 +61,18 @@ export class SqliteQuery extends AbstractQuery { if (this.isInsertQuery(results, metaData) || this.isUpsertQuery()) { this.handleInsertQuery(results, metaData); if (!this.instance) { + const modelDefinition = this.model?.modelDefinition; + // handle bulkCreate AI primary key if ( metaData.constructor.name === 'Statement' - && this.model - && this.model.autoIncrementAttribute - && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute - && this.model.rawAttributes[this.model.primaryKeyAttribute] + && modelDefinition?.autoIncrementAttributeName + && modelDefinition?.autoIncrementAttributeName === this.model.primaryKeyAttribute ) { const startId = metaData[this.getInsertIdField()] - metaData.changes + 1; result = []; for (let i = startId; i < startId + metaData.changes; i++) { - result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i }); + result.push({ [modelDefinition.getColumnName(this.model.primaryKeyAttribute)]: i }); } } else { result = metaData[this.getInsertIdField()]; @@ -94,44 +94,6 @@ export class SqliteQuery extends AbstractQuery { } if (this.isSelectQuery()) { - if (this.options.raw) { - return this.handleSelectQuery(results); - } - - // This is a map of prefix strings to models, e.g. user.projects -> Project model - const prefixes = this._collectModels(this.options.include); - - results = results.map(result => { - return _.mapValues(result, (value, name) => { - let model; - if (name.includes('.')) { - const lastind = name.lastIndexOf('.'); - - model = prefixes[name.slice(0, Math.max(0, lastind))]; - - name = name.slice(lastind + 1); - } else { - model = this.options.model; - } - - const tableName = model.getTableName().toString().replace(/`/g, ''); - const tableTypes = columnTypes[tableName] || {}; - - if (tableTypes && !(name in tableTypes)) { - // The column is aliased - _.forOwn(model.rawAttributes, (attribute, key) => { - if (name === key && attribute.field) { - name = attribute.field; - - return false; - } - }); - } - - return value; - }); - }); - return this.handleSelectQuery(results); } @@ -403,13 +365,12 @@ export class SqliteQuery extends AbstractQuery { } if (this.model) { - _.forOwn(this.model.uniqueKeys, constraint => { - if (_.isEqual(constraint.fields, fields) && Boolean(constraint.msg)) { - message = constraint.msg; - - return false; + for (const index of this.model.getIndexes()) { + if (index.unique && _.isEqual(index.fields, fields) && index.msg) { + message = index.msg; + break; } - }); + } } return new sequelizeErrors.UniqueConstraintError({ message, errors, cause: err, fields, stack: errStack }); diff --git a/src/hooks-legacy.ts b/src/hooks-legacy.ts index aa1d04a31ee2..026d6eb38019 100644 --- a/src/hooks-legacy.ts +++ b/src/hooks-legacy.ts @@ -1,4 +1,4 @@ -import type { HookHandlerBuilder } from './hooks.js'; +import type { HookHandlerBuilder, HookHandler } from './hooks.js'; import { hooksReworked } from './utils/deprecations.js'; // TODO: delete this in Sequelize v8 @@ -13,11 +13,12 @@ export interface LegacyRunHookFunction { } export function legacyBuildRunHook( - hookHandlerBuilder: HookHandlerBuilder, + // added for typing purposes + _hookHandlerBuilder: HookHandlerBuilder, ): LegacyRunHookFunction { return async function runHooks( - this: object, + this: { hooks: HookHandler }, hookName: HookName, ...args: HookConfig[HookName] extends (...args2: any) => any ? Parameters @@ -25,7 +26,7 @@ export function legacyBuildRunHook( ): Promise { hooksReworked(); - return hookHandlerBuilder.getFor(this).runAsync(hookName, ...args); + return this.hooks.runAsync(hookName, ...args); }; } @@ -49,10 +50,11 @@ export interface LegacyAddAnyHookFunction { } export function legacyBuildAddAnyHook( - hookHandlerBuilder: HookHandlerBuilder, + // added for typing purposes + _hookHandlerBuilder: HookHandlerBuilder, ): LegacyAddAnyHookFunction { - return function addHook( + return function addHook }, HookName extends keyof HookConfig>( this: This, hookName: HookName, listenerNameOrHook: HookConfig[HookName] | string, @@ -62,10 +64,10 @@ export function legacyBuildAddAnyHook( if (hook) { // @ts-expect-error -- TypeScript struggles with the multiple possible signatures of addListener - hookHandlerBuilder.getFor(this).addListener(hookName, hook, listenerNameOrHook); + this.hooks.addListener(hookName, hook, listenerNameOrHook); } else { // @ts-expect-error -- TypeScript struggles with the multiple possible signatures of addListener - hookHandlerBuilder.getFor(this).addListener(hookName, listenerNameOrHook); + this.hooks.addListener(hookName, listenerNameOrHook); } return this; @@ -90,7 +92,7 @@ export function legacyBuildAddHook, hookName: HookName, ): LegacyAddHookFunction { - return function addHook( + return function addHook }>( this: This, listenerNameOrHook: HookConfig[HookName] | string, hook?: HookConfig[HookName], @@ -99,32 +101,41 @@ export function legacyBuildAddHook(hookHandlerBuilder: HookHandlerBuilder) { - return function hasHook(this: object, hookName: HookName): boolean { +export function legacyBuildHasHook( + // added for typing purposes + _hookHandlerBuilder: HookHandlerBuilder, +) { + return function hasHook( + this: { hooks: HookHandler }, + hookName: HookName, + ): boolean { hooksReworked(); - return hookHandlerBuilder.getFor(this).hasListeners(hookName); + return this.hooks.hasListeners(hookName); }; } -export function legacyBuildRemoveHook(hookHandlerBuilder: HookHandlerBuilder) { +export function legacyBuildRemoveHook( + // added for typing purposes + _hookHandlerBuilder: HookHandlerBuilder, +) { return function removeHook( - this: object, + this: { hooks: HookHandler }, hookName: HookName, listenerNameOrListener: HookConfig[HookName] | string, ): void { hooksReworked(); - return hookHandlerBuilder.getFor(this).removeListener(hookName, listenerNameOrListener); + return this.hooks.removeListener(hookName, listenerNameOrListener); }; } diff --git a/src/hooks.ts b/src/hooks.ts index b632e54d8dbe..b4618724abf9 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -78,6 +78,12 @@ export class HookHandler { return this.#listeners.count(hookName) > 0; } + getListenerCount(hookName: keyof HookConfig): number { + this.#assertValidHookName(hookName); + + return this.#listeners.count(hookName); + } + runSync( hookName: HookName, ...args: HookConfig[HookName] extends (...args2: any) => any @@ -141,7 +147,7 @@ export class HookHandler { } addListeners(listeners: { - [Key in keyof HookConfig]?: AllowArray + [Key in keyof HookConfig]?: AllowArray }) { for (const hookName of this.#validHookNames) { const hookListeners = listeners[hookName]; @@ -151,7 +157,11 @@ export class HookHandler { const hookListenersArray = Array.isArray(hookListeners) ? hookListeners : [hookListeners]; for (const listener of hookListenersArray) { - this.addListener(hookName, listener); + if (typeof listener === 'function') { + this.addListener(hookName, listener); + } else { + this.addListener(hookName, listener.callback, listener.name); + } } } } diff --git a/src/instance-validator.js b/src/instance-validator.js index ca4bf6ac1ceb..609eb3735d6c 100644 --- a/src/instance-validator.js +++ b/src/instance-validator.js @@ -28,9 +28,9 @@ export class InstanceValidator { }; if (options.fields && !options.skip) { - options.skip = _.difference(Object.keys(modelInstance.constructor.rawAttributes), options.fields); + options.skip = _.difference(Array.from(modelInstance.constructor.modelDefinition.attributes.keys()), options.fields); } else { - options.skip = options.skip || []; + options.skip ??= []; } this.options = options; @@ -133,26 +133,30 @@ export class InstanceValidator { // promisify all attribute invocations const validators = []; - _.forIn(this.modelInstance.rawAttributes, (rawAttribute, field) => { - if (this.options.skip.includes(field)) { - return; + const { attributes } = this.modelInstance.constructor.modelDefinition; + + for (const attribute of attributes.values()) { + const attrName = attribute.attributeName; + + if (this.options.skip.includes(attrName)) { + continue; } - const value = this.modelInstance.dataValues[field]; + const value = this.modelInstance.dataValues[attrName]; if (value instanceof SequelizeMethod) { - return; + continue; } - if (!rawAttribute._autoGenerated && !rawAttribute.autoIncrement) { + if (!attribute._autoGenerated && !attribute.autoIncrement) { // perform validations based on schema - this._validateSchema(rawAttribute, field, value); + this._validateSchema(attribute, attrName, value); } - if (Object.prototype.hasOwnProperty.call(this.modelInstance.validators, field)) { - validators.push(this._singleAttrValidate(value, field, rawAttribute.allowNull)); + if (attribute.validate) { + validators.push(this._singleAttrValidate(value, attrName, attribute.allowNull)); } - }); + } return await Promise.all(validators); } @@ -191,22 +195,24 @@ export class InstanceValidator { * @private * * @param {*} value Anything. - * @param {string} field The field name. + * @param {string} attributeName The attribute name. * @param {boolean} allowNull Whether or not the schema allows null values * * @returns {Promise} A promise, will always resolve, auto populates error on this.error local object. */ - async _singleAttrValidate(value, field, allowNull) { + async _singleAttrValidate(value, attributeName, allowNull) { // If value is null and allowNull is false, no validators should run (see #9143) - if ((value === null || value === undefined) && !allowNull) { + if (value == null && !allowNull) { // The schema validator (_validateSchema) has already generated the validation error. Nothing to do here. return; } // Promisify each validator const validators = []; - _.forIn(this.modelInstance.validators[field], (test, validatorType) => { + const attribute = this.modelInstance.constructor.modelDefinition.attributes.get(attributeName); + + _.forIn(attribute.validate, (test, validatorType) => { if (['isUrl', 'isURL', 'isEmail'].includes(validatorType)) { // Preserve backwards compat. Validator.js now expects the second param to isURL and isEmail to be an object if (typeof test === 'object' && test !== null && test.msg) { @@ -220,7 +226,7 @@ export class InstanceValidator { // Custom validators should always run, except if value is null and allowNull is false (see #9143) if (typeof test === 'function') { - validators.push(this._invokeCustomValidator(test, validatorType, true, value, field)); + validators.push(this._invokeCustomValidator(test, validatorType, true, value, attributeName)); return; } @@ -230,7 +236,7 @@ export class InstanceValidator { return; } - const validatorPromise = this._invokeBuiltinValidator(value, test, validatorType, field); + const validatorPromise = this._invokeBuiltinValidator(value, test, validatorType, attributeName); // errors are handled in settling, stub this validatorPromise.catch(() => {}); validators.push(validatorPromise); @@ -239,7 +245,7 @@ export class InstanceValidator { return Promise .all(validators.map(validator => validator.catch(error => { const isBuiltIn = Boolean(error.validatorName); - this._pushError(isBuiltIn, field, error, value, error.validatorName, error.validatorArgs); + this._pushError(isBuiltIn, attributeName, error, value, error.validatorName, error.validatorArgs); }))); } @@ -354,23 +360,24 @@ export class InstanceValidator { /** * Will validate a single field against its schema definition (isnull). * - * @param {object} rawAttribute As defined in the Schema. - * @param {string} field The field name. + * @param {object} attribute As defined in the Schema. + * @param {string} attributeName The field name. * @param {*} value anything. * * @private */ - _validateSchema(rawAttribute, field, value) { - if (rawAttribute.allowNull === false && (value === null || value === undefined)) { - const association = Object.values(this.modelInstance.constructor.associations).find(association => association instanceof BelongsTo && association.foreignKey === rawAttribute.fieldName); + _validateSchema(attribute, attributeName, value) { + if (attribute.allowNull === false && value == null) { + const association = Object.values(this.modelInstance.constructor.associations).find(association => association instanceof BelongsTo && association.foreignKey === attribute.fieldName); if (!association || !this.modelInstance.get(association.as)) { - const validators = this.modelInstance.validators[field]; - const errMsg = _.get(validators, 'notNull.msg', `${this.modelInstance.constructor.name}.${field} cannot be null`); + const modelDefinition = this.modelInstance.constructor.modelDefinition; + const validators = modelDefinition.attributes.get(attributeName)?.validate; + const errMsg = _.get(validators, 'notNull.msg', `${this.modelInstance.constructor.name}.${attributeName} cannot be null`); this.errors.push(new sequelizeError.ValidationErrorItem( errMsg, 'notNull violation', // sequelizeError.ValidationErrorItem.Origins.CORE, - field, + attributeName, value, this.modelInstance, 'is_null', @@ -378,9 +385,9 @@ export class InstanceValidator { } } - const type = rawAttribute.type; + const type = attribute.type; if (value != null && !(value instanceof SequelizeMethod) && type instanceof AbstractDataType) { - const error = validateDataType(type, field, this.modelInstance, value); + const error = validateDataType(type, attributeName, this.modelInstance, value); if (error) { this.errors.push(error); } diff --git a/src/model-definition.ts b/src/model-definition.ts new file mode 100644 index 000000000000..d0718b309a6e --- /dev/null +++ b/src/model-definition.ts @@ -0,0 +1,893 @@ +import NodeUtil from 'node:util'; +import isPlainObject from 'lodash/isPlainObject'; +import omit from 'lodash/omit'; +import type { Association } from './associations/index.js'; +import * as DataTypes from './data-types.js'; +import { isDataTypeClass } from './dialects/abstract/data-types-utils.js'; +import { AbstractDataType } from './dialects/abstract/data-types.js'; +import type { IndexOptions, TableNameWithSchema } from './dialects/abstract/query-interface.js'; +import { BaseError } from './errors/index.js'; +import type { HookHandler } from './hooks.js'; +import type { ModelHooks } from './model-hooks.js'; +import { staticModelHooks } from './model-hooks.js'; +import { conformIndex } from './model-internals.js'; +import type { + BuiltModelOptions, + InitOptions, + AttributeOptions, + ModelAttributes, + ModelStatic, + NormalizedAttributeOptions, + NormalizedAttributeReferencesOptions, + ModelOptions, +} from './model.js'; +import type { Sequelize } from './sequelize.js'; +import { fieldToColumn } from './utils/deprecations.js'; +import { toDefaultValue } from './utils/dialect.js'; +import { MapView, SetView } from './utils/immutability.js'; +import { some } from './utils/iterators.js'; +import { isModelStatic } from './utils/model-utils.js'; +import { getAllOwnEntries, noPrototype, removeUndefined } from './utils/object.js'; +import { generateIndexName, pluralize, underscoredIf } from './utils/string.js'; + +export interface TimestampAttributes { + createdAt?: string; + updatedAt?: string; + deletedAt?: string; +} + +/** + * The goal of this class is to store the definition of a model. + * + * It is part of the Repository Design Pattern. + * See https://github.com/sequelize/sequelize/issues/15389 for more details. + * + * There is only one ModelDefinition instance per model per sequelize instance. + */ +export class ModelDefinition { + readonly #sequelize: Sequelize; + readonly options: BuiltModelOptions; + readonly #table: TableNameWithSchema; + get table(): TableNameWithSchema { + return this.#table; + } + + readonly associations: { [associationName: string]: Association } = Object.create(null); + + /** + * The list of attributes that have *not* been normalized. + * This list can be mutated. Call {@link refreshAttributes} to update the normalized attributes ({@link attributes)}. + */ + readonly rawAttributes: { [attributeName: string]: AttributeOptions }; + + readonly #attributes = new Map(); + + /** + * The list of attributes that have been normalized. + * + * This map is fully frozen and cannot be modified directly. + * Modify {@link rawAttributes} then call {@link refreshAttributes} instead. + */ + readonly attributes = new MapView(this.#attributes); + + readonly #physicalAttributes = new Map(); + + /** + * The list of attributes that actually exist in the database, as opposed to {@link virtualAttributeNames}. + */ + readonly physicalAttributes = new MapView(this.#physicalAttributes); + + readonly #columns = new Map(); + readonly columns = new MapView(this.#columns); + + readonly #primaryKeyAttributeNames = new Set(); + + readonly primaryKeysAttributeNames = new SetView(this.#primaryKeyAttributeNames); + + /** + * List of attributes that cannot be modified by the user + */ + readonly #readOnlyAttributeNames = new Set(); + + /** + * List of attributes that cannot be modified by the user (read-only) + */ + readonly readOnlyAttributeNames = new SetView(this.#readOnlyAttributeNames); + + /** + * Records which attributes are the different built-in timestamp attributes + */ + readonly timestampAttributeNames: TimestampAttributes = Object.create(null); + + /** + * The name of the attribute that records the version of the model instance. + */ + readonly #versionAttributeName: string | undefined; + + get versionAttributeName(): string | undefined { + return this.#versionAttributeName; + } + + readonly #jsonAttributeNames = new Set(); + readonly jsonAttributeNames = new SetView(this.#jsonAttributeNames); + + readonly #virtualAttributeNames = new Set(); + + /** + * The list of attributes that do not really exist in the database, as opposed to {@link physicalAttributeNames}. + */ + readonly virtualAttributeNames = new SetView(this.#virtualAttributeNames); + + readonly #attributesWithGetters = new Set(); + readonly attributesWithGetters = new SetView(this.#attributesWithGetters); + + readonly #attributesWithSetters = new Set(); + readonly attributesWithSetters = new SetView(this.#attributesWithSetters); + + /** + * @deprecated Code should not rely on this as users can create custom attributes. + */ + readonly #booleanAttributeNames = new Set(); + + /** + * @deprecated Code should not rely on this as users can create custom attributes. + */ + readonly booleanAttributeNames = new SetView(this.#booleanAttributeNames); + + /** + * @deprecated Code should not rely on this as users can create custom attributes. + */ + readonly #dateAttributeNames = new Set(); + + /** + * @deprecated Code should not rely on this as users can create custom attributes. + */ + readonly dateAttributeNames = new SetView(this.#dateAttributeNames); + + #autoIncrementAttributeName: string | null = null; + get autoIncrementAttributeName(): string | null { + return this.#autoIncrementAttributeName; + } + + readonly #defaultValues = new Map unknown>(); + readonly defaultValues = new MapView(this.#defaultValues); + + /** + * Final list of indexes, built by {@link refreshIndexes} + */ + #indexes: IndexOptions[] = []; + + /** + * @deprecated Temporary property to be able to use elements that have not migrated to ModelDefinition yet. + */ + readonly #model: ModelStatic; + + get modelName(): string { + return this.options.modelName; + } + + get underscored(): boolean { + return this.options.underscored; + } + + get sequelize(): Sequelize { + return this.#sequelize; + } + + get hooks(): HookHandler { + return staticModelHooks.getFor(this); + } + + constructor(attributesOptions: ModelAttributes, modelOptions: InitOptions, model: ModelStatic) { + if (!modelOptions.sequelize) { + throw new Error('new ModelDefinition() expects a Sequelize instance to be passed through the option bag, which is the second parameter.'); + } + + if (!modelOptions.modelName) { + throw new Error('new ModelDefinition() expects a modelName to be passed through the option bag, which is the second parameter.'); + } + + this.#sequelize = modelOptions.sequelize; + this.#model = model; + + const globalOptions = this.#sequelize.options; + + // caution: mergeModelOptions mutates its first input + this.options = mergeModelOptions( + Object.assign( + // default options + { + noPrimaryKey: false, + timestamps: true, + validate: {}, + freezeTableName: false, + underscored: false, + paranoid: false, + rejectOnEmpty: false, + schema: '', + schemaDelimiter: '', + defaultScope: {}, + scopes: {}, + name: {}, + indexes: [], + }, + globalOptions.define as ModelOptions, + ), + modelOptions, + true, + ) as BuiltModelOptions; + + // @ts-expect-error -- guide to help users migrate to alternatives, these were deprecated in v6 + if (this.options.getterMethods || this.options.setterMethods) { + throw new Error(`Error in the definition of Model ${this.modelName}: The "getterMethods" and "setterMethods" options have been removed. + +If you need to use getters & setters that behave like attributes, use VIRTUAL attributes. +If you need regular getters & setters, define your model as a class and add getter & setters. +See https://sequelize.org/docs/v6/core-concepts/getters-setters-virtuals/#deprecated-in-sequelize-v7-gettermethods-and-settermethods for more information.`); + } + + this.options.name.plural ??= pluralize(modelOptions.modelName); + // Model Names must be singular! + this.options.name.singular ??= modelOptions.modelName; + + this.#sequelize.hooks.runSync('beforeDefine', attributesOptions, this.options); + + delete modelOptions.modelName; + + // if you call "define" multiple times for the same modelName, do not clutter the factory + if (this.sequelize.isDefined(this.modelName)) { + this.sequelize.modelManager.removeModel(this.sequelize.modelManager.getModel(this.modelName)!); + } + + if (this.options.hooks) { + this.hooks.addListeners(this.options.hooks); + } + + if (!this.options.tableName) { + this.options.tableName = this.options.freezeTableName + ? this.modelName + : underscoredIf(pluralize(this.modelName), this.underscored); + } + + this.#table = Object.freeze(this.sequelize.queryInterface.queryGenerator.extractTableDetails(removeUndefined({ + tableName: this.options.tableName, + schema: this.options.schema, + delimiter: this.options.schemaDelimiter, + }))); + + // error check options + for (const [validatorName, validator] of getAllOwnEntries(this.options.validate)) { + if (typeof validator !== 'function') { + throw new TypeError(`Members of the validate option must be functions. Model: ${this.modelName}, error with validate member ${String(validatorName)}`); + } + } + + // attributes that will be added at the start of this.rawAttributes (id) + const rawAttributes: { [attributeName: string]: AttributeOptions } = Object.create(null); + + for (const [attributeName, rawAttributeOrDataType] of getAllOwnEntries(attributesOptions)) { + if (typeof attributeName === 'symbol') { + throw new TypeError('Symbol attributes are not supported'); + } + + let rawAttribute: AttributeOptions; + try { + rawAttribute = this.sequelize.normalizeAttribute(rawAttributeOrDataType); + } catch (error) { + throw new BaseError(`An error occurred for attribute ${attributeName} on model ${this.modelName}.`, { cause: error }); + } + + rawAttributes[attributeName] = rawAttribute; + + if (rawAttribute.field) { + fieldToColumn(); + } + } + + // setup names of timestamp attributes + if (this.options.timestamps) { + for (const key of ['createdAt', 'updatedAt', 'deletedAt'] as const) { + 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.timestampAttributeNames.createdAt = typeof this.options.createdAt === 'string' ? this.options.createdAt : 'createdAt'; + + this.#readOnlyAttributeNames.add(this.timestampAttributeNames.createdAt); + } + + if (this.options.updatedAt !== false) { + this.timestampAttributeNames.updatedAt = typeof this.options.updatedAt === 'string' ? this.options.updatedAt : 'updatedAt'; + this.#readOnlyAttributeNames.add(this.timestampAttributeNames.updatedAt); + } + + if (this.options.paranoid && this.options.deletedAt !== false) { + this.timestampAttributeNames.deletedAt = typeof this.options.deletedAt === 'string' ? this.options.deletedAt : 'deletedAt'; + + this.#readOnlyAttributeNames.add(this.timestampAttributeNames.deletedAt); + } + } + + // setup name for version attribute + if (this.options.version) { + this.#versionAttributeName = typeof this.options.version === 'string' ? this.options.version : 'version'; + this.#readOnlyAttributeNames.add(this.#versionAttributeName); + } + + this.rawAttributes = Object.create(null); + + // Add id if no primary key was manually added to definition + if (!this.options.noPrimaryKey && !some(Object.values(rawAttributes), attr => Boolean(attr.primaryKey))) { + if ('id' in rawAttributes && rawAttributes.id?.primaryKey === undefined) { + throw new Error(`An attribute called 'id' was defined in model '${this.options.tableName}' but primaryKey is not set. This is likely to be an error, which can be fixed by setting its 'primaryKey' option to true. If this is intended, explicitly set its 'primaryKey' option to false`); + } + + // add PK first for a clean attribute order + this.rawAttributes.id = { + type: DataTypes.INTEGER(), + allowNull: false, + primaryKey: true, + autoIncrement: true, + _autoGenerated: true, + }; + } + + // add all user defined attributes + + for (const [attributeName, rawAttribute] of Object.entries(rawAttributes)) { + this.rawAttributes[attributeName] = rawAttribute; + } + + // add timestamp & version last for a clean attribute order + + if (this.timestampAttributeNames.createdAt) { + this.#addTimestampAttribute(this.timestampAttributeNames.createdAt, false); + } + + if (this.timestampAttributeNames.updatedAt) { + this.#addTimestampAttribute(this.timestampAttributeNames.updatedAt, false); + } + + if (this.timestampAttributeNames.deletedAt) { + this.#addTimestampAttribute(this.timestampAttributeNames.deletedAt, true); + } + + if (this.#versionAttributeName) { + const existingAttribute: AttributeOptions | undefined = this.rawAttributes[this.#versionAttributeName]; + + if (existingAttribute?.type && !(existingAttribute.type instanceof DataTypes.INTEGER)) { + throw new Error(`Sequelize is trying to add the version attribute ${NodeUtil.inspect(this.#versionAttributeName)} to Model ${NodeUtil.inspect(this.modelName)}, +but an attribute with the same name already exists and declares a data type. +The "version" attribute is managed automatically by Sequelize, and its type must be DataTypes.INTEGER. Please either: +- remove the "type" property from your attribute definition, +- rename either your attribute or the version attribute, +- or disable the automatic timestamp attributes.`); + } + + if (existingAttribute?.allowNull === true) { + throw new Error(`Sequelize is trying to add the timestamp attribute ${NodeUtil.inspect(this.#versionAttributeName)} to Model ${NodeUtil.inspect(this.modelName)}, +but an attribute with the same name already exists and its allowNull option (${existingAttribute.allowNull}) conflicts with the one Sequelize is trying to set (false). +The "version" attribute is managed automatically by Sequelize, and its nullability is not configurable. Please either: +- remove the "allowNull" property from your attribute definition, +- rename either your attribute or the version attribute, +- or disable the automatic version attribute.`); + } + + this.rawAttributes[this.#versionAttributeName] = { + ...existingAttribute, + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + _autoGenerated: true, + }; + } + + this.refreshAttributes(); + } + + #addTimestampAttribute(attributeName: string, allowNull: boolean) { + const existingAttribute: AttributeOptions | undefined = this.rawAttributes[attributeName]; + + if (existingAttribute?.type && !(existingAttribute.type instanceof DataTypes.DATE)) { + throw new Error(`Sequelize is trying to add the timestamp attribute ${NodeUtil.inspect(attributeName)} to Model ${NodeUtil.inspect(this.modelName)}, +but an attribute with the same name already exists and declares a data type. +Timestamp attributes are managed automatically by Sequelize, and their data type must be DataTypes.DATE (https://github.com/sequelize/sequelize/issues/2572). Please either: +- remove the "type" property from your attribute definition, +- rename either your attribute or the timestamp attribute, +- or disable the automatic timestamp attributes.`); + } + + if (existingAttribute?.allowNull != null && existingAttribute?.allowNull !== allowNull) { + throw new Error(`Sequelize is trying to add the timestamp attribute ${NodeUtil.inspect(attributeName)} to Model ${NodeUtil.inspect(this.modelName)}, +but an attribute with the same name already exists and its allowNull option (${existingAttribute.allowNull}) conflicts with the one Sequelize is trying to set (${allowNull}). +Timestamp attributes are managed automatically by Sequelize, and their nullability is not configurable. Please either: +- remove the "allowNull" property from your attribute definition, +- rename either your attribute or the timestamp attribute, +- or disable the automatic timestamp attributes.`); + } + + this.rawAttributes[attributeName] = { + // @ts-expect-error -- this property is not mandatory in timestamp attributes + type: DataTypes.DATE(6), + ...this.rawAttributes[attributeName], + allowNull, + _autoGenerated: true, + }; + } + + /** + * Normalizes all attribute definitions, using {@link rawAttributes} as the source. + */ + refreshAttributes() { + this.hooks.runSync('beforeDefinitionRefresh'); + + this.#attributes.clear(); + this.#booleanAttributeNames.clear(); + this.#dateAttributeNames.clear(); + this.#jsonAttributeNames.clear(); + this.#virtualAttributeNames.clear(); + this.#physicalAttributes.clear(); + this.#defaultValues.clear(); + this.#columns.clear(); + this.#primaryKeyAttributeNames.clear(); + this.#autoIncrementAttributeName = null; + this.#attributesWithGetters.clear(); + this.#attributesWithSetters.clear(); + + // indexes defined through attributes + const attributeIndexes: IndexOptions[] = []; + + for (const [attributeName, rawAttribute] of Object.entries(this.rawAttributes)) { + if (typeof attributeName !== 'string') { + throw new TypeError(`Attribute names must be strings, but "${this.modelName}" declared a non-string attribute: ${NodeUtil.inspect(attributeName)}`); + } + + // Checks whether the name is ambiguous with isColString + // we check whether the attribute starts *or* ends because the following query: + // { '$json.key$' } + // could be interpreted as both + // "json"."key" (accessible attribute 'key' on model 'json') + // or + // "$json" #>> {key$} (accessing key 'key$' on attribute '$json') + if (attributeName.startsWith('$') || attributeName.endsWith('$')) { + throw new Error(`Name of attribute "${attributeName}" in model "${this.modelName}" cannot start or end with "$" as "$attribute$" is reserved syntax used to reference nested columns in queries.`); + } + + if (attributeName.includes('.')) { + throw new Error(`Name of attribute "${attributeName}" in model "${this.modelName}" cannot include the character "." as it would be ambiguous with the syntax used to reference nested columns, and nested json keys, in queries.`); + } + + if (attributeName.includes('::')) { + throw new Error(`Name of attribute "${attributeName}" in model "${this.modelName}" cannot include the character sequence "::" as it is reserved syntax used to cast attributes in queries.`); + } + + if (attributeName.includes('->')) { + throw new Error(`Name of attribute "${attributeName}" in model "${this.modelName}" cannot include the character sequence "->" as it is reserved syntax used in SQL generated by Sequelize to target nested associations.`); + } + + if (!isPlainObject(rawAttribute)) { + throw new Error(`Attribute "${this.modelName}.${attributeName}" must be specified as a plain object.`); + } + + if (!rawAttribute.type) { + throw new Error(`Attribute "${this.modelName}.${attributeName}" does not specify its DataType.`); + } + + try { + const columnName = rawAttribute.columnName ?? rawAttribute.field ?? underscoredIf(attributeName, this.underscored); + + const builtAttribute = noPrototype({ + ...omit(rawAttribute, ['unique', 'index']), + type: this.#sequelize.normalizeDataType(rawAttribute.type), + references: normalizeReference(rawAttribute.references), + + // fieldName is a legacy name, renamed to attributeName. + fieldName: attributeName, + attributeName, + + // field is a legacy name, renamed to columnName. + field: columnName, + columnName, + + // @ts-expect-error -- undocumented legacy property, to be removed. + Model: this.#model, + + // undocumented legacy property, to be removed. + _modelAttribute: true, + }); + + if (builtAttribute.type instanceof AbstractDataType) { + // @ts-expect-error -- defaultValue is not readOnly yet! + builtAttribute.type + = builtAttribute.type.clone().attachUsageContext({ + // TODO: Repository Pattern - replace with ModelDefinition + model: this.#model, + attributeName, + sequelize: this.sequelize, + }); + } + + if (Object.prototype.hasOwnProperty.call(builtAttribute, 'defaultValue')) { + if (isDataTypeClass(builtAttribute.defaultValue)) { + // @ts-expect-error -- defaultValue is not readOnly yet! + builtAttribute.defaultValue + = new builtAttribute.defaultValue(); + } + + this.#defaultValues.set(attributeName, () => toDefaultValue(builtAttribute.defaultValue, this.sequelize.dialect)); + } + + // TODO: remove "notNull" & "isNull" validators + if (rawAttribute.allowNull !== false && rawAttribute.validate?.notNull) { + throw new Error(`"notNull" validator is only allowed with "allowNull:false"`); + } + + if (builtAttribute.primaryKey === true) { + this.#primaryKeyAttributeNames.add(attributeName); + } + + if (builtAttribute.type instanceof DataTypes.BOOLEAN) { + this.#booleanAttributeNames.add(attributeName); + } else if (builtAttribute.type instanceof DataTypes.DATE || rawAttribute.type instanceof DataTypes.DATEONLY) { + this.#dateAttributeNames.add(attributeName); + } else if (builtAttribute.type instanceof DataTypes.JSON) { + this.#jsonAttributeNames.add(attributeName); + } + + if (Object.prototype.hasOwnProperty.call(rawAttribute, 'unique') && rawAttribute.unique) { + const uniqueIndexes = Array.isArray(rawAttribute.unique) ? rawAttribute.unique : [rawAttribute.unique]; + + for (const uniqueIndex of uniqueIndexes) { + if (uniqueIndex === true || typeof uniqueIndex === 'string') { + attributeIndexes.push({ + unique: true, + fields: [builtAttribute.columnName], + ...(typeof uniqueIndex === 'string' ? { name: uniqueIndex } : undefined), + }); + } else { + attributeIndexes.push({ + ...uniqueIndex, + unique: true, + fields: [builtAttribute.columnName], + }); + } + } + } + + if (Object.prototype.hasOwnProperty.call(rawAttribute, 'index') && rawAttribute.index) { + const indexes = Array.isArray(rawAttribute.index) ? rawAttribute.index : [rawAttribute.index]; + + for (const index of indexes) { + const jsonbIndexDefaults = rawAttribute.type instanceof DataTypes.JSONB ? { using: 'gin' } : undefined; + + if (index === true || typeof index === 'string') { + attributeIndexes.push({ + fields: [builtAttribute.columnName], + ...(typeof index === 'string' ? { name: index } : undefined), + ...jsonbIndexDefaults, + }); + } else { + // @ts-expect-error -- forbidden property + if (index.fields) { + throw new Error('"fields" cannot be specified for indexes defined on attributes. Use the "indexes" option on the table definition instead.'); + } + + attributeIndexes.push({ + ...jsonbIndexDefaults, + ...index, + fields: [builtAttribute.columnName], + }); + } + } + } + + if (builtAttribute.autoIncrement) { + if (this.#autoIncrementAttributeName) { + throw new Error(`Only one autoIncrement attribute is allowed per model, but both ${NodeUtil.inspect(attributeName)} and ${NodeUtil.inspect(this.#autoIncrementAttributeName)} are marked as autoIncrement.`); + } + + this.#autoIncrementAttributeName = attributeName; + } + + Object.freeze(builtAttribute); + + this.#attributes.set(attributeName, builtAttribute); + this.#columns.set(builtAttribute.columnName, builtAttribute); + + if (builtAttribute.type instanceof DataTypes.VIRTUAL) { + this.#virtualAttributeNames.add(attributeName); + } else { + this.#physicalAttributes.set(attributeName, builtAttribute); + } + + if (builtAttribute.get) { + this.#attributesWithGetters.add(attributeName); + } + + if (builtAttribute.set) { + this.#attributesWithSetters.add(attributeName); + } + } catch (error) { + throw new BaseError(`An error occurred while normalizing attribute ${JSON.stringify(attributeName)} in model ${JSON.stringify(this.modelName)}.`, { cause: error }); + } + } + + this.#refreshIndexes(attributeIndexes); + + this.hooks.runSync('afterDefinitionRefresh'); + } + + #refreshIndexes(attributeIndexes: IndexOptions[]): void { + this.#indexes = []; + + for (const index of this.options.indexes) { + this.#addIndex(index); + } + + for (const index of attributeIndexes) { + this.#addIndex(index); + } + } + + #addIndex(index: IndexOptions): void { + index = this.#nameIndex(conformIndex(index)); + + if (typeof index.fields?.[0] === 'string') { + const column = this.columns.get(index.fields[0])?.attributeName; + + if (column) { + // @ts-expect-error -- TODO: remove this 'column'. It does not work with composite indexes, and is only used by db2. On top of that, it's named "column" but is actually an attribute name. + index.column = column; + } + } + + const existingIndex = this.#indexes.find(i => i.name === index.name); + if (existingIndex == null) { + this.#indexes.push(index); + + return; + } + + for (const key of Object.keys(index) as Array) { + if (index[key] === undefined) { + continue; + } + + // @ts-expect-error -- TODO: remove this 'column'. It does not work with composite indexes, and is only used by db2 which should use fields instead. + if (key === 'column') { + continue; + } + + // TODO: rename "fields" to columnNames + if (key === 'fields') { + if (existingIndex.fields == null) { + existingIndex.fields = index.fields!; + } else { + existingIndex.fields = [...existingIndex.fields, ...index.fields!]; + } + + continue; + } + + if (existingIndex[key] === undefined) { + // @ts-expect-error -- same type + existingIndex[key] = index[key]; + } + + if (existingIndex[key] !== index[key]) { + throw new Error(`Index "${index.name}" has conflicting options: "${key}" was defined with different values ${NodeUtil.inspect(existingIndex[key])} and ${NodeUtil.inspect(index[key])}.`); + } + } + } + + #nameIndex(newIndex: IndexOptions): IndexOptions { + if (Object.prototype.hasOwnProperty.call(newIndex, 'name')) { + return newIndex; + } + + const newName = generateIndexName(this.table, newIndex); + + // TODO: check for collisions on *all* models, not just this one, as index names are global. + for (const index of this.getIndexes()) { + if (index.name === newName) { + throw new Error(`Sequelize tried to give the name "${newName}" to index: +${NodeUtil.inspect(newIndex)} +on model "${this.#model.name}", but that name is already taken by index: +${NodeUtil.inspect(index)} + +Specify a different name for either index to resolve this issue.`); + } + } + + newIndex.name = newName; + + return newIndex; + } + + getIndexes(): readonly IndexOptions[] { + return this.#indexes; + } + + /** + * Returns the column name corresponding to the given attribute name. + * + * @param attributeName + */ + getColumnName(attributeName: string): string { + const attribute = this.#attributes.get(attributeName); + + if (attribute == null) { + throw new Error(`Attribute "${attributeName}" does not exist on model "${this.modelName}".`); + } + + return attribute.columnName; + } + + /** + * Returns the column name corresponding to the given attribute name if it exists, otherwise returns the attribute name. + * + * ⚠️ Using this method is highly discouraged. Users should specify column names & attribute names separately, to prevent any ambiguity. + * + * @param attributeName + */ + getColumnNameLoose(attributeName: string): string { + const attribute = this.#attributes.get(attributeName); + + return attribute?.columnName ?? attributeName; + } +} + +const modelDefinitions = new WeakMap(); + +export function registerModelDefinition(model: ModelStatic, modelDefinition: ModelDefinition): void { + modelDefinitions.set(model, modelDefinition); +} + +export function hasModelDefinition(model: ModelStatic): boolean { + return modelDefinitions.has(model); +} + +export function getModelDefinition(model: ModelStatic): ModelDefinition { + const definition = modelDefinitions.get(model); + if (!definition) { + throw new Error(`Model ${model.name} has not been initialized yet.`); + } + + return definition; +} + +export function normalizeReference(references: AttributeOptions['references']): NormalizedAttributeReferencesOptions | undefined { + if (!references) { + return undefined; + } + + if (typeof references === 'string') { + return Object.freeze({ + table: references, + get model() { + throw new Error('references.model has been renamed to references.tableName in normalized references options.'); + }, + }); + } + + if (isModelStatic(references)) { + return Object.freeze({ + table: references.table, + get model() { + throw new Error('references.model has been renamed to references.tableName in normalized references options.'); + }, + }); + } + + const { model, table, ...referencePassDown } = references; + + if (model && table) { + throw new Error('"references" cannot contain both "model" and "tableName"'); + } + + // It's possible that the model has not been defined yet but the user configured other fields, in cases where + // the reference is added by an association initializing itself. + // If that happens, we won't add the reference until the association is initialized and this method gets called again. + if (!model && !table) { + return undefined; + } + + if (model || table) { + return Object.freeze({ + + table: model ? model.table : table!, + ...referencePassDown, + get model() { + throw new Error('references.model has been renamed to references.tableName in normalized references options.'); + }, + }); + } +} + +/** + * This method mutates the first parameter. + * + * @param existingModelOptions + * @param options + * @param overrideOnConflict + */ +export function mergeModelOptions( + existingModelOptions: ModelOptions, + options: ModelOptions, + overrideOnConflict: boolean, +): ModelOptions { + // merge-able: scopes, indexes + for (const [optionName, optionValue] of Object.entries(options)) { + if (!(optionName in existingModelOptions)) { + // @ts-expect-error -- runtime type checking is enforced by model + existingModelOptions[optionName] = optionValue; + continue; + } + + // These are objects. We merge their properties, unless the same key is used in both values. + if (optionName === 'scopes' || optionName === 'validate') { + for (const [subOptionName, subOptionValue] of getAllOwnEntries(optionValue)) { + // @ts-expect-error -- dynamic type, not worth typing + if (existingModelOptions[optionName][subOptionName] === subOptionValue) { + continue; + } + + if (!overrideOnConflict && subOptionName in existingModelOptions[optionName]!) { + throw new Error(`Trying to set the option ${optionName}[${JSON.stringify(subOptionName)}], but a value already exists.`); + } + + // @ts-expect-error -- runtime type checking is enforced by model + existingModelOptions[optionName][subOptionName] = subOptionValue; + } + + continue; + } + + if (optionName === 'hooks') { + const existingHooks = existingModelOptions.hooks!; + for (const hookType of Object.keys(optionValue) as Array) { + if (!existingHooks[hookType]) { + // @ts-expect-error -- type is too complex for typescript + existingHooks[hookType] = optionValue[hookType]; + continue; + } + + const existingHooksOfType = Array.isArray(existingHooks[hookType]) + ? existingHooks[hookType] + : [existingHooks[hookType]]; + + if (!Array.isArray(optionValue[hookType])) { + // @ts-expect-error -- typescript doesn't like this merge algorithm. + existingHooks[hookType] = [...existingHooksOfType, optionValue[hookType]]; + } else { + existingHooks[hookType] = [...existingHooksOfType, ...optionValue[hookType]]; + } + } + + continue; + } + + // This is an array. Simple array merge. + if (optionName === 'indexes') { + existingModelOptions.indexes = [...existingModelOptions.indexes!, ...optionValue]; + + continue; + } + + // @ts-expect-error -- dynamic type, not worth typing + if (!overrideOnConflict && optionValue !== existingModelOptions[optionName]) { + throw new Error(`Trying to set the option ${optionName}, but a value already exists.`); + } + + // @ts-expect-error -- dynamic type, not worth typing + existingModelOptions[optionName] = optionValue; + } + + return existingModelOptions; +} diff --git a/src/model-hooks.ts b/src/model-hooks.ts new file mode 100644 index 000000000000..383d3b38e10c --- /dev/null +++ b/src/model-hooks.ts @@ -0,0 +1,135 @@ +import type { AfterAssociateEventData, AssociationOptions, BeforeAssociateEventData } from './associations/index.js'; +import type { AsyncHookReturn } from './hooks.js'; +import { HookHandlerBuilder } from './hooks.js'; +import type { ValidationOptions } from './instance-validator.js'; +import type { + BulkCreateOptions, CountOptions, + CreateOptions, DestroyOptions, FindOptions, + InstanceDestroyOptions, + InstanceRestoreOptions, + InstanceUpdateOptions, + Model, ModelStatic, RestoreOptions, UpdateOptions, + UpsertOptions, +} from './model.js'; +import type { SyncOptions } from './sequelize.js'; + +export interface ModelHooks { + beforeValidate(instance: M, options: ValidationOptions): AsyncHookReturn; + afterValidate(instance: M, options: ValidationOptions): AsyncHookReturn; + validationFailed(instance: M, options: ValidationOptions, error: unknown): AsyncHookReturn; + beforeCreate(attributes: M, options: CreateOptions): AsyncHookReturn; + afterCreate(attributes: M, options: CreateOptions): AsyncHookReturn; + beforeDestroy(instance: M, options: InstanceDestroyOptions): AsyncHookReturn; + afterDestroy(instance: M, options: InstanceDestroyOptions): AsyncHookReturn; + beforeRestore(instance: M, options: InstanceRestoreOptions): AsyncHookReturn; + afterRestore(instance: M, options: InstanceRestoreOptions): AsyncHookReturn; + beforeUpdate(instance: M, options: InstanceUpdateOptions): AsyncHookReturn; + afterUpdate(instance: M, options: InstanceUpdateOptions): AsyncHookReturn; + beforeUpsert(attributes: M, options: UpsertOptions): AsyncHookReturn; + afterUpsert(attributes: [ M, boolean | null ], options: UpsertOptions): AsyncHookReturn; + beforeSave( + instance: M, + options: InstanceUpdateOptions | CreateOptions + ): AsyncHookReturn; + afterSave( + instance: M, + options: InstanceUpdateOptions | CreateOptions + ): AsyncHookReturn; + beforeBulkCreate(instances: M[], options: BulkCreateOptions): AsyncHookReturn; + afterBulkCreate(instances: readonly M[], options: BulkCreateOptions): AsyncHookReturn; + beforeBulkDestroy(options: DestroyOptions): AsyncHookReturn; + afterBulkDestroy(options: DestroyOptions): AsyncHookReturn; + beforeBulkRestore(options: RestoreOptions): AsyncHookReturn; + afterBulkRestore(options: RestoreOptions): AsyncHookReturn; + beforeBulkUpdate(options: UpdateOptions): AsyncHookReturn; + afterBulkUpdate(options: UpdateOptions): AsyncHookReturn; + + /** + * A hook that is run at the start of {@link Model.count} + */ + beforeCount(options: CountOptions): AsyncHookReturn; + + /** + * A hook that is run before a find (select) query + */ + beforeFind(options: FindOptions): AsyncHookReturn; + + /** + * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded + * + * @deprecated use `beforeFind` instead + */ + beforeFindAfterExpandIncludeAll(options: FindOptions): AsyncHookReturn; + + /** + * A hook that is run before a find (select) query, after all option have been normalized + * + * @deprecated use `beforeFind` instead + */ + beforeFindAfterOptions(options: FindOptions): AsyncHookReturn; + /** + * A hook that is run after a find (select) query + */ + afterFind(instancesOrInstance: readonly M[] | M | null, options: FindOptions): AsyncHookReturn; + + /** + * A hook that is run at the start of {@link Model#sync} + */ + beforeSync(options: SyncOptions): AsyncHookReturn; + + /** + * A hook that is run at the end of {@link Model#sync} + */ + afterSync(options: SyncOptions): AsyncHookReturn; + beforeAssociate(data: BeforeAssociateEventData, options: AssociationOptions): AsyncHookReturn; + afterAssociate(data: AfterAssociateEventData, options: AssociationOptions): AsyncHookReturn; + + /** + * Runs before the definition of the model changes because {@link ModelDefinition#refreshAttributes} was called. + */ + beforeDefinitionRefresh(): void; + + /** + * Runs after the definition of the model has changed because {@link ModelDefinition#refreshAttributes} was called. + */ + afterDefinitionRefresh(): void; +} + +export const validModelHooks: Array = [ + 'beforeValidate', 'afterValidate', 'validationFailed', + 'beforeCreate', 'afterCreate', + 'beforeDestroy', 'afterDestroy', + 'beforeRestore', 'afterRestore', + 'beforeUpdate', 'afterUpdate', + 'beforeUpsert', 'afterUpsert', + 'beforeSave', 'afterSave', + 'beforeBulkCreate', 'afterBulkCreate', + 'beforeBulkDestroy', 'afterBulkDestroy', + 'beforeBulkRestore', 'afterBulkRestore', + 'beforeBulkUpdate', 'afterBulkUpdate', + 'beforeCount', + 'beforeFind', 'beforeFindAfterExpandIncludeAll', 'beforeFindAfterOptions', 'afterFind', + 'beforeSync', 'afterSync', + 'beforeAssociate', 'afterAssociate', + 'beforeDefinitionRefresh', 'afterDefinitionRefresh', +]; + +export const staticModelHooks = new HookHandlerBuilder(validModelHooks, async ( + eventTarget, + isAsync, + hookName: keyof ModelHooks, + args, +) => { + // This forwards hooks run on Models to the Sequelize instance's hooks. + const model = eventTarget as ModelStatic; + + if (!model.sequelize) { + throw new Error('Model must be initialized before running hooks on it.'); + } + + if (isAsync) { + await model.sequelize.hooks.runAsync(hookName, ...args); + } else { + model.sequelize.hooks.runSync(hookName, ...args); + } +}); diff --git a/src/model-internals.ts b/src/model-internals.ts index d8a54333a5ad..bc960f8421c8 100644 --- a/src/model-internals.ts +++ b/src/model-internals.ts @@ -1,4 +1,5 @@ import NodeUtil from 'node:util'; +import type { IndexOptions } from './dialects/abstract/query-interface.js'; import { EagerLoadingError } from './errors'; import type { Transactionable } from './model'; import type { Sequelize } from './sequelize'; @@ -152,3 +153,18 @@ export function setTransactionFromAls(options: Transactionable, sequelize: Seque options.transaction = sequelize.getCurrentAlsTransaction(); } } + +export function conformIndex(index: IndexOptions): IndexOptions { + if (!index.fields) { + throw new Error('Missing "fields" property for index definition'); + } + + index = { ...index }; + + if (index.type && index.type.toLowerCase() === 'unique') { + index.unique = true; + delete index.type; + } + + return index; +} diff --git a/src/model-manager.d.ts b/src/model-manager.d.ts index d10940d64806..a3cad554ddf4 100644 --- a/src/model-manager.d.ts +++ b/src/model-manager.d.ts @@ -1,4 +1,4 @@ -import type { Model, ModelStatic } from './model'; +import type { ModelStatic } from './model'; import type { Sequelize } from './sequelize'; export class ModelManager { @@ -9,7 +9,8 @@ export class ModelManager { constructor(sequelize: Sequelize); addModel(model: T): T; removeModel(model: ModelStatic): void; - getModel(against: unknown, options?: { attribute?: string }): typeof Model; + getModel(against: unknown, options?: { attribute?: string }): ModelStatic | undefined; + hasModel(model: ModelStatic): boolean; /** * Returns an array that lists every model, sorted in order diff --git a/src/model-manager.js b/src/model-manager.js index 1622b0385d41..af19332b371d 100644 --- a/src/model-manager.js +++ b/src/model-manager.js @@ -30,6 +30,10 @@ export class ModelManager { return this.models.find(model => model[options.attribute] === against); } + hasModel(targetModel) { + return this.models.includes(targetModel); + } + get all() { return this.models; } @@ -45,30 +49,24 @@ export class ModelManager { const models = new Map(); const sorter = new Toposort(); + const queryGenerator = this.sequelize.queryInterface.queryGenerator; + for (const model of this.models) { let deps = []; - let tableName = model.getTableName(); - - if (_.isObject(tableName)) { - tableName = `${tableName.schema}.${tableName.tableName}`; - } + const tableName = queryGenerator.quoteTable(model); models.set(tableName, model); - for (const attrName in model.rawAttributes) { - if (Object.prototype.hasOwnProperty.call(model.rawAttributes, attrName)) { - const attribute = model.rawAttributes[attrName]; - - if (attribute.references) { - let dep = attribute.references.model; + const { attributes } = model.modelDefinition; + for (const attrName of attributes.keys()) { + const attribute = attributes.get(attrName); - if (_.isObject(dep)) { - dep = `${dep.schema}.${dep.tableName}`; - } - - deps.push(dep); - } + if (!attribute.references) { + continue; } + + const dep = queryGenerator.quoteTable(attribute.references.table); + deps.push(dep); } deps = deps.filter(dep => tableName !== dep); diff --git a/src/model-repository.ts b/src/model-repository.ts new file mode 100644 index 000000000000..9e6b11d1f184 --- /dev/null +++ b/src/model-repository.ts @@ -0,0 +1,38 @@ +import type { ModelDefinition } from './model-definition.js'; +import type { ModelTypeScript } from './model-typescript.js'; +import type { Sequelize } from './sequelize.js'; + +/** + * The goal of this class is to become the new home of all the static methods that are currently present on the Model class, + * as a way to enable a true Repository Mode for Sequelize. + * + * Currently this class is not usable as a repository (due to having a dependency on ModelStatic), but as we migrate all of + * Model to this class, we will be able to remove the dependency on ModelStatic, and make this class usable as a repository. + * + * See https://github.com/sequelize/sequelize/issues/15389 for more details. + * + * Unlike {@link ModelDefinition}, it's possible to have multiple different repositories for the same model (as users can provide their own implementation). + */ +class ModelRepository { + readonly #modelDefinition: ModelDefinition; + readonly #sequelize: Sequelize; + + constructor(modelDefinition: ModelDefinition, sequelize: Sequelize) { + this.#modelDefinition = modelDefinition; + this.#sequelize = sequelize; + } +} + +const modelRepositories = new WeakMap(); + +export function getModelRepository(model: typeof ModelTypeScript): ModelRepository { + let internals = modelRepositories.get(model); + if (internals) { + return internals; + } + + // @ts-expect-error -- temporary ts-ignore until we can drop ModelTypeScript + internals = new ModelRepository(model); + + return internals; +} diff --git a/src/model-typescript.ts b/src/model-typescript.ts index 93b4bf4a34c8..ab369814804a 100644 --- a/src/model-typescript.ts +++ b/src/model-typescript.ts @@ -1,3 +1,4 @@ +import { isDecoratedModel } from './decorators/shared/model.js'; import { legacyBuildAddAnyHook, legacyBuildAddHook, @@ -5,204 +6,184 @@ import { legacyBuildRemoveHook, legacyBuildRunHook, } from './hooks-legacy.js'; -import type { AsyncHookReturn } from './hooks.js'; -import { HookHandlerBuilder } from './hooks.js'; -import type { ValidationOptions } from './instance-validator.js'; +import { getModelDefinition, hasModelDefinition, ModelDefinition, registerModelDefinition } from './model-definition.js'; +import { staticModelHooks } from './model-hooks.js'; +import type { Model } from './model.js'; +import { noModelTableName } from './utils/deprecations.js'; +import { getObjectFromMap } from './utils/object.js'; import type { - AfterAssociateEventData, - AssociationOptions, - BeforeAssociateEventData, - BulkCreateOptions, - CountOptions, - CreateOptions, - DestroyOptions, - FindOptions, - InstanceDestroyOptions, - InstanceRestoreOptions, - InstanceUpdateOptions, - Model, ModelStatic, - RestoreOptions, - SyncOptions, - UpdateOptions, - UpsertOptions, + ModelStatic, Sequelize, AbstractQueryGenerator, AbstractQueryInterface, + IndexOptions, InitOptions, + Attributes, BrandedKeysOf, ForeignKeyBrand, ModelAttributes, Optional, + NormalizedAttributeOptions, + BuiltModelOptions, AttributeOptions, + Association, TableNameWithSchema, } from '.'; -export interface ModelHooks { - beforeValidate(instance: M, options: ValidationOptions): AsyncHookReturn; - afterValidate(instance: M, options: ValidationOptions): AsyncHookReturn; - validationFailed(instance: M, options: ValidationOptions, error: unknown): AsyncHookReturn; - beforeCreate(attributes: M, options: CreateOptions): AsyncHookReturn; - afterCreate(attributes: M, options: CreateOptions): AsyncHookReturn; - beforeDestroy(instance: M, options: InstanceDestroyOptions): AsyncHookReturn; - afterDestroy(instance: M, options: InstanceDestroyOptions): AsyncHookReturn; - beforeRestore(instance: M, options: InstanceRestoreOptions): AsyncHookReturn; - afterRestore(instance: M, options: InstanceRestoreOptions): AsyncHookReturn; - beforeUpdate(instance: M, options: InstanceUpdateOptions): AsyncHookReturn; - afterUpdate(instance: M, options: InstanceUpdateOptions): AsyncHookReturn; - beforeUpsert(attributes: M, options: UpsertOptions): AsyncHookReturn; - afterUpsert(attributes: [ M, boolean | null ], options: UpsertOptions): AsyncHookReturn; - beforeSave( - instance: M, - options: InstanceUpdateOptions | CreateOptions - ): AsyncHookReturn; - afterSave( - instance: M, - options: InstanceUpdateOptions | CreateOptions - ): AsyncHookReturn; - beforeBulkCreate(instances: M[], options: BulkCreateOptions): AsyncHookReturn; - afterBulkCreate(instances: readonly M[], options: BulkCreateOptions): AsyncHookReturn; - beforeBulkDestroy(options: DestroyOptions): AsyncHookReturn; - afterBulkDestroy(options: DestroyOptions): AsyncHookReturn; - beforeBulkRestore(options: RestoreOptions): AsyncHookReturn; - afterBulkRestore(options: RestoreOptions): AsyncHookReturn; - beforeBulkUpdate(options: UpdateOptions): AsyncHookReturn; - afterBulkUpdate(options: UpdateOptions): AsyncHookReturn; +// DO NOT EXPORT THIS CLASS! +// This is a temporary class to progressively migrate the Sequelize class to TypeScript by slowly moving its functions here. +export class ModelTypeScript { + static get queryInterface(): AbstractQueryInterface { + return this.sequelize.queryInterface; + } + + static get queryGenerator(): AbstractQueryGenerator { + return this.queryInterface.queryGenerator; + } /** - * A hook that is run at the start of {@link Model.count} + * A reference to the sequelize instance. */ - beforeCount(options: CountOptions): AsyncHookReturn; + get sequelize(): Sequelize { + return (this.constructor as typeof ModelTypeScript).sequelize; + } /** - * A hook that is run before a find (select) query + * A reference to the sequelize instance. + * + * Accessing this property throws if the model has not been registered with a Sequelize instance yet. */ - beforeFind(options: FindOptions): AsyncHookReturn; + static get sequelize(): Sequelize { + return this.modelDefinition.sequelize; + } /** - * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded - * - * @deprecated use `beforeFind` instead + * Returns the model definition of this model. + * The model definition contains all metadata about this model. */ - beforeFindAfterExpandIncludeAll(options: FindOptions): AsyncHookReturn; + static get modelDefinition(): ModelDefinition { + // @ts-expect-error -- getModelDefinition expects ModelStatic + return getModelDefinition(this); + } /** - * A hook that is run before a find (select) query, after all option have been normalized - * - * @deprecated use `beforeFind` instead + * An object hash from alias to association object */ - beforeFindAfterOptions(options: FindOptions): AsyncHookReturn; + static get associations(): { [associationName: string]: Association } { + return this.modelDefinition.associations; + } + /** - * A hook that is run after a find (select) query + * The name of the primary key attribute (on the JS side). + * + * @deprecated This property doesn't work for composed primary keys. Use {@link primaryKeyAttributes} instead. */ - afterFind(instancesOrInstance: readonly M[] | M | null, options: FindOptions): AsyncHookReturn; + static get primaryKeyAttribute(): string | null { + return this.primaryKeyAttributes[0] ?? null; + } /** - * A hook that is run at the start of {@link Model#sync} + * The name of the primary key attributes (on the JS side). + * + * @deprecated use {@link modelDefinition}. */ - beforeSync(options: SyncOptions): AsyncHookReturn; + static get primaryKeyAttributes(): string[] { + return [...this.modelDefinition.primaryKeysAttributeNames]; + } /** - * A hook that is run at the end of {@link Model#sync} + * The column name of the primary key. + * + * @deprecated don't use this. It doesn't work with composite PKs. It may be removed in the future to reduce duplication. + * Use the. Use {@link Model.primaryKeys} instead. */ - afterSync(options: SyncOptions): AsyncHookReturn; - beforeAssociate(data: BeforeAssociateEventData, options: AssociationOptions): AsyncHookReturn; - afterAssociate(data: AfterAssociateEventData, options: AssociationOptions): AsyncHookReturn; -} + static get primaryKeyField(): string | null { + const primaryKeyAttribute = this.primaryKeyAttribute; + if (!primaryKeyAttribute) { + return null; + } -export const validModelHooks: Array = [ - 'beforeValidate', 'afterValidate', 'validationFailed', - 'beforeCreate', 'afterCreate', - 'beforeDestroy', 'afterDestroy', - 'beforeRestore', 'afterRestore', - 'beforeUpdate', 'afterUpdate', - 'beforeUpsert', 'afterUpsert', - 'beforeSave', 'afterSave', - 'beforeBulkCreate', 'afterBulkCreate', - 'beforeBulkDestroy', 'afterBulkDestroy', - 'beforeBulkRestore', 'afterBulkRestore', - 'beforeBulkUpdate', 'afterBulkUpdate', - 'beforeCount', - 'beforeFind', 'beforeFindAfterExpandIncludeAll', 'beforeFindAfterOptions', 'afterFind', - 'beforeSync', 'afterSync', - 'beforeAssociate', 'afterAssociate', -]; - -const staticModelHooks = new HookHandlerBuilder(validModelHooks, async ( - eventTarget, - isAsync, - hookName, - args, -) => { - // This forwards hooks run on Models to the Sequelize instance's hooks. - const model = eventTarget as ModelStatic; - - if (!model.sequelize) { - throw new Error('Model must be initialized before running hooks on it.'); + return this.modelDefinition.getColumnName(primaryKeyAttribute); } - if (isAsync) { - await model.sequelize.hooks.runAsync(hookName, ...args); - } else { - model.sequelize.hooks.runSync(hookName, ...args); - } -}); + /** + * Like {@link Model.rawAttributes}, but only includes attributes that are part of the Primary Key. + */ + static get primaryKeys(): { [attribute: string]: NormalizedAttributeOptions } { + const out = Object.create(null); -const staticPrivateStates = new WeakMap(); + const definition = this.modelDefinition; -// DO NOT EXPORT THIS CLASS! -// This is a temporary class to progressively migrate the Sequelize class to TypeScript by slowly moving its functions here. -export class ModelTypeScript { - static get queryInterface(): AbstractQueryInterface { - return this.sequelize.queryInterface; - } + for (const primaryKey of definition.primaryKeysAttributeNames) { + out[primaryKey] = definition.attributes.get(primaryKey)!; + } - static get queryGenerator(): AbstractQueryGenerator { - return this.queryInterface.queryGenerator; + return out; } /** - * A reference to the sequelize instance. + * The options that the model was initialized with */ - get sequelize(): Sequelize { - return (this.constructor as typeof ModelTypeScript).sequelize; + static get options(): BuiltModelOptions { + return this.modelDefinition.options; } /** - * A reference to the sequelize instance. + * The name of the database table * - * Accessing this property throws if the model has not been registered with a Sequelize instance yet. + * @deprecated use {@link modelDefinition} or {@link table}. */ - static get sequelize(): Sequelize { - const sequelize = staticPrivateStates.get(this)?.sequelize; + static get tableName(): string { + noModelTableName(); - if (sequelize == null) { - throw new Error(`Model "${this.name}" has not been initialized yet. You can check whether a model has been initialized by calling its isInitialized method.`); - } + return this.modelDefinition.table.tableName; + } - return sequelize; + static get table(): TableNameWithSchema { + return this.modelDefinition.table; } - static assertIsInitialized(): void { - const sequelize = staticPrivateStates.get(this)?.sequelize; + /** + * @deprecated use {@link modelDefinition}'s {@link ModelDefinition#rawAttributes} or {@link ModelDefinition#attributes} instead. + */ + static get rawAttributes(): { [attribute: string]: AttributeOptions } { + throw new Error(`${this.name}.rawAttributes has been removed, as it has been split in two: +- If you only need to read the final attributes, use ${this.name}.modelDefinition.attributes +- If you need to modify the attributes, mutate ${this.name}.modelDefinition.rawAttributes, then call ${this.name}.modelDefinition.refreshAttributes()`); + } - if (sequelize == null) { - throw new Error(`Model "${this.name}" has not been initialized yet. You can check whether a model has been initialized by calling its isInitialized method.`); - } + /** + * @deprecated use {@link modelDefinition}'s {@link ModelDefinition#rawAttributes} or {@link ModelDefinition#attributes} instead. + */ + get rawAttributes(): { [attribute: string]: AttributeOptions } { + return (this.constructor as typeof ModelTypeScript).rawAttributes; } - static isInitialized(): boolean { - const sequelize = staticPrivateStates.get(this)?.sequelize; + /** + * @deprecated use {@link modelDefinition}'s {@link ModelDefinition#columns}. + */ + static get fieldRawAttributesMap(): { [columnName: string]: NormalizedAttributeOptions } { + return getObjectFromMap(this.modelDefinition.columns); + } - return sequelize != null; + /** + * @deprecated use {@link modelDefinition}'s {@link ModelDefinition#physicalAttributes}. + */ + static get tableAttributes(): { [attribute: string]: NormalizedAttributeOptions } { + return getObjectFromMap(this.modelDefinition.physicalAttributes); } - // TODO: make this hard-private once Model.init has been moved here - private static _setSequelize(sequelize: Sequelize) { - const privateState = staticPrivateStates.get(this) ?? {}; + /** + * A mapping of column name to attribute name + * + * @internal + */ + static get fieldAttributeMap(): { [columnName: string]: string } { + const out = Object.create(null); - if (privateState.sequelize != null && privateState.sequelize !== sequelize) { - throw new Error(`Model "${this.name}" already belongs to a different Sequelize instance.`); + const attributes = this.modelDefinition.attributes; + for (const attribute of attributes.values()) { + out[attribute.columnName] = attribute.attributeName; } - privateState.sequelize = sequelize; - staticPrivateStates.set(this, privateState); + return out; } static get hooks() { - return staticModelHooks.getFor(this); + return this.modelDefinition.hooks; } static addHook = legacyBuildAddAnyHook(staticModelHooks); @@ -257,4 +238,211 @@ export class ModelTypeScript { static beforeAssociate = legacyBuildAddHook(staticModelHooks, 'beforeAssociate'); static afterAssociate = legacyBuildAddHook(staticModelHooks, 'afterAssociate'); + + /** + * 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 + * ```javascript + * 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 https://sequelize.org/docs/v7/core-concepts/model-basics/ + * @see https://sequelize.org/docs/v7/core-concepts/validations-and-constraints/ + * + * @param 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. + * @param options These options are merged with the default define options provided to the Sequelize constructor + */ + static init>( + this: MS, + attributes: ModelAttributes< + M, + // 'foreign keys' are optional in Model.init as they are added by association declaration methods + Optional, BrandedKeysOf, typeof ForeignKeyBrand>> + >, + options: InitOptions, + ): MS { + if (isDecoratedModel(this)) { + throw new Error(`Model.init cannot be used if the model uses one of Sequelize's decorators. You must pass your model to the Sequelize constructor using the "models" option instead.`); + } + + if (!options.sequelize) { + throw new Error('Model.init expects a Sequelize instance to be passed through the option bag, which is the second parameter.'); + } + + initModel(this, attributes, options); + + return this; + } + + static getIndexes(): readonly IndexOptions[] { + return this.modelDefinition.getIndexes(); + } + + /** + * Unique indexes that can be declared as part of a CREATE TABLE query. + * + * @deprecated prefer using {@link getIndexes}, this will eventually be removed. + */ + static get uniqueKeys() { + const indexes = this.getIndexes(); + const uniqueKeys = Object.create(null); + + // TODO: "column" should be removed from index definitions + const supportedOptions = ['unique', 'fields', 'column', 'name']; + + for (const index of indexes) { + if (!index.unique) { + continue; + } + + if (!index.name) { + continue; + } + + if (!index.fields) { + continue; + } + + if (!index.fields.every(field => typeof field === 'string')) { + continue; + } + + if (!Object.keys(index).every(optionName => supportedOptions.includes(optionName))) { + continue; + } + + uniqueKeys[index.name] = index; + } + + return uniqueKeys; + } + + // TODO [>7]: Remove this + private static get _indexes(): never { + throw new Error('Model._indexes has been replaced with Model.getIndexes()'); + } + + /** + * Refreshes the Model's attribute definition. + * + * @deprecated use {@link modelDefinition}. + */ + static refreshAttributes(): void { + this.modelDefinition.refreshAttributes(); + } + + static assertIsInitialized(): void { + if (!this.isInitialized()) { + throw new Error(`Model "${this.name}" has not been initialized yet. You can check whether a model has been initialized by calling its isInitialized method.`); + } + } + + static isInitialized(): boolean { + // @ts-expect-error -- getModelDefinition expects ModelStatic + return hasModelDefinition(this); + } + + /** + * Get the table name of the model, taking schema into account. The method will an object with `tableName`, `schema` and `delimiter` properties. + * + * @deprecated use {@link modelDefinition} or {@link table}. + */ + static getTableName(): TableNameWithSchema { + // TODO no deprecation warning is issued here, as this is still used internally. + // Start emitting a warning once we have removed all internal usages. + + const queryGenerator = this.sequelize.queryInterface.queryGenerator; + + return { + ...this.table, + /** + * @deprecated This should not be relied upon! + */ + // @ts-expect-error -- This toString is a hacky property that must be removed + toString() { + return queryGenerator.quoteTable(this); + }, + }; + } +} + +export function initModel(model: ModelStatic, attributes: ModelAttributes, options: InitOptions): void { + options.modelName ||= model.name; + + const modelDefinition = new ModelDefinition( + attributes, + options, + model, + ); + + registerModelDefinition(model, modelDefinition); + + Object.defineProperty(model, 'name', { value: modelDefinition.modelName }); + + // @ts-expect-error -- TODO: type + model._scope = model.options.defaultScope; + // @ts-expect-error -- TODO: type + model._scopeNames = ['defaultScope']; + + model.sequelize.modelManager.addModel(model); + model.sequelize.hooks.runSync('afterDefine', model); + + addAttributeGetterAndSetters(model); + model.hooks.addListener('afterDefinitionRefresh', () => { + addAttributeGetterAndSetters(model); + }); +} + +function addAttributeGetterAndSetters(model: ModelStatic) { + const modelDefinition = model.modelDefinition; + + // TODO: temporary workaround due to cyclic import. Should not be necessary once Model is fully migrated to TypeScript. + const { Model: TmpModel } = require('./model.js'); + + // add attributes to the DAO prototype + for (const attribute of modelDefinition.attributes.values()) { + const attributeName = attribute.attributeName; + + if (attributeName in TmpModel.prototype) { + // @ts-expect-error -- TODO: type sequelize.log + model.sequelize.log(`Attribute ${attributeName} in model ${model.name} is shadowing a built-in property of the Model prototype. This is not recommended. Consider renaming your attribute.`); + + continue; + } + + const attributeProperty: PropertyDescriptor = { + configurable: true, + get(this: Model) { + return this.get(attributeName); + }, + set(this: Model, value: unknown) { + return this.set(attributeName, value); + }, + }; + + Object.defineProperty(model.prototype, attributeName, attributeProperty); + } } diff --git a/src/model.d.ts b/src/model.d.ts index f05d9a5a0082..cc8352117aed 100644 --- a/src/model.d.ts +++ b/src/model.d.ts @@ -12,10 +12,14 @@ import type { } from './associations/index'; import type { Deferrable } from './deferrable'; import type { AbstractDataType, DataType } from './dialects/abstract/data-types.js'; -import type { IndexOptions, TableName, TableNameWithSchema } from './dialects/abstract/query-interface'; +import type { + IndexOptions, + TableName, + TableNameWithSchema, +} from './dialects/abstract/query-interface'; import type { IndexHints } from './index-hints'; import type { ValidationOptions } from './instance-validator'; -import type { ModelHooks } from './model-typescript.js'; +import type { ModelHooks } from './model-hooks.js'; import { ModelTypeScript } from './model-typescript.js'; import type { Sequelize, SyncOptions, QueryOptions } from './sequelize'; import type { @@ -32,9 +36,9 @@ import type { AnyFunction, MakeNullishOptional, Nullish, - OmitConstructors, + OmitConstructors, PartlyRequired, } from './utils/types.js'; -import type { LOCK, Op, Optional, Transaction, TableHints } from './index'; +import type { LOCK, Op, Transaction, TableHints } from './index'; export interface Logging { /** @@ -1646,20 +1650,6 @@ export interface ModelNameOptions { plural?: string; } -/** - * Interface for getterMethods in InitOptions - */ -export interface ModelGetterOptions { - [name: string]: (this: M) => unknown; -} - -/** - * Interface for setterMethods in InitOptions - */ -export interface ModelSetterOptions { - [name: string]: (this: M, val: any) => void; -} - /** * Interface for Define Scope Options */ @@ -1673,13 +1663,20 @@ export interface ModelScopeOptions { /** * References options for the column's attributes */ -export interface ModelAttributeColumnReferencesOptions { +export interface AttributeReferencesOptions { /** - * The name of the table to reference (the sql name), or the Model to reference. + * The Model to reference. + * + * Ignored if {@link tableName} is specified. */ - model: TableName | ModelStatic; + model?: ModelStatic; /** + * The name of the table to reference (the sql name). + */ + table?: TableName; + + /** * The column on the target model that this foreign key references */ key?: string; @@ -1692,13 +1689,20 @@ export interface ModelAttributeColumnReferencesOptions { deferrable?: Deferrable | Class; } +export interface NormalizedAttributeReferencesOptions extends Omit { + /** + * The name of the table to reference (the sql name). + */ + readonly table: TableName; +} + // TODO: when merging model.d.ts with model.js, make this an enum. export type ReferentialAction = 'CASCADE' | 'RESTRICT' | 'SET DEFAULT' | 'SET NULL' | 'NO ACTION'; /** * Column options for the model schema attributes */ -export interface ModelAttributeColumnOptions { +export interface AttributeOptions { /** * A string or a data type. * @@ -1714,13 +1718,17 @@ export interface ModelAttributeColumnOptions { */ allowNull?: boolean; + /** + * @deprecated use {@link columnName} instead. + */ + field?: string; + /** * The name of the column. * * If no value is provided, Sequelize will use the name of the attribute (in snake_case if {@link InitOptions.underscored} is true) */ - // TODO [>7]: rename to "columnName" - field?: string; + columnName?: string; /** * A literal default value, a JavaScript function, or an SQL function (using {@link fn}) @@ -1734,6 +1742,12 @@ export interface ModelAttributeColumnOptions { */ unique?: AllowArray; + /** + * If true, an index will be created for this column. + * If a string is provided, the column will be part of a composite index together with the other attributes that specify the same index name. + */ + index?: AllowArray; + /** * If true, this attribute will be marked as primary key */ @@ -1758,9 +1772,9 @@ export interface ModelAttributeColumnOptions { * Makes this attribute a foreign key. * You typically don't need to use this yourself, instead use associations. * - * Setting this value to a string equivalent to setting it to `{ model: 'myString' }`. + * Setting this value to a string equivalent to setting it to `{ tableName: 'myString' }`. */ - references?: string | ModelAttributeColumnReferencesOptions; + references?: string | ModelStatic | AttributeReferencesOptions; /** * What should happen when the referenced key is updated. @@ -1796,29 +1810,39 @@ export interface ModelAttributeColumnOptions { * Use {@link Model.setDataValue} to access the underlying values. */ set?(this: M, val: unknown): void; -} -export interface BuiltModelAttributeColumnOptions extends Omit, 'type' | 'unique'> { /** - * The name of the attribute (JS side). + * This attribute is added by sequelize. Do not use! + * + * @private + * @internal */ - fieldName: string; + // TODO: use a private symbol + _autoGenerated?: boolean; +} + +export interface NormalizedAttributeOptions extends Readonly, 'columnName'>, + | 'type' + // index and unique are always removed from attribute options, Model.getIndexes() must be used instead. + | 'index' | 'unique' +>> { /** - * Like {@link ModelAttributeColumnOptions.type}, but normalized. + * @deprecated use {@link NormalizedAttributeOptions.attributeName} instead. */ - type: string | AbstractDataType; - references?: ModelAttributeColumnReferencesOptions; + readonly fieldName: string; - unique?: Array<{ name: string, msg?: string }>; + /** + * The name of the attribute (JS side). + */ + readonly attributeName: string; /** - * This attribute was added by sequelize. Do not use! - * - * @private - * @internal + * Like {@link AttributeOptions.type}, but normalized. */ - _autoGenerated?: boolean; + readonly type: string | AbstractDataType; + readonly references?: NormalizedAttributeReferencesOptions; } /** @@ -1828,7 +1852,7 @@ export type ModelAttributes = { /** * The description of a database column */ - [name in keyof TAttributes]: DataType | ModelAttributeColumnOptions; + [name in keyof TAttributes]: DataType | AttributeOptions; }; /** @@ -1895,7 +1919,7 @@ export interface ModelOptions { paranoid?: boolean; /** - * If true, Sequelize will snake_case the name of columns that do not have an explicit value set (using {@link ModelAttributeColumnOptions.field}). + * If true, Sequelize will snake_case the name of columns that do not have an explicit value set (using {@link AttributeOptions.field}). * The name of the table will also be snake_cased, unless {@link ModelOptions.tableName} is set, or {@link ModelOptions.freezeTableName} is true. * * @default false @@ -2018,7 +2042,10 @@ export interface ModelOptions { * @see https://sequelize.org/docs/v7/other-topics/hooks/ */ hooks?: { - [Key in keyof ModelHooks>]?: AllowArray>[Key]> + [Key in keyof ModelHooks>]?: AllowArray< + | ModelHooks>[Key] + | { name: string | symbol, callback: ModelHooks>[Key] } + > }; /** @@ -2033,16 +2060,6 @@ export interface ModelOptions { [name: string]: (value: unknown) => boolean, }; - /** - * Allows defining additional setters that will be available on model instances. - */ - setterMethods?: ModelSetterOptions; - - /** - * Allows defining additional getters that will be available on model instances. - */ - getterMethods?: ModelGetterOptions; - /** * Enable optimistic locking. * When enabled, sequelize will add a version count attribute to the model and throw an @@ -2066,7 +2083,7 @@ export interface InitOptions extends ModelOptions { } export type BuiltModelName = Required; -export type BuiltModelOptions = Omit, 'name'> & { +export type BuiltModelOptions = Omit, 'modelName' | 'indexes' | 'underscored' | 'validate' | 'tableName'>, 'name'> & { name: BuiltModelName, }; @@ -2139,159 +2156,13 @@ export abstract class Model6]: make this a non-exported symbol (same as the one in hooks.d.ts) - /** The name of the database table */ - static readonly tableName: string; - - /** - * The name of the primary key attribute (on the JS side). - * - * @deprecated This property doesn't work for composed primary keys. Use {@link Model.primaryKeyAttributes} instead. - */ - static readonly primaryKeyAttribute: string; - - /** - * The column name of the primary key. - * - * @deprecated don't use this. It doesn't work with composite PKs. It may be removed in the future to reduce duplication. - * Use the. Use {@link Model.primaryKeys} instead. - */ - static readonly primaryKeyField: string; - - /** - * The name of the primary key attributes (on the JS side). - */ - static readonly primaryKeyAttributes: readonly string[]; - - /** - * Like {@link Model.rawAttributes}, but only includes attributes that are part of the Primary Key. - */ - static readonly primaryKeys: { [attribute: string]: BuiltModelAttributeColumnOptions }; - - static readonly uniqueKeys: { - [indexName: string]: { - fields: string[], - msg: string | null, - /** - * The name of the attribute - */ - name: string, - column: string, - customIndex: boolean, - }, - }; - - /** - * @internal - */ - static readonly fieldRawAttributesMap: { - [columnName: string]: BuiltModelAttributeColumnOptions, - }; - - /** - * A mapping of column name to attribute name - * - * @internal - */ - static readonly fieldAttributeMap: { - [columnName: string]: string, - }; - - /** - * Like {@link Model.getAttributes}, but only includes attributes that exist in the database. - * i.e. virtual attributes are omitted. - * - * @internal - */ - static tableAttributes: { - [attributeName: string]: BuiltModelAttributeColumnOptions, - }; - - /** - * An object hash from alias to association object - */ - static readonly associations: { - [key: string]: Association, - }; - - /** - * The options that the model was initialized with - */ - static readonly options: BuiltModelOptions; - - // TODO [>7]: Remove `rawAttributes` in v8 - /** - * The attributes of the model. - * - * @deprecated use {@link Model.getAttributes} for better typings. - */ - static readonly rawAttributes: { [attribute: string]: BuiltModelAttributeColumnOptions }; - /** * Returns the attributes of the model */ static getAttributes(this: ModelStatic): { - readonly [Key in keyof Attributes]: BuiltModelAttributeColumnOptions + readonly [Key in keyof Attributes]: NormalizedAttributeOptions }; - static getIndexes(): readonly IndexOptions[]; - - /** - * Initialize a model, representing a table in the DB, with attributes and options. - * - * The table columns are define by the hash that is given as the second argument. Each attribute of the hash represents a column. A short table definition might look like this: - * - * ```js - * 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 - * ``` - * - * As shown above, column definitions can be either strings, a reference to one of the datatypes that are predefined on the Sequelize constructor, or an object that allows you to specify both the type of the column, and other attributes such as default values, foreign key constraints and custom setters and getters. - * - * For a list of possible data types, see https://sequelize.org/docs/v7/other-topics/other-data-types - * - * For more about getters and setters, see https://sequelize.org/docs/v7/core-concepts/getters-setters-virtuals/ - * - * For more about instance and class methods, see https://sequelize.org/docs/v7/core-concepts/model-basics/#taking-advantage-of-models-being-classes - * - * For more about validation, see https://sequelize.org/docs/v7/core-concepts/validations-and-constraints/ - * - * @param 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. - * @param options These options are merged with the default define options provided to the Sequelize constructor - * @returns the initialized model - */ - static init, M extends InstanceType>( - this: MS, - attributes: ModelAttributes< - M, - // 'foreign keys' are optional in Model.init as they are added by association declaration methods - Optional, BrandedKeysOf, typeof ForeignKeyBrand>> - >, - options: InitOptions - ): MS; - - /** - * Refreshes the Model's attribute definition. - */ - static refreshAttributes(): void; - /** * Checks whether an association with this name has already been registered. * @@ -2346,8 +2217,8 @@ export abstract class Model; - /** * Returns true if this instance has not yet been persisted to the database */ diff --git a/src/model.js b/src/model.js index 04bfb8de3e30..22f9c7efa9c9 100644 --- a/src/model.js +++ b/src/model.js @@ -1,9 +1,7 @@ 'use strict'; import omit from 'lodash/omit'; -import { isDecoratedModel } from './decorators/shared/model'; import { AbstractDataType } from './dialects/abstract/data-types'; -import { BaseError } from './errors'; import { intersects } from './utils/array'; import { toDefaultValue } from './utils/dialect'; import { @@ -13,12 +11,12 @@ import { mapValueFieldNames, mapWhereFieldNames, } from './utils/format'; -import { cloneDeep, mergeDefaults, merge, defaults, flattenObjectDeep } from './utils/object'; +import { every, find } from './utils/iterators'; +import { cloneDeep, mergeDefaults, defaults, flattenObjectDeep, getObjectFromMap } from './utils/object'; import { isWhereEmpty } from './utils/query-builder-utils'; import { ModelTypeScript } from './model-typescript'; import { isModelStatic, isSameInitialModel } from './utils/model-utils'; import { SequelizeMethod } from './utils/sequelize-method'; -import { generateIndexName, singularize, pluralize, underscoredIf } from './utils/string'; const assert = require('node:assert'); const NodeUtil = require('node:util'); @@ -89,7 +87,7 @@ export class Model extends ModelTypeScript { // this constructor is done running. setTimeout(() => { const overwrittenAttributes = []; - for (const key of Object.keys(this.constructor._attributeManipulation)) { + for (const key of this.constructor.modelDefinition.attributes.keys()) { if (Object.prototype.hasOwnProperty.call(this, key)) { overwrittenAttributes.push(key); } @@ -105,8 +103,8 @@ export class Model extends ModelTypeScript { options = { isNewRecord: true, - _schema: this.constructor._schema, - _schemaDelimiter: this.constructor._schemaDelimiter, + _schema: this.constructor.modelDefinition.table.schema, + _schemaDelimiter: this.constructor.modelDefinition.table.delimiter, ...options, model: this.constructor, }; @@ -141,49 +139,48 @@ export class Model extends ModelTypeScript { } _initValues(values, options) { - let defaults; - let key; - values = { ...values }; if (options.isNewRecord) { - defaults = {}; + const modelDefinition = this.constructor.modelDefinition; - if (this.constructor._hasDefaultValues) { - defaults = _.mapValues(this.constructor._defaultValues, valueFn => { - const value = valueFn(); + const defaults = modelDefinition.defaultValues.size > 0 + ? _.mapValues(getObjectFromMap(modelDefinition.defaultValues), getDefaultValue => { + const value = getDefaultValue(); return value && value instanceof SequelizeMethod ? value : _.cloneDeep(value); - }); - } + }) + : Object.create(null); // 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 > 0) { - for (const primaryKeyAttribute of this.constructor.primaryKeyAttributes) { + if (modelDefinition.primaryKeysAttributeNames.size > 0) { + for (const primaryKeyAttribute of modelDefinition.primaryKeysAttributeNames) { 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] = toDefaultValue(defaults[this.constructor._timestampAttributes.createdAt], this.sequelize.dialect); - delete defaults[this.constructor._timestampAttributes.createdAt]; + const { createdAt: createdAtAttrName, updatedAt: updatedAtAttrName, deletedAt: deletedAtAttrName } = modelDefinition.timestampAttributeNames; + + if (createdAtAttrName && defaults[createdAtAttrName]) { + this.dataValues[createdAtAttrName] = toDefaultValue(defaults[createdAtAttrName], this.sequelize.dialect); + delete defaults[createdAtAttrName]; } - if (this.constructor._timestampAttributes.updatedAt && defaults[this.constructor._timestampAttributes.updatedAt]) { - this.dataValues[this.constructor._timestampAttributes.updatedAt] = toDefaultValue(defaults[this.constructor._timestampAttributes.updatedAt], this.sequelize.dialect); - delete defaults[this.constructor._timestampAttributes.updatedAt]; + if (updatedAtAttrName && defaults[updatedAtAttrName]) { + this.dataValues[updatedAtAttrName] = toDefaultValue(defaults[updatedAtAttrName], this.sequelize.dialect); + delete defaults[updatedAtAttrName]; } - if (this.constructor._timestampAttributes.deletedAt && defaults[this.constructor._timestampAttributes.deletedAt]) { - this.dataValues[this.constructor._timestampAttributes.deletedAt] = toDefaultValue(defaults[this.constructor._timestampAttributes.deletedAt], this.sequelize.dialect); - delete defaults[this.constructor._timestampAttributes.deletedAt]; + if (deletedAtAttrName && defaults[deletedAtAttrName]) { + this.dataValues[deletedAtAttrName] = toDefaultValue(defaults[deletedAtAttrName], this.sequelize.dialect); + delete defaults[deletedAtAttrName]; } - for (key in defaults) { + for (const key in defaults) { if (values[key] === undefined) { this.set(key, toDefaultValue(defaults[key], this.sequelize.dialect), { raw: true }); delete values[key]; @@ -218,11 +215,13 @@ export class Model extends ModelTypeScript { return options; } - const deletedAtCol = model._timestampAttributes.deletedAt; - const deletedAtAttribute = model.rawAttributes[deletedAtCol]; - const deletedAtObject = {}; + const modelDefinition = model.modelDefinition; + + const deletedAtCol = modelDefinition.timestampAttributeNames.deletedAt; + const deletedAtAttribute = modelDefinition.attributes.get(deletedAtCol); + const deletedAtObject = Object.create(null); - let deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; + let deletedAtDefaultValue = deletedAtAttribute.defaultValue ?? null; deletedAtDefaultValue = deletedAtDefaultValue || { [Op.eq]: null, @@ -239,96 +238,25 @@ export class Model extends ModelTypeScript { return options; } - static _addDefaultAttributes() { - const tail = {}; - let head = {}; - - // Add id if no primary key was manually added to definition - if (!this.options.noPrimaryKey && !_.some(this.rawAttributes, 'primaryKey')) { - if ('id' in this.rawAttributes && this.rawAttributes.id.primaryKey === undefined) { - throw new Error(`An attribute called 'id' was defined in model '${this.tableName}' but primaryKey is not set. This is likely to be an error, which can be fixed by setting its 'primaryKey' option to true. If this is intended, explicitly set its 'primaryKey' option to false`); - } - - 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(6), - allowNull: false, - _autoGenerated: true, - }; - } - - if (this._timestampAttributes.updatedAt) { - tail[this._timestampAttributes.updatedAt] = { - type: DataTypes.DATE(6), - allowNull: false, - _autoGenerated: true, - }; - } - - if (this._timestampAttributes.deletedAt) { - tail[this._timestampAttributes.deletedAt] = { - type: DataTypes.DATE(6), - _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; - } - /** * Returns the attributes of the model. * * @returns {object|any} */ static getAttributes() { - return this.rawAttributes; + return getObjectFromMap(this.modelDefinition.attributes); } - static _findAutoIncrementAttribute() { - this.autoIncrementAttribute = null; + get validators() { + throw new Error('Model#validators has been removed. Use the validators option on Model.modelDefinition.attributes instead.'); + } - 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.'); - } + static get _schema() { + throw new Error('Model._schema has been removed. Use Model.modelDefinition instead.'); + } - this.autoIncrementAttribute = name; - } - } - } + static get _schemaDelimiter() { + throw new Error('Model._schemaDelimiter has been removed. Use Model.modelDefinition instead.'); } static _getAssociationDebugList() { @@ -708,24 +636,6 @@ ${associationOwner._getAssociationDebugList()}`); } } - 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 _baseMerge(...args) { _.assignWith(...args); @@ -771,515 +681,6 @@ ${associationOwner._getAssociationDebugList()}`); }); } - /** - * Indexes created from options.indexes when calling Model.init - */ - static _manualIndexes; - - /** - * Indexes created from {@link ModelAttributeColumnOptions.unique} - */ - static _attributeIndexes; - - static getIndexes() { - return [ - ...(this._manualIndexes ?? []), - ...(this._attributeIndexes ?? []), - ...(this.uniqueKeys ? Object.values(this.uniqueKeys) : []), - ]; - } - - static get _indexes() { - throw new Error('Model._indexes has been replaced with Model.getIndexes()'); - } - - static _nameIndex(newIndex) { - if (Object.prototype.hasOwnProperty.call(newIndex, 'name')) { - return newIndex; - } - - const newName = generateIndexName(this.getTableName(), newIndex); - - // TODO: check for collisions on *all* models, not just this one, as index names are global. - for (const index of this.getIndexes()) { - if (index.name === newName) { - throw new Error(`Sequelize tried to give the name "${newName}" to index: -${NodeUtil.inspect(newIndex)} -on model "${this.name}", but that name is already taken by index: -${NodeUtil.inspect(index)} - -Specify a different name for either index to resolve this issue.`); - } - } - - newIndex.name = newName; - - return newIndex; - } - - /** - * 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 - * ```javascript - * 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 https://sequelize.org/docs/v7/core-concepts/model-basics/ - * @see https://sequelize.org/docs/v7/core-concepts/validations-and-constraints/ - * - * @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. - * @param {object} options These options are merged with the default define options provided to the Sequelize constructor - * @returns {Model} - */ - static init(attributes, options = {}) { - // TODO: In a future major release, Model.init should be reworked to work in two steps: - // - Model.init, Model.hasOne, Model.hasMany, Model.belongsTo, and Model.belongsToMany should *only* call registerModelAttributeOptions, registerModelOptions, and registerModelAssociation - // - Then all models are passed to the Sequelize constructor, which actually inits the options & attributes of all models, *then* adds all associations. - // - If the model is already registered, Model.hasOne, Model.hasMany, Model.belongsTo, and Model.belongsToMany should add the association immediately, so sequelize.define() continues to work - // Model.init should be renamed to something else to prevent confusion (Model.configure?) - if (isDecoratedModel(this)) { - throw new Error(`Model.init cannot be used if the model uses one of Sequelize's decorators. You must pass your model to the Sequelize constructor using the "models" option instead.`); - } - - return this._internalInit(attributes, options); - } - - static _internalInit(attributes, options = {}) { - if (!options.sequelize) { - throw new Error('Model.init expects a Sequelize instance to be passed through the option bag, which is the second parameter.'); - } - - this._setSequelize(options.sequelize); - - const globalOptions = this.sequelize.options; - - options = merge(_.cloneDeep(globalOptions.define), options); - - if (!options.modelName) { - options.modelName = this.name; - } - - options = merge({ - name: { - plural: pluralize(options.modelName), - singular: singularize(options.modelName), - }, - indexes: [], - omitNull: globalOptions.omitNull, - schema: globalOptions.schema, - }, options); - - this.sequelize.hooks.runSync('beforeDefine', attributes, options); - - if (options.modelName !== this.name) { - Object.defineProperty(this, 'name', { value: options.modelName }); - } - - delete options.modelName; - - this.options = { - noPrimaryKey: false, - timestamps: true, - validate: {}, - freezeTableName: false, - underscored: false, - paranoid: false, - rejectOnEmpty: false, - schema: '', - 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 = Object.create(null); - if (options.hooks) { - this.hooks.addListeners(options.hooks); - } - - // TODO: use private field - this.underscored = this.options.underscored; - - if (!this.options.tableName) { - this.tableName = this.options.freezeTableName ? this.name : underscoredIf(pluralize(this.name), this.underscored); - } else { - this.tableName = this.options.tableName; - } - - this._schema = this.options.schema || this.sequelize.options.schema || this.sequelize.dialect.getDefaultSchema(); - 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 TypeError(`Members of the validate option must be functions. Model: ${this.name}, error with validate member ${validatorType}`); - } - }); - - this.rawAttributes = _.mapValues(attributes, (attribute, name) => { - try { - attribute = this.sequelize.normalizeAttribute(attribute); - } catch (error) { - throw new BaseError(`An error occurred for attribute ${name} on model ${this.name}.`, { cause: error }); - } - - if (attribute.type instanceof AbstractDataType) { - attribute.type.attachUsageContext({ - model: this, - attributeName: name, - sequelize: this.sequelize, - }); - } - - // Checks whether the name is ambiguous with isColString - // we check whether the attribute starts *or* ends because the following query: - // { '$json.key$' } - // could be interpreted as both - // "json"."key" (accessible attribute 'key' on model 'json') - // or - // "$json" #>> {key$} (accessing key 'key$' on attribute '$json') - if (name.startsWith('$') || name.endsWith('$')) { - throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot start or end with "$" as "$attribute$" is reserved syntax used to reference nested columns in queries.`); - } - - if (name.includes('.')) { - throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot include the character "." as it would be ambiguous with the syntax used to reference nested columns, and nested json keys, in queries.`); - } - - if (name.includes('::')) { - throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot include the character sequence "::" as it is reserved syntax used to cast attributes in queries.`); - } - - if (name.includes('->')) { - throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot include the character sequence "->" as it is reserved syntax used in SQL generated by Sequelize to target nested associations.`); - } - - if (attribute.type === undefined) { - throw new Error(`Attribute "${this.name}.${name}" does not specify its DataType.`); - } - - 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; - }); - - this._manualIndexes = this.options.indexes - .map(index => this._nameIndex(this._conformIndex(index))); - - this.primaryKeys = Object.create(null); - this._readOnlyAttributes = new Set(); - this._timestampAttributes = Object.create(null); - - // 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.hooks.runSync('afterDefine', this); - - return this; - } - - static refreshAttributes() { - const attributeManipulation = {}; - - this.prototype._customGetters = {}; - this.prototype._customSetters = {}; - - for (const type of ['get', 'set']) { - 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._hasBooleanAttributes = false; - this._hasDateAttributes = false; - this._jsonAttributes = new Set(); - this._virtualAttributes = new Set(); - this._defaultValues = {}; - this.prototype.validators = {}; - - this.fieldRawAttributesMap = Object.create(null); - - this.primaryKeys = Object.create(null); - this.uniqueKeys = Object.create(null); - - this._attributeIndexes = []; - - _.each(this.rawAttributes, (definition, name) => { - try { - definition.type = this.sequelize.normalizeDataType(definition.type); - if (definition.type instanceof AbstractDataType) { - definition.type.attachUsageContext({ - model: this, - attributeName: name, - sequelize: this.sequelize, - }); - } - - definition.Model = this; - definition.fieldName = name; - definition._modelAttribute = true; - - if (definition.field === undefined) { - definition.field = underscoredIf(name, this.underscored); - } - - if (definition.primaryKey === true) { - this.primaryKeys[name] = definition; - } - - this.fieldRawAttributesMap[definition.field] = definition; - - 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] = () => toDefaultValue(definition.defaultValue, this.sequelize.dialect); - } - - if (Object.prototype.hasOwnProperty.call(definition, 'unique') && definition.unique) { - if (!Array.isArray(definition.unique)) { - definition.unique = [definition.unique]; - } - - for (let i = 0; i < definition.unique.length; i++) { - let unique = definition.unique[i]; - - if (typeof unique === 'string') { - unique = { - name: unique, - }; - } else if (unique === true) { - unique = {}; - } - - definition.unique[i] = unique; - - const index = unique.name && this.uniqueKeys[unique.name] - ? this.uniqueKeys[unique.name] - : { fields: [] }; - - index.fields.push(definition.field); - index.msg = index.msg || unique.msg || null; - - // TODO: remove this 'column'? It does not work with composite indexes, and is only used by db2 which should use fields instead. - index.column = name; - - index.customIndex = unique !== true; - index.unique = true; - - if (unique.name) { - index.name = unique.name; - } else { - this._nameIndex(index); - } - - unique.name ??= index.name; - - this.uniqueKeys[index.name] = index; - } - } - - if (Object.prototype.hasOwnProperty.call(definition, 'validate')) { - this.prototype.validators[name] = definition.validate; - } - - if (definition.index === true && definition.type instanceof DataTypes.JSONB) { - this._attributeIndexes.push( - this._nameIndex( - this._conformIndex({ - fields: [definition.field || name], - using: 'gin', - }), - ), - ); - - delete definition.index; - } - } catch (error) { - throw new BaseError(`An error occured while normalizing attribute ${this.name}#${name}.`, { cause: error }); - } - }); - - // 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 > 0; - - this._hasVirtualAttributes = this._virtualAttributes.size > 0; - - 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); - - this._attributeManipulation = attributeManipulation; - } - /** * Remove attribute from model definition. * Only use if you know what you're doing. @@ -1287,8 +688,8 @@ Specify a different name for either index to resolve this issue.`); * @param {string} attribute name of attribute to remove */ static removeAttribute(attribute) { - delete this.rawAttributes[attribute]; - this.refreshAttributes(); + delete this.modelDefinition.rawAttributes[attribute]; + this.modelDefinition.refreshAttributes(); } /** @@ -1300,11 +701,13 @@ Specify a different name for either index to resolve this issue.`); * @param {object} newAttributes */ static mergeAttributesDefault(newAttributes) { - mergeDefaults(this.rawAttributes, newAttributes); + const rawAttributes = this.modelDefinition.rawAttributes; + + mergeDefaults(rawAttributes, newAttributes); - this.refreshAttributes(); + this.modelDefinition.refreshAttributes(); - return this.rawAttributes; + return rawAttributes; } /** @@ -1319,14 +722,15 @@ Specify a different name for either index to resolve this issue.`); options = { ...this.options, ...options }; options.hooks = options.hooks === undefined ? true : Boolean(options.hooks); - const attributes = this.tableAttributes; - const rawAttributes = this.fieldRawAttributesMap; + const modelDefinition = this.modelDefinition; + const physicalAttributes = getObjectFromMap(modelDefinition.physicalAttributes); + const columnDefs = getObjectFromMap(modelDefinition.columns); if (options.hooks) { await this.hooks.runAsync('beforeSync', options); } - const tableName = this.getTableName(options); + const tableName = { ...this.table }; if (options.schema && options.schema !== tableName.schema) { // Some users sync the same set of tables in different schemas for various reasons // They then set `searchPath` when running a query to use different schemas. @@ -1350,10 +754,10 @@ Specify a different name for either index to resolve this issue.`); } if (!tableExists) { - await this.queryInterface.createTable(tableName, attributes, options, this); + await this.queryInterface.createTable(tableName, physicalAttributes, options, this); } else { // enums are always updated, even if alter is not set. createTable calls it too. - await this.queryInterface.ensureEnums(tableName, attributes, options, this); + await this.queryInterface.ensureEnums(tableName, physicalAttributes, options, this); } if (tableExists && options.alter) { @@ -1367,13 +771,13 @@ Specify a different name for either index to resolve this issue.`); const foreignKeyReferences = tableInfos[1]; const removedConstraints = {}; - for (const columnName in attributes) { - if (!Object.prototype.hasOwnProperty.call(attributes, columnName)) { + for (const columnName in physicalAttributes) { + if (!Object.prototype.hasOwnProperty.call(physicalAttributes, columnName)) { continue; } - if (!columns[columnName] && !columns[attributes[columnName].field]) { - await this.queryInterface.addColumn(tableName, attributes[columnName].field || columnName, attributes[columnName], options); + if (!columns[columnName] && !columns[physicalAttributes[columnName].field]) { + await this.queryInterface.addColumn(tableName, physicalAttributes[columnName].field || columnName, physicalAttributes[columnName], options); } } @@ -1383,7 +787,7 @@ Specify a different name for either index to resolve this issue.`); continue; } - const currentAttribute = rawAttributes[columnName]; + const currentAttribute = columnDefs[columnName]; if (!currentAttribute) { await this.queryInterface.removeColumn(tableName, columnName, options); continue; @@ -1403,9 +807,9 @@ Specify a different name for either index to resolve this issue.`); database = schema; } - const foreignReferenceSchema = currentAttribute.references.model.schema; - const foreignReferenceTableName = typeof references.model === 'object' - ? references.model.tableName : references.model; + const foreignReferenceSchema = currentAttribute.references.table.schema; + const foreignReferenceTableName = typeof references.table === 'object' + ? references.table.tableName : references.table; // Find existed foreign keys for (const foreignKeyReference of foreignKeyReferences) { const constraintName = foreignKeyReference.constraintName; @@ -1530,27 +934,6 @@ Specify a different name for either index to resolve this issue.`); return this._initialModel ?? this; } - /** - * Get the table name of the model, taking schema into account. The method will return The name as a string if the model - * has no schema, or an object with `tableName`, `schema` and `delimiter` properties. - * - * @returns {string|object} - */ - static getTableName() { - const self = this; - - return { - tableName: this.tableName, - schema: this._schema, - delimiter: this._schemaDelimiter || '.', - // TODO: remove, it should not be relied on - // once this is removed, also remove the various omit(..., 'toString') that are used in tests when deep-equaling table names. - toString() { - return self.sequelize.queryInterface.queryGenerator.quoteTable(this); - }, - }; - } - /** * Add a new scope to the model * @@ -1653,9 +1036,11 @@ Specify a different name for either index to resolve this issue.`); scopeNames.push(scopeName ? scopeName : 'defaultScope'); } + const modelDefinition = this.modelDefinition; + return initialModel._withScopeAndSchema({ - schema: this._schema || '', - schemaDelimiter: this._schemaDelimiter || '', + schema: modelDefinition.table.schema || '', + schemaDelimiter: modelDefinition.table.delimiter || '', }, mergedScope, scopeNames); } @@ -1686,10 +1071,16 @@ Specify a different name for either index to resolve this issue.`); static withInitialScope() { const initialModel = this.getInitialModel(); - if (this._schema !== initialModel._schema || this._schemaDelimiter !== initialModel._schemaDelimiter) { + const modelDefinition = this.modelDefinition; + const initialModelDefinition = initialModel.modelDefinition; + + if ( + modelDefinition.table.schema !== initialModelDefinition.table.schema + || modelDefinition.table.delimiter !== initialModelDefinition.table.delimiter + ) { return initialModel.withSchema({ - schema: this._schema, - schemaDelimiter: this._schemaDelimiter, + schema: modelDefinition.table.schema, + schemaDelimiter: modelDefinition.table.delimiter, }); } @@ -1703,6 +1094,12 @@ Specify a different name for either index to resolve this issue.`); this._modelVariantRefs = new Set([new WeakRef(this)]); } + const newTable = this.queryGenerator.extractTableDetails({ + tableName: this.modelDefinition.table.tableName, + schema: schemaOptions.schema, + delimiter: schemaOptions.delimiter, + }); + for (const modelVariantRef of this._modelVariantRefs) { const modelVariant = modelVariantRef.deref(); @@ -1711,11 +1108,13 @@ Specify a different name for either index to resolve this issue.`); continue; } - if (modelVariant._schema !== (schemaOptions.schema || '')) { + const variantTable = modelVariant.table; + + if (variantTable.schema !== newTable.schema) { continue; } - if (modelVariant._schemaDelimiter !== (schemaOptions.schemaDelimiter || '')) { + if (variantTable.delimiter !== newTable.delimiter) { continue; } @@ -1731,12 +1130,13 @@ Specify a different name for either index to resolve this issue.`); return modelVariant; } - const clone = this._createModelVariant(); + const clone = this._createModelVariant({ + schema: schemaOptions.schema, + schemaDelimiter: schemaOptions.schemaDelimiter, + }); // eslint-disable-next-line no-undef -- eslint doesn't know about WeakRef, this will be resolved once we migrate to TS. this._modelVariantRefs.add(new WeakRef(clone)); - clone._schema = schemaOptions.schema || ''; - clone._schemaDelimiter = schemaOptions.schemaDelimiter || ''; clone._scope = mergedScope; clone._scopeNames = scopeNames; @@ -1747,23 +1147,19 @@ Specify a different name for either index to resolve this issue.`); return clone; } - static _createModelVariant() { + static _createModelVariant(optionOverrides) { const model = class extends this {}; model._initialModel = this; Object.defineProperty(model, 'name', { value: this.name }); - model._setSequelize(this.sequelize); - model.rawAttributes = _.mapValues(this.rawAttributes, attributeDefinition => { - return { - ...attributeDefinition, - // DataTypes can only belong to one model at a time. The variant must receive a copy, or their usage context will be wrong. - type: attributeDefinition.type instanceof AbstractDataType - ? attributeDefinition.type.clone() - : attributeDefinition.type, - }; + model.init(this.modelDefinition.rawAttributes, { + ...this.options, + ...optionOverrides, }); - model.refreshAttributes(); + // This is done for legacy reasons, where in a previous design both models shared the same association objects. + // TODO: re-create the associations on the new model instead of sharing them. + Object.assign(model.modelDefinition.associations, this.modelDefinition.associations); return model; } @@ -1798,7 +1194,9 @@ Specify a different name for either index to resolve this issue.`); throw new sequelizeErrors.QueryError('The attributes option must be an array of column names or an object'); } - this._warnOnInvalidOptions(options, Object.keys(this.rawAttributes)); + const modelDefinition = this.modelDefinition; + + this._warnOnInvalidOptions(options, Object.keys(modelDefinition.attributes)); const tableNames = {}; @@ -1849,7 +1247,7 @@ Specify a different name for either index to resolve this issue.`); } if (!options.attributes) { - options.attributes = Object.keys(this.rawAttributes); + options.attributes = Array.from(modelDefinition.attributes.keys()); options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes); } @@ -1862,7 +1260,7 @@ Specify a different name for either index to resolve this issue.`); } const selectOptions = { ...options, tableNames: Object.keys(tableNames) }; - const results = await this.queryInterface.select(this, this.getTableName(selectOptions), selectOptions); + const results = await this.queryInterface.select(this, this.table, selectOptions); if (options.hooks) { await this.hooks.runAsync('afterFind', results, options); } @@ -1896,7 +1294,9 @@ Specify a different name for either index to resolve this issue.`); } static _injectDependentVirtualAttributes(attributes) { - if (!this._hasVirtualAttributes) { + const modelDefinition = this.modelDefinition; + + if (modelDefinition.virtualAttributeNames.size === 0) { return attributes; } @@ -1906,10 +1306,10 @@ Specify a different name for either index to resolve this issue.`); for (const attribute of attributes) { if ( - this._virtualAttributes.has(attribute) - && this.rawAttributes[attribute].type.attributeDependencies + modelDefinition.virtualAttributeNames.has(attribute) + && modelDefinition.attributes.get(attribute).type.attributeDependencies ) { - attributes = attributes.concat(this.rawAttributes[attribute].type.attributeDependencies); + attributes = attributes.concat(modelDefinition.attributes.get(attribute).type.attributeDependencies); } } @@ -2068,7 +1468,7 @@ Specify a different name for either index to resolve this issue.`); _validateIncludedElements(options); } - const attrOptions = this.rawAttributes[attribute]; + const attrOptions = this.getAttributes()[attribute]; const field = attrOptions && attrOptions.field || attribute; let aggregateColumn = this.sequelize.col(field); @@ -2381,9 +1781,11 @@ Specify a different name for either index to resolve this issue.`); options = { ...options }; + const modelDefinition = this.modelDefinition; + if (options.defaults) { const defaults = Object.keys(options.defaults); - const unknownDefaults = defaults.filter(name => !this.rawAttributes[name]); + const unknownDefaults = defaults.filter(name => !modelDefinition.attributes.has(name)); if (unknownDefaults.length > 0) { logger.warn(`Unknown attributes (${unknownDefaults}) passed to defaults option of findOrCreate`); @@ -2398,11 +1800,10 @@ Specify a different name for either index to resolve this issue.`); try { // TODO: use managed sequelize.transaction() instead - const t = await this.sequelize.startUnmanagedTransaction(options); - transaction = t; - options.transaction = t; + transaction = await this.sequelize.startUnmanagedTransaction(options); + options.transaction = transaction; - const found = await this.findOne(defaults({ transaction }, options)); + const found = await this.findOne(options); if (found !== null) { return [found, false]; } @@ -2430,10 +1831,10 @@ Specify a different name for either index to resolve this issue.`); const flattenedWhere = flattenObjectDeep(options.where); const flattenedWhereKeys = Object.keys(flattenedWhere).map(name => _.last(name.split('.'))); - const whereFields = flattenedWhereKeys.map(name => _.get(this.rawAttributes, `${name}.field`, name)); + const whereFields = flattenedWhereKeys.map(name => modelDefinition.attributes.get(name)?.columnName ?? name); const defaultFields = options.defaults && Object.keys(options.defaults) - .filter(name => this.rawAttributes[name]) - .map(name => this.rawAttributes[name].field || name); + .filter(name => modelDefinition.attributes.get(name)) + .map(name => modelDefinition.getColumnNameLoose(name)); const errFieldKeys = Object.keys(error.fields); const errFieldsWhereIntersects = intersects(errFieldKeys, whereFields); @@ -2443,7 +1844,7 @@ Specify a different name for either index to resolve this issue.`); if (errFieldsWhereIntersects) { _.each(error.fields, (value, key) => { - const name = this.fieldRawAttributesMap[key].fieldName; + const name = modelDefinition.columns.get(key).attributeName; if (value.toString() !== options.where[name].toString()) { throw new Error(`${this.name}#findOrCreate: value used for ${name} was not equal for both the find and the create calls, '${options.where[name]}' vs '${value}'`); } @@ -2554,8 +1955,10 @@ Specify a different name for either index to resolve this issue.`); setTransactionFromAls(options, this.sequelize); - const createdAtAttr = this._timestampAttributes.createdAt; - const updatedAtAttr = this._timestampAttributes.updatedAt; + const modelDefinition = this.modelDefinition; + + const createdAtAttr = modelDefinition.timestampAttributeNames.createdAt; + const updatedAtAttr = modelDefinition.timestampAttributeNames.updatedAt; const hasPrimary = this.primaryKeyField in values || this.primaryKeyAttribute in values; const instance = this.build(values); @@ -2573,18 +1976,18 @@ Specify a different name for either index to resolve this issue.`); // Map field names const updatedDataValues = _.pick(instance.dataValues, changed); - const insertValues = mapValueFieldNames(instance.dataValues, Object.keys(instance.rawAttributes), this); + const insertValues = mapValueFieldNames(instance.dataValues, modelDefinition.attributes.keys(), this); const updateValues = mapValueFieldNames(updatedDataValues, options.fields, this); const now = new Date(); // Attach createdAt if (createdAtAttr && !insertValues[createdAtAttr]) { - const field = this.rawAttributes[createdAtAttr].field || createdAtAttr; + const field = modelDefinition.attributes.get(createdAtAttr).columnName || createdAtAttr; insertValues[field] = this._getDefaultTimestamp(createdAtAttr) || now; } if (updatedAtAttr && !updateValues[updatedAtAttr]) { - const field = this.rawAttributes[updatedAtAttr].field || updatedAtAttr; + const field = modelDefinition.attributes.get(updatedAtAttr).columnName || updatedAtAttr; insertValues[field] = updateValues[field] = this._getDefaultTimestamp(updatedAtAttr) || now; } @@ -2594,13 +1997,13 @@ Specify a different name for either index to resolve this issue.`); // TODO: remove. This is fishy and is going to be a source of bugs (because it replaces null values with arbitrary values that could be actual data). // If DB2 doesn't support NULL in unique columns, then it should error if the user tries to insert NULL in one. this.uniqno = this.sequelize.dialect.queryGenerator.addUniqueFields( - insertValues, this.rawAttributes, this.uniqno, + insertValues, this.modelDefinition.rawAttributes, this.uniqno, ); } // Build adds a null value for the primary key, if none was given by the user. // We need to remove that because of some Postgres technicalities. - if (!hasPrimary && this.primaryKeyAttribute && !this.rawAttributes[this.primaryKeyAttribute].defaultValue) { + if (!hasPrimary && this.primaryKeyAttribute && !modelDefinition.attributes.get(this.primaryKeyAttribute).defaultValue) { delete insertValues[this.primaryKeyField]; delete updateValues[this.primaryKeyField]; } @@ -2697,10 +2100,11 @@ Specify a different name for either index to resolve this issue.`); } const model = options.model; + const modelDefinition = model.modelDefinition; - options.fields = options.fields || Object.keys(model.rawAttributes); - const createdAtAttr = model._timestampAttributes.createdAt; - const updatedAtAttr = model._timestampAttributes.updatedAt; + options.fields = options.fields || Array.from(modelDefinition.attributes.keys()); + const createdAtAttr = modelDefinition.timestampAttributeNames.createdAt; + const updatedAtAttr = modelDefinition.timestampAttributeNames.updatedAt; if (options.updateOnDuplicate !== undefined) { if (Array.isArray(options.updateOnDuplicate) && options.updateOnDuplicate.length > 0) { @@ -2808,7 +2212,7 @@ Specify a different name for either index to resolve this issue.`); } const out = mapValueFieldNames(values, options.fields, model); - for (const key of model._virtualAttributes) { + for (const key of modelDefinition.virtualAttributeNames) { delete out[key]; } @@ -2817,13 +2221,16 @@ Specify a different name for either index to resolve this issue.`); // Map attributes to fields for serial identification const fieldMappedAttributes = {}; - for (const attr in model.tableAttributes) { - fieldMappedAttributes[model.rawAttributes[attr].field || attr] = model.rawAttributes[attr]; + for (const attrName in model.tableAttributes) { + const attribute = modelDefinition.attributes.get(attrName); + fieldMappedAttributes[attribute.columnName] = attribute; } // Map updateOnDuplicate attributes to fields if (options.updateOnDuplicate) { - options.updateOnDuplicate = options.updateOnDuplicate.map(attr => model.rawAttributes[attr].field || attr); + options.updateOnDuplicate = options.updateOnDuplicate.map(attrName => { + return modelDefinition.getColumnName(attrName); + }); const upsertKeys = []; @@ -2840,7 +2247,7 @@ Specify a different name for either index to resolve this issue.`); // Map returning attributes to fields if (options.returning && Array.isArray(options.returning)) { - options.returning = options.returning.map(attr => _.get(model.rawAttributes[attr], 'field', attr)); + options.returning = options.returning.map(attr => modelDefinition.getColumnNameLoose(attr)); } const results = await model.queryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes); @@ -2861,9 +2268,12 @@ Specify a different name for either index to resolve this issue.`); if (Object.prototype.hasOwnProperty.call(result, key)) { const record = result[key]; - const attr = _.find(model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key); + const attr = find( + modelDefinition.attributes.values(), + attribute => attribute.attributeName === key || attribute.columnName === key, + ); - instance.dataValues[attr && attr.fieldName || key] = record; + instance.dataValues[attr && attr.attributeName || key] = record; } } } @@ -2922,15 +2332,19 @@ Specify a different name for either index to resolve this issue.`); ...include.association.through.scope, }; if (associationInstance[include.association.through.model.name]) { - for (const attr of Object.keys(include.association.through.model.rawAttributes)) { - if (include.association.through.model.rawAttributes[attr]._autoGenerated - || attr === include.association.foreignKey - || attr === include.association.otherKey - || typeof associationInstance[include.association.through.model.name][attr] === 'undefined') { + const throughDefinition = include.association.through.model.modelDefinition; + + for (const attributeName of throughDefinition.attributes.keys()) { + const attribute = throughDefinition.attributes.get(attributeName); + + if (attribute._autoGenerated + || attributeName === include.association.foreignKey + || attributeName === include.association.otherKey + || typeof associationInstance[include.association.through.model.name][attributeName] === 'undefined') { continue; } - values[attr] = associationInstance[include.association.through.model.name][attr]; + values[attributeName] = associationInstance[include.association.through.model.name][attributeName]; } } @@ -2954,17 +2368,20 @@ Specify a different name for either index to resolve this issue.`); // map fields back to attributes for (const instance of instances) { - for (const attr in model.rawAttributes) { - if (model.rawAttributes[attr].field - && instance.dataValues[model.rawAttributes[attr].field] !== undefined - && model.rawAttributes[attr].field !== attr + const attributeDefs = modelDefinition.attributes; + + for (const attribute of attributeDefs.values()) { + if ( + instance.dataValues[attribute.columnName] !== undefined + && attribute.columnName !== attribute.attributeName ) { - instance.dataValues[attr] = instance.dataValues[model.rawAttributes[attr].field]; - delete instance.dataValues[model.rawAttributes[attr].field]; + instance.dataValues[attribute.attributeName] = instance.dataValues[attribute.columnName]; + // TODO: if a column shares the same name as an attribute, this will cause a bug! + delete instance.dataValues[attribute.columnName]; } - instance._previousDataValues[attr] = instance.dataValues[attr]; - instance.changed(attr, false); + instance._previousDataValues[attribute.attributeName] = instance.dataValues[attribute.attributeName]; + instance.changed(attribute.attributeName, false); } instance.isNewRecord = false; @@ -3018,6 +2435,9 @@ Specify a different name for either index to resolve this issue.`); throw new Error('Expected plain object, array or sequelize method in the options.where parameter of model.destroy.'); } + const modelDefinition = this.modelDefinition; + const attributes = modelDefinition.attributes; + options = _.defaults(options, { hooks: true, individualHooks: false, @@ -3048,19 +2468,19 @@ Specify a different name for either index to resolve this issue.`); let result; // Run delete query (or update if paranoid) - if (this._timestampAttributes.deletedAt && !options.force) { + if (modelDefinition.timestampAttributeNames.deletedAt && !options.force) { // Set query type appropriately when running soft delete options.type = QueryTypes.BULKUPDATE; const attrValueHash = {}; - const deletedAtAttribute = this.rawAttributes[this._timestampAttributes.deletedAt]; - const field = this.rawAttributes[this._timestampAttributes.deletedAt].field; + const deletedAtAttribute = attributes.get(modelDefinition.timestampAttributeNames.deletedAt); + const deletedAtColumnName = deletedAtAttribute.columnName; const where = { - [field]: Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null, + [deletedAtColumnName]: Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null, }; - attrValueHash[field] = new Date(); - result = await this.queryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, this.rawAttributes); + attrValueHash[deletedAtColumnName] = new Date(); + result = await this.queryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, getObjectFromMap(modelDefinition.attributes)); } else { result = await this.queryInterface.bulkDelete(this.getTableName(options), options.where, options, this); } @@ -3090,7 +2510,9 @@ Specify a different name for either index to resolve this issue.`); * @returns {Promise} */ static async restore(options) { - if (!this._timestampAttributes.deletedAt) { + const modelDefinition = this.modelDefinition; + + if (!modelDefinition.timestampAttributeNames.deletedAt) { throw new Error('Model is not paranoid'); } @@ -3124,13 +2546,13 @@ Specify a different name for either index to resolve this issue.`); // Run undelete query const attrValueHash = {}; - const deletedAtCol = this._timestampAttributes.deletedAt; - const deletedAtAttribute = this.rawAttributes[deletedAtCol]; - const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; + const deletedAtAttributeName = modelDefinition.timestampAttributeNames.deletedAt; + const deletedAtAttribute = modelDefinition.attributes.get(deletedAtAttributeName); + const deletedAtDefaultValue = deletedAtAttribute.defaultValue ?? null; - attrValueHash[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue; + attrValueHash[deletedAtAttribute.columnName || deletedAtAttributeName] = deletedAtDefaultValue; options.omitNull = false; - const result = await this.queryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, this.rawAttributes); + const result = await this.queryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, getObjectFromMap(modelDefinition.attributes)); // Run afterDestroy hook on each record individually if (options.individualHooks) { await Promise.all( @@ -3168,6 +2590,8 @@ Specify a different name for either index to resolve this issue.`); this._injectScope(options); this._optionsMustContainWhere(options); + const modelDefinition = this.modelDefinition; + options = this._paranoidClause(this, _.defaults(options, { validate: true, hooks: true, @@ -3182,6 +2606,8 @@ Specify a different name for either index to resolve this issue.`); // Clone values so it doesn't get modified for caller scope and ignore undefined values values = _.omitBy(values, value => value === undefined); + const updatedAtAttrName = modelDefinition.timestampAttributeNames.updatedAt; + // Remove values that are not in the options.fields if (options.fields && Array.isArray(options.fields)) { for (const key of Object.keys(values)) { @@ -3190,15 +2616,14 @@ Specify a different name for either index to resolve this issue.`); } } } else { - const updatedAtAttr = this._timestampAttributes.updatedAt; - options.fields = _.intersection(Object.keys(values), Object.keys(this.tableAttributes)); - if (updatedAtAttr && !options.fields.includes(updatedAtAttr)) { - options.fields.push(updatedAtAttr); + options.fields = _.intersection(Object.keys(values), Array.from(modelDefinition.physicalAttributes.keys())); + if (updatedAtAttrName && !options.fields.includes(updatedAtAttrName)) { + options.fields.push(updatedAtAttrName); } } - if (this._timestampAttributes.updatedAt && !options.silent) { - values[this._timestampAttributes.updatedAt] = this._getDefaultTimestamp(this._timestampAttributes.updatedAt) || new Date(); + if (updatedAtAttrName && !options.silent) { + values[updatedAtAttrName] = this._getDefaultTimestamp(updatedAtAttrName) || new Date(); } options.model = this; @@ -3207,15 +2632,16 @@ Specify a different name for either index to resolve this issue.`); // Validate if (options.validate) { const build = this.build(values); - build.set(this._timestampAttributes.updatedAt, values[this._timestampAttributes.updatedAt], { raw: true }); + build.set(updatedAtAttrName, values[updatedAtAttrName], { raw: true }); if (options.sideEffects) { Object.assign(values, _.pick(build.get(), build.changed())); options.fields = _.union(options.fields, Object.keys(values)); } + // TODO: instead of setting "skip", set the "fields" property on a copy of options that's passed to "validate" // We want to skip validations for all other fields - options.skip = _.difference(Object.keys(this.rawAttributes), Object.keys(values)); + options.skip = _.difference(Array.from(modelDefinition.attributes.keys()), Object.keys(values)); const attributes = await build.validate(options); options.skip = undefined; if (attributes && attributes.dataValues) { @@ -3310,7 +2736,7 @@ Specify a different name for either index to resolve this issue.`); if (updateDoneRowByRow) { result = [instances.length, instances]; } else if (_.isEmpty(valuesUse) - || Object.keys(valuesUse).length === 1 && valuesUse[this._timestampAttributes.updatedAt]) { + || Object.keys(valuesUse).length === 1 && valuesUse[updatedAtAttrName]) { // only updatedAt is being passed, then skip update result = [0]; } else { @@ -3318,7 +2744,7 @@ Specify a different name for either index to resolve this issue.`); options = mapOptionFieldNames(options, this); options.hasTrigger = this.options ? this.options.hasTrigger : false; - const affectedRows = await this.queryInterface.bulkUpdate(this.getTableName(options), valuesUse, options.where, options, this.tableAttributes); + const affectedRows = await this.queryInterface.bulkUpdate(this.getTableName(options), valuesUse, options.where, options, getObjectFromMap(this.modelDefinition.physicalAttributes)); if (options.returning) { result = [affectedRows.length, affectedRows]; instances = affectedRows; @@ -3353,15 +2779,20 @@ Specify a different name for either index to resolve this issue.`); * * @returns {Promise} hash of attributes and their types */ + // TODO: move "schema" to options static async describe(schema, options) { - return await this.queryInterface.describeTable(this.tableName, { schema: schema || this._schema || '', ...options }); + const table = this.modelDefinition.table; + + return await this.queryInterface.describeTable(table.tableName, { schema: schema || table.schema, ...options }); } - static _getDefaultTimestamp(attr) { - if (Boolean(this.rawAttributes[attr]) && Boolean(this.rawAttributes[attr].defaultValue)) { - return toDefaultValue(this.rawAttributes[attr].defaultValue, this.sequelize.dialect); - } + static _getDefaultTimestamp(attributeName) { + const attributes = this.modelDefinition.attributes; + const attribute = attributes.get(attributeName); + if (attribute?.defaultValue) { + return toDefaultValue(attribute.defaultValue, this.sequelize.dialect); + } } static _expandAttributes(options) { @@ -3369,7 +2800,7 @@ Specify a different name for either index to resolve this issue.`); return; } - let attributes = Object.keys(this.rawAttributes); + let attributes = Array.from(this.modelDefinition.attributes.keys()); if (options.attributes.exclude) { attributes = attributes.filter(elem => !options.attributes.exclude.includes(elem)); @@ -3468,20 +2899,25 @@ Instead of specifying a Model, either: fields = [fields]; } + const modelDefinition = this.modelDefinition; + const attributeDefs = modelDefinition.attributes; + if (Array.isArray(fields)) { - fields = fields.map(f => { - if (this.rawAttributes[f] && this.rawAttributes[f].field && this.rawAttributes[f].field !== f) { - return this.rawAttributes[f].field; + fields = fields.map(attributeName => { + const attributeDef = attributeDefs.get(attributeName); + if (attributeDef && attributeDef.columnName !== attributeName) { + return attributeDef.columnName; } - return f; + return attributeName; }); } else if (fields && typeof fields === 'object') { - fields = Object.keys(fields).reduce((rawFields, f) => { - if (this.rawAttributes[f] && this.rawAttributes[f].field && this.rawAttributes[f].field !== f) { - rawFields[this.rawAttributes[f].field] = fields[f]; + fields = Object.keys(fields).reduce((rawFields, attributeName) => { + const attributeDef = attributeDefs.get(attributeName); + if (attributeDef && attributeDef.columnName !== attributeName) { + rawFields[attributeDef.columnName] = fields[attributeName]; } else { - rawFields[f] = fields[f]; + rawFields[attributeName] = fields[attributeName]; } return rawFields; @@ -3519,16 +2955,16 @@ Instead of specifying a Model, either: // If optimistic locking is enabled, we can take advantage that this is an // increment/decrement operation and send it here as well. We put `-1` for // decrementing because it will be subtracted, getting `-(-1)` which is `+1` - if (this._versionAttribute) { - incrementAmountsByField[this._versionAttribute] = isSubtraction ? -1 : 1; + if (modelDefinition.versionAttributeName) { + incrementAmountsByField[modelDefinition.versionAttributeName] = isSubtraction ? -1 : 1; } const extraAttributesToBeUpdated = {}; - const updatedAtAttr = this._timestampAttributes.updatedAt; - if (!options.silent && updatedAtAttr && !incrementAmountsByField[updatedAtAttr]) { - const attrName = this.rawAttributes[updatedAtAttr].field || updatedAtAttr; - extraAttributesToBeUpdated[attrName] = this._getDefaultTimestamp(updatedAtAttr) || new Date(); + const updatedAtAttrName = modelDefinition.timestampAttributeNames.updatedAt; + if (!options.silent && updatedAtAttrName && !incrementAmountsByField[updatedAtAttrName]) { + const columnName = modelDefinition.getColumnName(updatedAtAttrName); + extraAttributesToBeUpdated[columnName] = this._getDefaultTimestamp(updatedAtAttrName) || new Date(); } const tableName = this.getTableName(options); @@ -3594,12 +3030,15 @@ Instead of specifying a Model, either: * Returns a Where Object that can be used to uniquely select this instance, using the instance's primary keys. * * @param {boolean} [checkVersion=false] include version attribute in where hash - * @param {boolean} [nullIfImpossible=false] return null instead of throwing an error if the instance is missing its primary keys and therefore no Where object can be built. + * @param {boolean} [nullIfImpossible=false] return null instead of throwing an error if the instance is missing its + * primary keys and therefore no Where object can be built. * * @returns {object} */ where(checkVersion, nullIfImpossible) { - if (this.constructor.primaryKeyAttributes.length === 0) { + const modelDefinition = this.constructor.modelDefinition; + + if (modelDefinition.primaryKeysAttributeNames.size === 0) { if (nullIfImpossible) { return null; } @@ -3616,20 +3055,20 @@ Instead of specifying a Model, either: const where = {}; - for (const attribute of this.constructor.primaryKeyAttributes) { - const attrVal = this.get(attribute, { raw: true }); + for (const attributeName of modelDefinition.primaryKeysAttributeNames) { + const attrVal = this.get(attributeName, { raw: true }); if (attrVal == null) { if (nullIfImpossible) { return null; } - throw new TypeError(`This model instance method needs to be able to identify the entity in a stable way, but this model instance is missing the value of its primary key "${attribute}". Make sure that attribute was not excluded when retrieving the model from the database.`); + throw new TypeError(`This model instance method needs to be able to identify the entity in a stable way, but this model instance is missing the value of its primary key "${attributeName}". Make sure that attribute was not excluded when retrieving the model from the database.`); } - where[attribute] = attrVal; + where[attributeName] = attrVal; } - const versionAttr = this.constructor._versionAttribute; + const versionAttr = modelDefinition.versionAttributeName; if (checkVersion && versionAttr) { where[versionAttr] = this.get(versionAttr, { raw: true }); } @@ -3678,68 +3117,65 @@ Instead of specifying a Model, either: * If key is given and a field or virtual getter is present for the key it will call that getter - else it will return the * value for key. * - * @param {string} [key] key to get value of + * @param {string} [attributeName] key to get value of * @param {object} [options] get options * * @returns {object|any} */ - get(key, options) { - if (options === undefined && typeof key === 'object') { - options = key; - key = undefined; + get(attributeName, options) { + if (options === undefined && typeof attributeName === 'object') { + options = attributeName; + attributeName = undefined; } options = options || {}; - if (key) { - if (Object.prototype.hasOwnProperty.call(this._customGetters, key) && !options.raw) { - return this._customGetters[key].call(this, key, options); + const { attributes, attributesWithGetters } = this.constructor.modelDefinition; + + if (attributeName) { + const attribute = attributes.get(attributeName); + if (attribute?.get && !options.raw) { + return attribute.get.call(this, attributeName, options); } - if (options.plain && this._options.include && this._options.includeNames.includes(key)) { - if (Array.isArray(this.dataValues[key])) { - return this.dataValues[key].map(instance => instance.get(options)); + if (options.plain && this._options.include && this._options.includeNames.includes(attributeName)) { + if (Array.isArray(this.dataValues[attributeName])) { + return this.dataValues[attributeName].map(instance => instance.get(options)); } - if (this.dataValues[key] instanceof Model) { - return this.dataValues[key].get(options); + if (this.dataValues[attributeName] instanceof Model) { + return this.dataValues[attributeName].get(options); } - return this.dataValues[key]; + return this.dataValues[attributeName]; } - return this.dataValues[key]; + return this.dataValues[attributeName]; } + // TODO: move to its own method instead of overloading. if ( - this._hasCustomGetters + attributesWithGetters.size > 0 || options.plain && this._options.include || options.clone ) { - const values = {}; - let _key; - - if (this._hasCustomGetters) { - for (_key in this._customGetters) { - if ( - this._options.attributes - && !this._options.attributes.includes(_key) - ) { + const values = Object.create(null); + if (attributesWithGetters.size > 0) { + for (const attributeName2 of attributesWithGetters) { + if (!this._options.attributes?.includes(attributeName2)) { continue; } - if (Object.prototype.hasOwnProperty.call(this._customGetters, _key)) { - values[_key] = this.get(_key, options); - } + values[attributeName2] = this.get(attributeName2, options); } } - for (_key in this.dataValues) { + for (const attributeName2 in this.dataValues) { if ( - !Object.prototype.hasOwnProperty.call(values, _key) - && Object.prototype.hasOwnProperty.call(this.dataValues, _key) + !Object.prototype.hasOwnProperty.call(values, attributeName2) + && Object.prototype.hasOwnProperty.call(this.dataValues, attributeName2) ) { - values[_key] = this.get(_key, options); + values[attributeName2] = this.get(attributeName2, options); } } @@ -3779,6 +3215,8 @@ Instead of specifying a Model, either: let values; let originalValue; + const modelDefinition = this.constructor.modelDefinition; + if (typeof key === 'object' && key !== null) { values = key; options = value || {}; @@ -3790,8 +3228,11 @@ Instead of specifying a Model, either: } } + const hasDateAttributes = modelDefinition.dateAttributeNames.size > 0; + const hasBooleanAttributes = modelDefinition.booleanAttributeNames.size > 0; + // If raw, and we're not dealing with includes or special attributes, just set it straight on the dataValues object - if (options.raw && !(this._options && this._options.include) && !(options && options.attributes) && !this.constructor._hasDateAttributes && !this.constructor._hasBooleanAttributes) { + if (options.raw && !(this._options && this._options.include) && !(options && options.attributes) && !hasDateAttributes && !hasBooleanAttributes) { if (Object.keys(this.dataValues).length > 0) { Object.assign(this.dataValues, values); } else { @@ -3814,8 +3255,10 @@ Instead of specifying a Model, either: }; setKeys(options.attributes); - if (this.constructor._hasVirtualAttributes) { - setKeys(this.constructor._virtualAttributes); + + const virtualAttributes = modelDefinition.virtualAttributeNames; + if (virtualAttributes.size > 0) { + setKeys(virtualAttributes); } if (this._options.includeNames) { @@ -3844,9 +3287,11 @@ Instead of specifying a Model, either: originalValue = this.dataValues[key]; } + const attributeDefinition = modelDefinition.attributes.get(key); + // If not raw, and there's a custom setter - if (!options.raw && this._customSetters[key]) { - this._customSetters[key].call(this, value, key); + if (!options.raw && attributeDefinition?.set) { + attributeDefinition.set.call(this, value, key); // custom setter should have changed value, get that changed value // TODO: v5 make setters return new value instead of changing internal store const newValue = this.dataValues[key]; @@ -3866,8 +3311,10 @@ Instead of specifying a Model, either: // Bunch of stuff we won't do when it's raw if (!options.raw) { // If attribute is not in model definition, return - if (!this._isAttribute(key)) { - if (key.includes('.') && this.constructor._jsonAttributes.has(key.split('.')[0])) { + if (!attributeDefinition) { + const jsonAttributeNames = modelDefinition.jsonAttributeNames; + + if (key.includes('.') && jsonAttributeNames.has(key.split('.')[0])) { const previousNestedValue = Dottie.get(this.dataValues, key); if (!_.isEqual(previousNestedValue, value)) { Dottie.set(this.dataValues, key, value); @@ -3879,18 +3326,20 @@ Instead of specifying a Model, either: } // If attempting to set primary key and primary key is already defined, return - if (this.constructor._hasPrimaryKeys && originalValue && this.constructor._isPrimaryKey(key)) { + const primaryKeyNames = modelDefinition.primaryKeysAttributeNames; + if (originalValue && primaryKeyNames.has(key)) { return this; } // If attempting to set read only attributes, return - if (!this.isNewRecord && this.constructor._hasReadOnlyAttributes && this.constructor._readOnlyAttributes.has(key)) { + const readOnlyAttributeNames = modelDefinition.readOnlyAttributeNames; + if (!this.isNewRecord && readOnlyAttributeNames.has(key)) { return this; } } // If there's a data type sanitizer - const attributeType = this.rawAttributes[key]?.type; + const attributeType = attributeDefinition?.type; if ( !options.comesFromDatabase && value != null @@ -4067,11 +3516,13 @@ Instead of specifying a Model, either: setTransactionFromAls(options, this.sequelize); + const modelDefinition = this.constructor.modelDefinition; + if (!options.fields) { if (this.isNewRecord) { - options.fields = Object.keys(this.constructor.rawAttributes); + options.fields = Array.from(modelDefinition.attributes.keys()); } else { - options.fields = _.intersection(this.changed(), Object.keys(this.constructor.rawAttributes)); + options.fields = _.intersection(this.changed(), Array.from(modelDefinition.attributes.keys())); } options.defaultFields = options.fields; @@ -4085,14 +3536,15 @@ Instead of specifying a Model, either: } } + // TODO: use modelDefinition.primaryKeyAttributes (plural!) const primaryKeyName = this.constructor.primaryKeyAttribute; - const primaryKeyAttribute = primaryKeyName && this.constructor.rawAttributes[primaryKeyName]; - const createdAtAttr = this.constructor._timestampAttributes.createdAt; - const versionAttr = this.constructor._versionAttribute; + const primaryKeyAttribute = primaryKeyName && modelDefinition.attributes.get(primaryKeyName); + const createdAtAttr = modelDefinition.timestampAttributeNames.createdAt; + const versionAttr = modelDefinition.versionAttributeName; const hook = this.isNewRecord ? 'Create' : 'Update'; const wasNewRecord = this.isNewRecord; const now = new Date(); - let updatedAtAttr = this.constructor._timestampAttributes.updatedAt; + let updatedAtAttr = modelDefinition.timestampAttributeNames.updatedAt; if (updatedAtAttr && options.fields.length > 0 && !options.fields.includes(updatedAtAttr)) { options.fields.push(updatedAtAttr); @@ -4133,8 +3585,10 @@ Instead of specifying a Model, either: // Db2 does not allow NULL values for unique columns. // Add dummy values if not provided by test case or user. if (this.sequelize.options.dialect === 'db2' && this.isNewRecord) { + // TODO: remove. This is fishy and is going to be a source of bugs (because it replaces null values with arbitrary values that could be actual data). + // If DB2 doesn't support NULL in unique columns, then it should error if the user tries to insert NULL in one. this.uniqno = this.sequelize.dialect.queryGenerator.addUniqueFields( - this.dataValues, this.constructor.rawAttributes, this.uniqno, + this.dataValues, modelDefinition.rawAttributes, this.uniqno, ); } @@ -4172,7 +3626,7 @@ Instead of specifying a Model, either: if (hookChanged && options.validate) { // Validate again - options.skip = _.difference(Object.keys(this.constructor.rawAttributes), hookChanged); + options.skip = _.difference(Array.from(modelDefinition.attributes.keys()), hookChanged); await this.validate(options); delete options.skip; } @@ -4200,12 +3654,12 @@ Instead of specifying a Model, either: })); } - const realFields = options.fields.filter(field => !this.constructor._virtualAttributes.has(field)); + const realFields = options.fields.filter(attributeName => !modelDefinition.virtualAttributeNames.has(attributeName)); if (realFields.length === 0) { return this; } - const versionFieldName = _.get(this.constructor.rawAttributes[versionAttr], 'field') || versionAttr; + const versionColumnName = versionAttr && modelDefinition.getColumnName(versionAttr); const values = mapValueFieldNames(this.dataValues, options.fields, this.constructor); let query; let args; @@ -4214,7 +3668,7 @@ Instead of specifying a Model, either: if (!this.isNewRecord) { where = this.where(true); if (versionAttr) { - values[versionFieldName] = Number.parseInt(values[versionFieldName], 10) + 1; + values[versionColumnName] = Number.parseInt(values[versionColumnName], 10) + 1; } query = 'update'; @@ -4241,18 +3695,19 @@ Instead of specifying a Model, either: where, }); } else { - result.dataValues[versionAttr] = values[versionFieldName]; + result.dataValues[versionAttr] = values[versionColumnName]; } } // Transfer database generated values (defaults, autoincrement, etc) - for (const attr of Object.keys(this.constructor.rawAttributes)) { - if (this.constructor.rawAttributes[attr].field - && values[this.constructor.rawAttributes[attr].field] !== undefined - && this.constructor.rawAttributes[attr].field !== attr + for (const attribute of modelDefinition.attributes.values()) { + if (attribute.columnName + && values[attribute.columnName] !== undefined + && attribute.columnName !== attribute.attributeName ) { - values[attr] = values[this.constructor.rawAttributes[attr].field]; - delete values[this.constructor.rawAttributes[attr].field]; + values[attribute.attributeName] = values[attribute.columnName]; + // TODO: if a column uses the same name as an attribute, this will break! + delete values[attribute.columnName]; } } @@ -4293,16 +3748,20 @@ Instead of specifying a Model, either: ...include.association.through.scope, }; - if (instance[include.association.through.model.name]) { - for (const attr of Object.keys(include.association.through.model.rawAttributes)) { - if (include.association.through.model.rawAttributes[attr]._autoGenerated - || attr === include.association.foreignKey - || attr === include.association.otherKey - || typeof instance[include.association.through.model.name][attr] === 'undefined') { + const throughModel = include.association.through.model; + if (instance[throughModel.name]) { + const throughDefinition = throughModel.modelDefinition; + for (const attribute of throughDefinition.attributes.values()) { + const { attributeName } = attribute; + + if (attribute._autoGenerated + || attributeName === include.association.foreignKey + || attributeName === include.association.otherKey + || typeof instance[throughModel.name][attributeName] === 'undefined') { continue; } - values0[attr] = instance[include.association.through.model.name][attr]; + values0[attributeName] = instance[throughModel.name][attributeName]; } } @@ -4344,11 +3803,11 @@ Instead of specifying a Model, either: * @returns {Promise} */ async reload(options) { - options = defaults({ - where: this.where(), - }, options, { - include: this._options.include || undefined, - }); + options = defaults( + { where: this.where() }, + options, + { include: this._options.include || undefined }, + ); const reloaded = await this.constructor.findOne(options); if (!reloaded) { @@ -4362,7 +3821,7 @@ Instead of specifying a Model, either: // re-set instance values this.set(reloaded.dataValues, { raw: true, - reset: true && !options.attributes, + reset: !options.attributes, }); return this; @@ -4438,6 +3897,8 @@ Instead of specifying a Model, either: setTransactionFromAls(options, this.sequelize); + const modelDefinition = this.constructor.modelDefinition; + // Run before hook if (options.hooks) { await this.constructor.hooks.runAsync('beforeDestroy', this, options); @@ -4446,12 +3907,10 @@ Instead of specifying a Model, either: const where = this.where(true); let result; - if (this.constructor._timestampAttributes.deletedAt && options.force === false) { - const attributeName = this.constructor._timestampAttributes.deletedAt; - const attribute = this.constructor.rawAttributes[attributeName]; - const defaultValue = Object.prototype.hasOwnProperty.call(attribute, 'defaultValue') - ? attribute.defaultValue - : null; + if (modelDefinition.timestampAttributeNames.deletedAt && options.force === false) { + const attributeName = modelDefinition.timestampAttributeNames.deletedAt; + const attribute = modelDefinition.attributes.get(attributeName); + const defaultValue = attribute.defaultValue ?? null; const currentValue = this.getDataValue(attributeName); const undefinedOrNull = currentValue == null && defaultValue == null; if (undefinedOrNull || _.isEqual(currentValue, defaultValue)) { @@ -4481,13 +3940,16 @@ Instead of specifying a Model, either: * @returns {boolean} */ isSoftDeleted() { - if (!this.constructor._timestampAttributes.deletedAt) { + const modelDefinition = this.constructor.modelDefinition; + + const deletedAtAttributeName = modelDefinition.timestampAttributeNames.deletedAt; + if (!deletedAtAttributeName) { throw new Error('Model is not paranoid'); } - const deletedAtAttribute = this.constructor.rawAttributes[this.constructor._timestampAttributes.deletedAt]; - const defaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; - const deletedAt = this.get(this.constructor._timestampAttributes.deletedAt) || null; + const deletedAtAttribute = modelDefinition.attributes.get(deletedAtAttributeName); + const defaultValue = deletedAtAttribute.defaultValue ?? null; + const deletedAt = this.get(deletedAtAttributeName) || null; const isSet = deletedAt !== defaultValue; return isSet; @@ -4503,7 +3965,10 @@ Instead of specifying a Model, either: * @returns {Promise} */ async restore(options) { - if (!this.constructor._timestampAttributes.deletedAt) { + const modelDefinition = this.constructor.modelDefinition; + const deletedAtAttributeName = modelDefinition.timestampAttributeNames.deletedAt; + + if (!deletedAtAttributeName) { throw new Error('Model is not paranoid'); } @@ -4520,11 +3985,10 @@ Instead of specifying a Model, either: await this.constructor.hooks.runAsync('beforeRestore', this, options); } - const deletedAtCol = this.constructor._timestampAttributes.deletedAt; - const deletedAtAttribute = this.constructor.rawAttributes[deletedAtCol]; - const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; + const deletedAtAttribute = modelDefinition.attributes.get(deletedAtAttributeName); + const deletedAtDefaultValue = deletedAtAttribute.defaultValue ?? null; - this.setDataValue(deletedAtCol, deletedAtDefaultValue); + this.setDataValue(deletedAtAttributeName, deletedAtDefaultValue); const result = await this.save({ ...options, hooks: false, omitNull: false }); // Run after hook if (options.hooks) { @@ -4614,15 +4078,20 @@ Instead of specifying a Model, either: * @returns {boolean} */ equals(other) { - if (!other || !other.constructor) { + if (!other || !(other instanceof Model)) { return false; } - if (!(other instanceof this.constructor)) { + const modelDefinition = this.constructor.modelDefinition; + const otherModelDefinition = this.constructor.modelDefinition; + + if (modelDefinition !== otherModelDefinition) { return false; } - return this.constructor.primaryKeyAttributes.every(attribute => this.get(attribute, { raw: true }) === other.get(attribute, { raw: true })); + return every(modelDefinition.primaryKeysAttributeNames, attribute => { + return this.get(attribute, { raw: true }) === other.get(attribute, { raw: true }); + }); } /** @@ -4636,10 +4105,6 @@ Instead of specifying a Model, either: return others.some(other => this.equals(other)); } - setValidators(attribute, validators) { - this.validators[attribute] = validators; - } - /** * Convert the instance to a JSON representation. * Proxies to calling `get` with no keys. diff --git a/src/sequelize-typescript.ts b/src/sequelize-typescript.ts index 7b4ba59986f9..1054e23085cb 100644 --- a/src/sequelize-typescript.ts +++ b/src/sequelize-typescript.ts @@ -11,8 +11,8 @@ import { } from './hooks-legacy.js'; import type { AsyncHookReturn, HookHandler } from './hooks.js'; import { HookHandlerBuilder } from './hooks.js'; -import type { ModelHooks } from './model-typescript.js'; -import { validModelHooks } from './model-typescript.js'; +import type { ModelHooks } from './model-hooks.js'; +import { validModelHooks } from './model-hooks.js'; import type { ConnectionOptions, Options, Sequelize } from './sequelize.js'; import type { TransactionOptions } from './transaction.js'; import { Transaction } from './transaction.js'; diff --git a/src/sequelize.d.ts b/src/sequelize.d.ts index 5007a9834472..c33a82383aff 100644 --- a/src/sequelize.d.ts +++ b/src/sequelize.d.ts @@ -1,7 +1,7 @@ import type { Options as RetryAsPromisedOptions } from 'retry-as-promised'; import type { AbstractDialect } from './dialects/abstract'; import type { AbstractConnectionManager } from './dialects/abstract/connection-manager'; -import type { AbstractDataType, DataTypeClassOrInstance } from './dialects/abstract/data-types.js'; +import type { AbstractDataType, DataType, DataTypeClassOrInstance } from './dialects/abstract/data-types.js'; import type { AbstractQueryInterface, ColumnsDescription } from './dialects/abstract/query-interface'; import type { CreateSchemaOptions } from './dialects/abstract/query-interface.types'; import type { @@ -9,7 +9,7 @@ import type { DropOptions, Logging, Model, - ModelAttributeColumnOptions, + AttributeOptions, ModelAttributes, ModelOptions, WhereOperators, @@ -258,7 +258,7 @@ export interface Options extends Logging { /** * Default options for model definitions. See Model.init. */ - define?: ModelOptions; + define?: Omit; /** * Default options for sequelize.query @@ -1010,8 +1010,11 @@ export class Sequelize extends SequelizeTypeScript { */ close(): Promise; + normalizeAttribute(attribute: AttributeOptions | DataType): AttributeOptions; + normalizeDataType(Type: string): string; normalizeDataType(Type: DataTypeClassOrInstance): AbstractDataType; + normalizeDataType(Type: string | DataTypeClassOrInstance): string | AbstractDataType; /** * Fetches the database version @@ -1099,7 +1102,7 @@ export function or(...args: T): { [Op.or]: T }; */ export function json(conditionsOrPath: string | object, value?: string | number | boolean): Json; -export type WhereLeftOperand = Fn | ColumnReference | Literal | Cast | ModelAttributeColumnOptions; +export type WhereLeftOperand = Fn | ColumnReference | Literal | Cast | AttributeOptions; /** * A way of specifying "attr = condition". diff --git a/src/sequelize.js b/src/sequelize.js index bfdd585eef2d..43cf7d4499bd 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -282,6 +282,11 @@ export class Sequelize extends SequelizeTypeScript { this.options.dialect = 'postgres'; } + // if (this.options.define.hooks) { + // throw new Error(`The "define" Sequelize option cannot be used to add hooks to all models. Please remove the "hooks" property from the "define" option you passed to the Sequelize constructor. + // Instead of using this option, you can listen to the same event on all models by adding the listener to the Sequelize instance itself, since all model hooks are forwarded to the Sequelize instance.`); + // } + if (this.options.logging === true) { deprecations.noTrueLogging(); this.options.logging = console.debug; @@ -485,7 +490,7 @@ export class Sequelize extends SequelizeTypeScript { * * sequelize.models.modelName // The model will now be available in models under the name given to define */ - define(modelName, attributes, options = {}) { + define(modelName, attributes = {}, options = {}) { options.modelName = modelName; options.sequelize = this; @@ -646,7 +651,8 @@ Use Sequelize#query if you wish to use replacements.`); // map raw fields to model attributes if (options.mapToModel) { - options.fieldMap = _.get(options, 'model.fieldAttributeMap', {}); + // TODO: throw if model is not specified + options.fieldMap = options.model?.fieldAttributeMap; } options = _.defaults(options, { @@ -1161,10 +1167,8 @@ Use Sequelize#query if you wish to use replacements.`); normalizeAttribute(attribute) { if (!_.isPlainObject(attribute)) { attribute = { type: attribute }; - } - - if (!attribute.type) { - return attribute; + } else { + attribute = { ...attribute }; } if (attribute.values) { @@ -1190,14 +1194,12 @@ Remove the "values" property to resolve this issue. `.trim()); } - attribute.type = this.normalizeDataType(attribute.type); - - if (Object.prototype.hasOwnProperty.call(attribute, 'defaultValue') && typeof attribute.defaultValue === 'function' - && [DataTypes.NOW, DataTypes.UUIDV1, DataTypes.UUIDV4].includes(attribute.defaultValue) - ) { - attribute.defaultValue = new attribute.defaultValue(); + if (!attribute.type) { + return attribute; } + attribute.type = this.normalizeDataType(attribute.type); + return attribute; } } diff --git a/src/utils/deprecations.ts b/src/utils/deprecations.ts index 2ee96e5a4ce5..d658acb02ad6 100644 --- a/src/utils/deprecations.ts +++ b/src/utils/deprecations.ts @@ -19,3 +19,5 @@ export const doNotUseRealDataType = deprecate(noop, 'Sequelize 7 has normalized export const noSchemaParameter = deprecate(noop, 'The schema parameter in QueryInterface#describeTable has been deprecated, use a TableNameWithSchema object to specify the schema or set the schema globally in the options.', 'SEQUELIZE0015'); export const noSchemaDelimiterParameter = deprecate(noop, 'The schemaDelimiter parameter in QueryInterface#describeTable has been deprecated, use a TableNameWithSchema object to specify the schemaDelimiter.', 'SEQUELIZE0016'); export const columnToAttribute = deprecate(noop, 'The @Column decorator has been renamed to @Attribute.', 'SEQUELIZE0017'); +export const fieldToColumn = deprecate(noop, 'The "field" option in attribute definitions has been renamed to "columnName".', 'SEQUELIZE0018'); +export const noModelTableName = deprecate(noop, 'Model.tableName has been replaced with the more complete Model.modelDefinition.table, or Model.table', 'SEQUELIZE0019'); diff --git a/src/utils/format.ts b/src/utils/format.ts index 81958f5d4cc2..6520b6c96852 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -3,13 +3,14 @@ import forIn from 'lodash/forIn'; import isPlainObject from 'lodash/isPlainObject'; import type { Attributes, - BuiltModelAttributeColumnOptions, + NormalizedAttributeOptions, Model, ModelStatic, WhereOptions, } from '..'; import * as DataTypes from '../data-types'; import { Op as operators } from '../operators'; +import { isString } from './check.js'; const operatorsSet = new Set(Object.values(operators)); @@ -39,8 +40,9 @@ export function mapFinderOptions !Model._virtualAttributes.has(v), + attributeName => !modelDefinition.virtualAttributeNames.has(attributeName), ); } @@ -59,7 +61,7 @@ export function mapFinderOptions( options: FinderOptions>, - Model: ModelStatic, + Model: ModelStatic, ): MappedFinderOptions> { // note: parts of Sequelize rely on this function mutating its inputs. @@ -76,8 +78,8 @@ export function mapOptionFieldNames( } // Map attributes to column names - const columnName: string | undefined = Model.rawAttributes[attributeName]?.field; - if (columnName && columnName !== attributeName) { + const columnName: string = Model.modelDefinition.getColumnNameLoose(attributeName); + if (columnName !== attributeName) { return [columnName, attributeName]; } @@ -99,10 +101,13 @@ export function mapWhereFieldNames(where: Record, Model: Model return where; } + const modelDefinition = Model.modelDefinition; + const newWhere: Record = Object.create(null); - // TODO: note on 'as any[]'; removing the cast causes the following error on attributeNameOrOperator "TS2538: Type 'symbol' cannot be used as an index type." - for (const attributeNameOrOperator of getComplexKeys(where) as any[]) { - const rawAttribute: BuiltModelAttributeColumnOptions | undefined = Model.rawAttributes[attributeNameOrOperator]; + for (const attributeNameOrOperator of getComplexKeys(where)) { + const rawAttribute: NormalizedAttributeOptions | undefined = isString(attributeNameOrOperator) + ? modelDefinition.attributes.get(attributeNameOrOperator) + : undefined; const columnNameOrOperator: PropertyKey = rawAttribute?.field ?? attributeNameOrOperator; @@ -194,15 +199,16 @@ export function combineTableNames(tableName1: string, tableName2: string): strin */ export function mapValueFieldNames( // TODO: rename to mapAttributesToColumNames? See https://github.com/sequelize/meetings/issues/17 dataValues: Record, - attributeNames: string[], - ModelClass: ModelStatic, + attributeNames: Iterable, + ModelClass: ModelStatic, ): Record { const values: Record = Object.create(null); + const modelDefinition = ModelClass.modelDefinition; for (const attributeName of attributeNames) { - if (dataValues[attributeName] !== undefined && !ModelClass._virtualAttributes.has(attributeName)) { + if (dataValues[attributeName] !== undefined && !modelDefinition.virtualAttributeNames.has(attributeName)) { // Field name mapping - const columnName = ModelClass.rawAttributes[attributeName]?.field ?? attributeName; + const columnName = modelDefinition.getColumnNameLoose(attributeName); values[columnName] = dataValues[attributeName]; } @@ -251,7 +257,7 @@ export function removeNullishValuesFromHash( return result; } -export function getColumnName(attribute: BuiltModelAttributeColumnOptions): string { +export function getColumnName(attribute: NormalizedAttributeOptions): string { assert(attribute.fieldName != null, 'getColumnName expects a normalized attribute meta'); // field is the column name alias diff --git a/src/utils/immutability.ts b/src/utils/immutability.ts new file mode 100644 index 000000000000..bba023c3f568 --- /dev/null +++ b/src/utils/immutability.ts @@ -0,0 +1,106 @@ +import NodeUtil from 'node:util'; +import type { InspectOptions } from 'node:util'; + +export class SetView { + #target: Set; + + constructor(target: Set) { + this.#target = target; + } + + /** + * @param value + * @returns a boolean indicating whether an element with the specified value exists in the Set or not. + */ + has(value: V): boolean { + return this.#target.has(value); + } + + /** + * @returns the number of (unique) elements in Set. + */ + get size() { + return this.#target.size; + } + + [Symbol.iterator](): IterableIterator { + return this.#target[Symbol.iterator](); + } + + values(): IterableIterator { + return this.#target.values(); + } + + toJSON() { + return [...this.#target]; + } + + [NodeUtil.inspect.custom](depth: number, options: InspectOptions): string { + const newOptions = Object.assign({}, options, { + depth: options.depth == null ? null : options.depth - 1, + }); + + return NodeUtil.inspect(this.#target, newOptions).replace(/^Set/, 'SetView'); + } +} + +export class MapView { + #target: Map; + + constructor(target: Map) { + this.#target = target; + } + + /** + * Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map. + * + * @param key + * @returns Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned. + */ + get(key: K): V | undefined { + return this.#target.get(key); + } + + /** + * @param key + * @returns boolean indicating whether an element with the specified key exists or not. + */ + has(key: K): boolean { + return this.#target.has(key); + } + + /** + * @returns the number of elements in the Map. + */ + get size(): number { + return this.#target.size; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.#target[Symbol.iterator](); + } + + entries(): IterableIterator<[K, V]> { + return this.#target.entries(); + } + + keys(): IterableIterator { + return this.#target.keys(); + } + + values(): IterableIterator { + return this.#target.values(); + } + + toJSON() { + return [...this.#target.entries()]; + } + + [NodeUtil.inspect.custom](depth: number, options: InspectOptions): string { + const newOptions = Object.assign({}, options, { + depth: options.depth == null ? null : options.depth - 1, + }); + + return NodeUtil.inspect(this.#target, newOptions).replace(/^Map/, 'MapView'); + } +} diff --git a/src/utils/iterators.ts b/src/utils/iterators.ts index 8a936cdbba30..de5c40bb44e5 100644 --- a/src/utils/iterators.ts +++ b/src/utils/iterators.ts @@ -16,6 +16,42 @@ export function *map( } } +export function some( + iterable: Iterable, + cb: (item: In) => boolean, +): boolean { + for (const item of iterable) { + if (cb(item)) { + return true; + } + } + + return false; +} + +export function every( + iterable: Iterable, + cb: (item: In) => boolean, +): boolean { + for (const item of iterable) { + if (!cb(item)) { + return false; + } + } + + return true; +} + +export function find(iterable: Iterable, cb: (item: Val) => boolean): Val | undefined { + for (const item of iterable) { + if (cb(item)) { + return item; + } + } + + return undefined; +} + /** * Combines two iterables, they will be iterated in order * diff --git a/src/utils/model-utils.ts b/src/utils/model-utils.ts index 59f04a0cd5b6..88b350acdea3 100644 --- a/src/utils/model-utils.ts +++ b/src/utils/model-utils.ts @@ -1,5 +1,4 @@ -import type { ModelStatic } from '../model'; -import { Model } from '../model'; +import type { ModelStatic, Model } from '../model'; /** * Returns true if the value is a model subclass. @@ -7,7 +6,10 @@ import { Model } from '../model'; * @param val The value whose type will be checked */ export function isModelStatic(val: any): val is ModelStatic { - return typeof val === 'function' && val.prototype instanceof Model; + // TODO: temporary workaround due to cyclic import. Should not be necessary once Model is fully migrated to TypeScript. + const { Model: TmpModel } = require('../model'); + + return typeof val === 'function' && val.prototype instanceof TmpModel; } /** diff --git a/src/utils/object.ts b/src/utils/object.ts index 3641b632e799..19503f8af292 100644 --- a/src/utils/object.ts +++ b/src/utils/object.ts @@ -8,6 +8,7 @@ import isUndefined from 'lodash/isUndefined.js'; import mergeWith from 'lodash/mergeWith'; import omitBy from 'lodash/omitBy.js'; import { getComplexKeys } from './format'; +import type { MapView } from './immutability.js'; import { combinedIterator, map } from './iterators.js'; // eslint-disable-next-line import/order -- caused by temporarily mixing require with import import { camelize } from './string'; @@ -36,7 +37,7 @@ export function mergeDefaults(a: T, b: Partial): T { return objectValue; } - // eslint-disable-next-line consistent-return,no-useless-return -- lodash actually wants us to return `undefined` to fallback to the default customizer. + // eslint-disable-next-line no-useless-return -- lodash actually wants us to return `undefined` to fallback to the default customizer. return; }); } @@ -75,7 +76,6 @@ export function merge(...args: object[]): object { return result; } -/* eslint-disable consistent-return -- lodash actually wants us to return `undefined` to fallback to the default customizer. */ export function cloneDeep(obj: T, onlyPlain?: boolean): T { return cloneDeepWith(obj || {}, elem => { // Do not try to customize cloning of arrays or POJOs @@ -221,6 +221,16 @@ export function removeUndefined(val: T): NoUndefinedField { return omitBy(val, isUndefined) as NoUndefinedField; } +export function getObjectFromMap(aMap: Map | MapView): Record { + const record = Object.create(null); + + for (const key of aMap.keys()) { + record[key] = aMap.get(key); + } + + return record; +} + /** * Returns all own keys of an object, including non-enumerable ones and symbols. * @@ -235,7 +245,18 @@ export function getAllOwnKeys(object: object): IterableIterator /** * Returns all own entries of an object, including non-enumerable ones and symbols. + * + * @param obj */ -export function getAllOwnEntries(object: { [s: PropertyKey]: T }): IterableIterator<[key: string | symbol, value: T]> { - return map(getAllOwnKeys(object), key => [key, object[key]]); +export function getAllOwnEntries(obj: { [s: PropertyKey]: T }): IterableIterator<[key: string | symbol, value: T]>; +export function getAllOwnEntries(obj: object): IterableIterator<[key: string | symbol, value: unknown]>; +export function getAllOwnEntries(obj: object): IterableIterator<[key: string | symbol, value: unknown]> { + // @ts-expect-error -- obj[key] is implicitly any + return map(getAllOwnKeys(obj), key => [key, obj[key]]); +} + +export function noPrototype(obj: T): T { + Object.setPrototypeOf(obj, null); + + return obj; } diff --git a/src/utils/types.ts b/src/utils/types.ts index 51b9a4b3b5c7..a15584c7cf0c 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -69,6 +69,8 @@ export type NonUndefined = T extends undefined ? never : T; export type AllowArray = T | T[]; +export type AllowLowercase = T | Lowercase; + export type AllowReadonlyArray = T | readonly T[]; export type ConstructorKeys = ({ [P in keyof T]: T[P] extends new () => any ? P : never })[keyof T]; diff --git a/test/integration/als.test.ts b/test/integration/als.test.ts index 0d3cb9b59aa8..3dd440fd410b 100644 --- a/test/integration/als.test.ts +++ b/test/integration/als.test.ts @@ -3,7 +3,7 @@ import delay from 'delay'; import sinon from 'sinon'; import { DataTypes, QueryTypes, Model } from '@sequelize/core'; import type { ModelStatic, InferAttributes, InferCreationAttributes } from '@sequelize/core'; -import type { ModelHooks } from '../../types/model-typescript.js'; +import type { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-hooks.js'; import { beforeAll2, createSequelizeInstance, disableDatabaseResetForSuite, diff --git a/test/integration/associations/belongs-to-many.test.js b/test/integration/associations/belongs-to-many.test.js index 20f01deff4a1..76864c38bfdb 100644 --- a/test/integration/associations/belongs-to-many.test.js +++ b/test/integration/associations/belongs-to-many.test.js @@ -5,7 +5,6 @@ const chai = require('chai'); const expect = chai.expect; const Support = require('../support'); const { DataTypes, Sequelize, Op } = require('@sequelize/core'); -const omit = require('lodash/omit'); const assert = require('node:assert'); const sinon = require('sinon'); const { resetSequelizeInstance } = require('../../support'); @@ -71,8 +70,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { it('gets all associated objects with all fields', async function () { const john = await this.User.findOne({ where: { username: 'John' } }); const tasks = await john.getTasks(); - for (const attr of Object.keys(tasks[0].rawAttributes)) { - expect(tasks[0]).to.have.property(attr); + for (const attributeName of this.Task.modelDefinition.attributes.keys()) { + expect(tasks[0]).to.have.property(attributeName); } }); @@ -1744,6 +1743,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { Comment.belongsToMany(Tag, { through: { model: ItemTag, unique: false, scope: { taggable: 'comment' } }, foreignKey: 'taggable_id', + // taggable_id already references Post, we can't make it reference Comment + foreignKeyConstraints: false, }); await this.sequelize.sync({ force: true }); @@ -1797,6 +1798,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { Comment.belongsToMany(Tag, { through: { model: ItemTag, unique: false, scope: { taggable: 'comment' } }, foreignKey: 'taggable_id', + // taggable_id already references Post, we can't make it reference Comment + foreignKeyConstraints: false, }); await this.sequelize.sync({ force: true }); @@ -2381,7 +2384,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { User.belongsToMany(Place, { through: 'user_places' }); Place.belongsToMany(User, { through: 'user_places' }); - const attributes = this.sequelize.model('user_places').rawAttributes; + const attributes = this.sequelize.model('user_places').getAttributes(); expect(attributes.PlaceId.field).to.equal('PlaceId'); expect(attributes.UserId.field).to.equal('UserId'); @@ -2580,12 +2583,16 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { describe('primary key handling for join table', () => { beforeEach(function () { - this.User = this.sequelize.define('User', + this.User = this.sequelize.define( + 'User', { username: DataTypes.STRING }, - { tableName: 'users' }); - this.Task = this.sequelize.define('Task', + { tableName: 'users' }, + ); + this.Task = this.sequelize.define( + 'Task', { title: DataTypes.STRING }, - { tableName: 'tasks' }); + { tableName: 'tasks' }, + ); }); it('removes the primary key if it was added by sequelize', function () { @@ -2598,8 +2605,6 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { }); it('keeps the primary key if it was added by the user', function () { - let fk; - this.UserTasks = this.sequelize.define('UserTask', { id: { type: DataTypes.INTEGER, @@ -2622,8 +2627,9 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { expect(Object.keys(this.UserTasks2.primaryKeys)).to.deep.equal(['userTasksId']); for (const model of [this.UserTasks, this.UserTasks2]) { - fk = Object.keys(model.uniqueKeys)[0]; - expect(model.uniqueKeys[fk].fields.sort()).to.deep.equal(['TaskId', 'UserId']); + const index = model.getIndexes()[0]; + + expect(index.fields.sort()).to.deep.equal(['TaskId', 'UserId']); } }); @@ -2634,20 +2640,28 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, - }, username: DataTypes.STRING, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE, + }, + username: DataTypes.STRING, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }); await this.sequelize.queryInterface.createTable('tasks', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, - }, title: DataTypes.STRING, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE, + }, + title: DataTypes.STRING, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }); - return this.sequelize.queryInterface.createTable( - 'users_tasks', - { TaskId: DataTypes.INTEGER, UserId: DataTypes.INTEGER, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE }, - ); + return this.sequelize.queryInterface.createTable('users_tasks', { + TaskId: DataTypes.INTEGER, + UserId: DataTypes.INTEGER, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }); }); it('removes all associations', async function () { @@ -3122,8 +3136,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { Group.belongsToMany(User, { as: 'MyUsers', through: 'group_user' }); expect(Group.associations.MyUsers.through.model === User.associations.MyGroups.through.model); - expect(Group.associations.MyUsers.through.model.rawAttributes.UserId).to.exist; - expect(Group.associations.MyUsers.through.model.rawAttributes.GroupId).to.exist; + expect(Group.associations.MyUsers.through.model.getAttributes().UserId).to.exist; + expect(Group.associations.MyUsers.through.model.getAttributes().GroupId).to.exist; }); it('correctly identifies its counterpart when through is a model', function () { @@ -3136,8 +3150,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { expect(Group.associations.MyUsers.through.model === User.associations.MyGroups.through.model); - expect(Group.associations.MyUsers.through.model.rawAttributes.UserId).to.exist; - expect(Group.associations.MyUsers.through.model.rawAttributes.GroupId).to.exist; + expect(Group.associations.MyUsers.through.model.getAttributes().UserId).to.exist; + expect(Group.associations.MyUsers.through.model.getAttributes().GroupId).to.exist; }); }); @@ -3290,8 +3304,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { this.User.belongsToMany(this.Task, { foreignKeyConstraints: false, through: 'tasksusers', inverse: { foreignKeyConstraints: false } }); const Through = this.sequelize.model('tasksusers'); - expect(Through.rawAttributes.taskId.references).to.eq(undefined, 'Attribute taskId should not be a foreign key'); - expect(Through.rawAttributes.userId.references).to.eq(undefined, 'Attribute userId should not be a foreign key'); + expect(Through.getAttributes().taskId.references).to.eq(undefined, 'Attribute taskId should not be a foreign key'); + expect(Through.getAttributes().userId.references).to.eq(undefined, 'Attribute userId should not be a foreign key'); await this.sequelize.sync({ force: true }); @@ -3337,13 +3351,13 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { foreignKey: { name: 'user_id', defaultValue: 42 }, through: 'UserProjects', }); - expect(UserProjects.through.model.rawAttributes.user_id).to.be.ok; - const targetTable = UserProjects.through.model.rawAttributes.user_id.references.model; + expect(UserProjects.through.model.getAttributes().user_id).to.be.ok; + const targetTable = UserProjects.through.model.getAttributes().user_id.references.table; assert(typeof targetTable === 'object'); - expect(omit(targetTable, 'toString')).to.deep.equal(omit(User.getTableName(), 'toString')); - expect(UserProjects.through.model.rawAttributes.user_id.references.key).to.equal('uid'); - expect(UserProjects.through.model.rawAttributes.user_id.defaultValue).to.equal(42); + expect(targetTable).to.deep.equal(User.table); + expect(UserProjects.through.model.getAttributes().user_id.references.key).to.equal('uid'); + expect(UserProjects.through.model.getAttributes().user_id.defaultValue).to.equal(42); }); }); diff --git a/test/integration/associations/belongs-to.test.js b/test/integration/associations/belongs-to.test.js index 4f429d13b077..c4894f7f3392 100644 --- a/test/integration/associations/belongs-to.test.js +++ b/test/integration/associations/belongs-to.test.js @@ -6,7 +6,6 @@ const sinon = require('sinon'); const expect = chai.expect; const Support = require('../support'); const { DataTypes, Sequelize } = require('@sequelize/core'); -const omit = require('lodash/omit'); const assert = require('node:assert'); const current = Support.sequelize; @@ -374,8 +373,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { User.belongsTo(Account); - expect(User.rawAttributes.AccountId).to.exist; - expect(User.rawAttributes.AccountId.field).to.equal('account_id'); + expect(User.getAttributes().AccountId).to.exist; + expect(User.getAttributes().AccountId.field).to.equal('account_id'); }); it('should use model name when using camelcase', function () { @@ -384,8 +383,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { User.belongsTo(Account); - expect(User.rawAttributes.AccountId).to.exist; - expect(User.rawAttributes.AccountId.field).to.equal('AccountId'); + expect(User.getAttributes().AccountId).to.exist; + expect(User.getAttributes().AccountId.field).to.equal('AccountId'); }); it('should support specifying the field of a foreign key', async function () { @@ -399,8 +398,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { }, }); - expect(User.rawAttributes.AccountId).to.exist; - expect(User.rawAttributes.AccountId.field).to.equal('account_id'); + expect(User.getAttributes().AccountId).to.exist; + expect(User.getAttributes().AccountId.field).to.equal('account_id'); await Account.sync({ force: true }); // Can't use Promise.all cause of foreign key references @@ -660,7 +659,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { User.belongsTo(Group); await this.sequelize.sync({ force: true }); - expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(DataTypes.STRING); + expect(User.getAttributes().GroupPKBTName.type).to.an.instanceof(DataTypes.STRING); }); it('should support a non-primary key as the association column on a target without a primary key', async function () { @@ -794,7 +793,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { await this.sequelize.sync({ force: true }); for (const dataType of dataTypes) { - expect(Tasks[dataType].rawAttributes.userId.type).to.be.an.instanceof(dataType); + expect(Tasks[dataType].getAttributes().userId.type).to.be.an.instanceof(dataType); } }); @@ -810,14 +809,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { }, }); - expect(Task.rawAttributes.uid).to.be.ok; - expect(Task.rawAttributes.uid.allowNull).to.be.false; - const targetTable = Task.rawAttributes.uid.references.model; + expect(Task.getAttributes().uid).to.be.ok; + expect(Task.getAttributes().uid.allowNull).to.be.false; + const targetTable = Task.getAttributes().uid.references.table; assert(typeof targetTable === 'object'); - expect(omit(targetTable, 'toString')).to.deep.equal(omit(User.getTableName(), 'toString')); + expect(targetTable).to.deep.equal(User.table); - expect(Task.rawAttributes.uid.references.key).to.equal('id'); + expect(Task.getAttributes().uid.references.key).to.equal('id'); }); it('works when taking a column directly from the object', function () { @@ -834,15 +833,15 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { }, }); - Profile.belongsTo(User, { foreignKey: Profile.rawAttributes.user_id }); + Profile.belongsTo(User, { foreignKey: Profile.getAttributes().user_id }); - expect(Profile.rawAttributes.user_id).to.be.ok; - const targetTable = Profile.rawAttributes.user_id.references.model; + expect(Profile.getAttributes().user_id).to.be.ok; + const targetTable = Profile.getAttributes().user_id.references.table; assert(typeof targetTable === 'object'); - expect(omit(targetTable, 'toString')).to.deep.equal(omit(User.getTableName(), 'toString')); - expect(Profile.rawAttributes.user_id.references.key).to.equal('uid'); - expect(Profile.rawAttributes.user_id.allowNull).to.be.false; + expect(targetTable).to.deep.equal(User.table); + expect(Profile.getAttributes().user_id.references.key).to.equal('uid'); + expect(Profile.getAttributes().user_id.allowNull).to.be.false; }); it('works when merging with an existing definition', function () { @@ -856,9 +855,9 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { Task.belongsTo(Project, { foreignKey: { allowNull: true } }); - expect(Task.rawAttributes.projectId).to.be.ok; - expect(Task.rawAttributes.projectId.defaultValue).to.equal(42); - expect(Task.rawAttributes.projectId.allowNull).to.be.ok; + expect(Task.getAttributes().projectId).to.be.ok; + expect(Task.getAttributes().projectId.defaultValue).to.equal(42); + expect(Task.getAttributes().projectId.allowNull).to.be.ok; }); }); }); diff --git a/test/integration/associations/has-many.test.js b/test/integration/associations/has-many.test.js index 282bbbe16772..c4dc450f9bf6 100644 --- a/test/integration/associations/has-many.test.js +++ b/test/integration/associations/has-many.test.js @@ -10,7 +10,6 @@ const sinon = require('sinon'); const current = Support.sequelize; const _ = require('lodash'); -const omit = require('lodash/omit'); const assert = require('node:assert'); const dialect = Support.getTestDialect(); @@ -499,8 +498,8 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { this.Label.belongsTo(this.Article); this.Article.hasMany(this.Label); - expect(Object.keys(this.Label.rawAttributes)).to.deep.equal(['id', 'text', 'ArticleId']); - expect(Object.keys(this.Label.rawAttributes).length).to.equal(3); + expect(Object.keys(this.Label.getAttributes())).to.deep.equal(['id', 'text', 'ArticleId']); + expect(Object.keys(this.Label.getAttributes()).length).to.equal(3); }); if (current.dialect.supports.transactions) { @@ -1254,8 +1253,8 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { User.hasMany(Account); - expect(Account.rawAttributes.UserId).to.exist; - expect(Account.rawAttributes.UserId.field).to.equal('user_id'); + expect(Account.getAttributes().UserId).to.exist; + expect(Account.getAttributes().UserId.field).to.equal('user_id'); }); it('should use model name when using camelcase', function () { @@ -1264,8 +1263,8 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { User.hasMany(Account); - expect(Account.rawAttributes.UserId).to.exist; - expect(Account.rawAttributes.UserId.field).to.equal('UserId'); + expect(Account.getAttributes().UserId).to.exist; + expect(Account.getAttributes().UserId.field).to.equal('UserId'); }); it('can specify data type for auto-generated relational keys', async function () { @@ -1280,7 +1279,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { User.hasMany(Tasks[dataType], { foreignKey: { name: 'userId', type: dataType }, foreignKeyConstraints: false }); await Tasks[dataType].sync({ force: true }); - expect(Tasks[dataType].rawAttributes.userId.type).to.be.an.instanceof(dataType); + expect(Tasks[dataType].getAttributes().userId.type).to.be.an.instanceof(dataType); } }); @@ -1296,7 +1295,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { User.hasMany(Task); await this.sequelize.sync({ force: true }); - expect(Task.rawAttributes.UserId.type instanceof DataTypes.STRING).to.be.ok; + expect(Task.getAttributes().UserId.type instanceof DataTypes.STRING).to.be.ok; }); describe('allows the user to provide an attribute definition object as foreignKey', () => { @@ -1311,13 +1310,13 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { }, }); - expect(Task.rawAttributes.uid).to.be.ok; - expect(Task.rawAttributes.uid.allowNull).to.be.false; - const targetTable = Task.rawAttributes.uid.references.model; + expect(Task.getAttributes().uid).to.be.ok; + expect(Task.getAttributes().uid.allowNull).to.be.false; + const targetTable = Task.getAttributes().uid.references.table; assert(typeof targetTable === 'object'); - expect(omit(targetTable, 'toString')).to.deep.equal(omit(User.getTableName(), 'toString')); - expect(Task.rawAttributes.uid.references.key).to.equal('id'); + expect(targetTable).to.deep.equal(User.table); + expect(Task.getAttributes().uid.references.key).to.equal('id'); }); it('works when taking a column directly from the object', function () { @@ -1334,15 +1333,15 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { }, }); - User.hasMany(Project, { foreignKey: Project.rawAttributes.user_id }); + User.hasMany(Project, { foreignKey: Project.getAttributes().user_id }); - expect(Project.rawAttributes.user_id).to.be.ok; - const targetTable = Project.rawAttributes.user_id.references.model; + expect(Project.getAttributes().user_id).to.be.ok; + const targetTable = Project.getAttributes().user_id.references.table; assert(typeof targetTable === 'object'); - expect(omit(targetTable, 'toString')).to.deep.equal(omit(User.getTableName(), 'toString')); - expect(Project.rawAttributes.user_id.references.key).to.equal('uid'); - expect(Project.rawAttributes.user_id.defaultValue).to.equal(42); + expect(targetTable).to.deep.equal(User.table); + expect(Project.getAttributes().user_id.references.key).to.equal('uid'); + expect(Project.getAttributes().user_id.defaultValue).to.equal(42); }); it('works when merging with an existing definition', function () { @@ -1356,9 +1355,9 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { User.hasMany(Task, { foreignKey: { allowNull: true } }); - expect(Task.rawAttributes.userId).to.be.ok; - expect(Task.rawAttributes.userId.defaultValue).to.equal(42); - expect(Task.rawAttributes.userId.allowNull).to.be.ok; + expect(Task.getAttributes().userId).to.be.ok; + expect(Task.getAttributes().userId.defaultValue).to.equal(42); + expect(Task.getAttributes().userId.allowNull).to.be.ok; }); }); diff --git a/test/integration/associations/has-one.test.js b/test/integration/associations/has-one.test.js index d395a06a5a6f..74de4fed6687 100644 --- a/test/integration/associations/has-one.test.js +++ b/test/integration/associations/has-one.test.js @@ -300,8 +300,8 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { Account.hasOne(User); - expect(User.rawAttributes.AccountId).to.exist; - expect(User.rawAttributes.AccountId.field).to.equal('account_id'); + expect(User.getAttributes().AccountId).to.exist; + expect(User.getAttributes().AccountId.field).to.equal('account_id'); }); it('should use model name when using camelcase', function () { @@ -310,8 +310,8 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { Account.hasOne(User); - expect(User.rawAttributes.AccountId).to.exist; - expect(User.rawAttributes.AccountId.field).to.equal('AccountId'); + expect(User.getAttributes().AccountId).to.exist; + expect(User.getAttributes().AccountId.field).to.equal('AccountId'); }); it('should support specifying the field of a foreign key', async function () { @@ -325,8 +325,8 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { }, }); - expect(User.rawAttributes.taskId).to.exist; - expect(User.rawAttributes.taskId.field).to.equal('task_id'); + expect(User.getAttributes().taskId).to.exist; + expect(User.getAttributes().taskId.field).to.equal('task_id'); await Task.sync({ force: true }); await User.sync({ force: true }); @@ -526,7 +526,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { Group.hasOne(User); await this.sequelize.sync({ force: true }); - expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(DataTypes.STRING); + expect(User.getAttributes().GroupPKBTName.type).to.an.instanceof(DataTypes.STRING); }); it('should support a non-primary key as the association column on a target with custom primary key', async function () { @@ -616,7 +616,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { User.hasOne(Tasks[dataType], { foreignKey: { name: 'userId', type: dataType }, foreignKeyConstraints: false }); await Tasks[dataType].sync({ force: true }); - expect(Tasks[dataType].rawAttributes.userId.type).to.be.an.instanceof(dataType); + expect(Tasks[dataType].getAttributes().userId.type).to.be.an.instanceof(dataType); })); }); }); @@ -634,9 +634,9 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { }, }); - expect(Object.keys(InternetOrders.rawAttributes).length).to.equal(2); - expect(InternetOrders.rawAttributes.OrderId).to.be.ok; - expect(InternetOrders.rawAttributes.OrdersId).not.to.be.ok; + expect(Object.keys(InternetOrders.getAttributes()).length).to.equal(2); + expect(InternetOrders.getAttributes().OrderId).to.be.ok; + expect(InternetOrders.getAttributes().OrdersId).not.to.be.ok; }); }); }); diff --git a/test/integration/associations/self.test.js b/test/integration/associations/self.test.js index 58a84d5c33a3..ec68447c461e 100644 --- a/test/integration/associations/self.test.js +++ b/test/integration/associations/self.test.js @@ -31,7 +31,7 @@ describe(Support.getTestDialectTeaser('Self'), () => { Person.hasMany(Person, { as: 'children', foreignKey: 'parent_id', inverse: { as: 'parent' } }); - expect(Person.rawAttributes.parent_id).to.be.ok; + expect(Person.getAttributes().parent_id).to.be.ok; await this.sequelize.sync({ force: true }); @@ -52,7 +52,7 @@ describe(Support.getTestDialectTeaser('Self'), () => { expect(Person.associations.Parents.otherKey).to.eq('PersonId'); expect(Person.associations.Childs.otherKey).to.eq('ChildId'); - const rawAttributes = Object.keys(this.sequelize.models.Family.rawAttributes); + const rawAttributes = Object.keys(this.sequelize.models.Family.getAttributes()); expect(rawAttributes).to.have.members(['createdAt', 'updatedAt', 'PersonId', 'ChildId']); expect(rawAttributes.length).to.equal(4); @@ -88,7 +88,7 @@ describe(Support.getTestDialectTeaser('Self'), () => { expect(Person.associations.Parents.otherKey).to.eq('preexisting_parent'); expect(Person.associations.Children.otherKey).to.eq('preexisting_child'); - const rawAttributes = Object.keys(Family.rawAttributes); + const rawAttributes = Object.keys(Family.getAttributes()); expect(rawAttributes).to.have.members(['preexisting_parent', 'preexisting_child']); expect(rawAttributes.length).to.equal(2); diff --git a/test/integration/data-types/methods.test.ts b/test/integration/data-types/methods.test.ts index 734174874647..3057c7a60952 100644 --- a/test/integration/data-types/methods.test.ts +++ b/test/integration/data-types/methods.test.ts @@ -166,7 +166,7 @@ describe('DataType Methods', () => { it(`updating a model calls 'parseDatabaseValue' on returned values`, async () => { const user = await models.User.create({ name: 'foo' }); user.name = 'bob'; - await user.save({ returning: true, logging: true }); + await user.save({ returning: true }); expect(user.name).to.eq(customValueSymbol, 'parseDatabaseValue has not been called'); }); diff --git a/test/integration/dialects/mariadb/dao-factory.test.js b/test/integration/dialects/mariadb/dao-factory.test.js index e735411bce56..446ded1f447b 100644 --- a/test/integration/dialects/mariadb/dao-factory.test.js +++ b/test/integration/dialects/mariadb/dao-factory.test.js @@ -16,7 +16,7 @@ if (dialect === 'mariadb') { username: { type: DataTypes.STRING, unique: true }, }, { timestamps: false }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.rawAttributes)).to.deep.equal({ + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.getAttributes())).to.deep.equal({ // note: UNIQUE is not specified here because it is only specified if the option passed to attributesToSQL is // 'unique: true'. // Model.init normalizes the 'unique' to ensure a consistent index, and createTableQuery handles adding @@ -32,7 +32,7 @@ if (dialect === 'mariadb') { }, { timestamps: false }); expect( this.sequelize.getQueryInterface().queryGenerator.attributesToSQL( - User.rawAttributes, + User.getAttributes(), ), ).to.deep.equal({ username: 'VARCHAR(255) DEFAULT \'foo\'', @@ -46,7 +46,7 @@ if (dialect === 'mariadb') { }, { timestamps: false }); expect( this.sequelize.getQueryInterface().queryGenerator.attributesToSQL( - User.rawAttributes, + User.getAttributes(), ), ).to.deep.equal({ username: 'VARCHAR(255) NOT NULL', @@ -60,7 +60,7 @@ if (dialect === 'mariadb') { }, { timestamps: false }); expect( this.sequelize.getQueryInterface().queryGenerator.attributesToSQL( - User.rawAttributes, + User.getAttributes(), ), ).to.deep.equal( { username: 'VARCHAR(255) PRIMARY KEY' }, @@ -74,7 +74,7 @@ if (dialect === 'mariadb') { expect( this.sequelize.getQueryInterface().queryGenerator.attributesToSQL( - User1.rawAttributes, + User1.getAttributes(), ), ).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', @@ -83,7 +83,7 @@ if (dialect === 'mariadb') { }); expect( this.sequelize.getQueryInterface().queryGenerator.attributesToSQL( - User2.rawAttributes, + User2.getAttributes(), ), ).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', @@ -97,7 +97,7 @@ if (dialect === 'mariadb') { { paranoid: true }); expect( this.sequelize.getQueryInterface().queryGenerator.attributesToSQL( - User.rawAttributes, + User.getAttributes(), ), ).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', @@ -112,7 +112,7 @@ if (dialect === 'mariadb') { { paranoid: true, underscored: true }); expect( this.sequelize.getQueryInterface().queryGenerator.attributesToSQL( - User.rawAttributes, + User.getAttributes(), ), ).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', @@ -125,13 +125,13 @@ if (dialect === 'mariadb') { it('omits text fields with defaultValues', function () { const User = this.sequelize.define(`User${Support.rand()}`, { name: { type: DataTypes.TEXT, defaultValue: 'helloworld' } }); - expect(User.rawAttributes.name.type.toString()).to.equal('TEXT'); + expect(User.getAttributes().name.type.toString()).to.equal('TEXT'); }); it('omits blobs fields with defaultValues', function () { const User = this.sequelize.define(`User${Support.rand()}`, { name: { type: DataTypes.STRING.BINARY, defaultValue: 'helloworld' } }); - expect(User.rawAttributes.name.type.toString()).to.equal( + expect(User.getAttributes().name.type.toString()).to.equal( 'VARCHAR(255) BINARY', ); }); diff --git a/test/integration/dialects/mysql/dao-factory.test.js b/test/integration/dialects/mysql/dao-factory.test.js index 647bc7260784..e24369d9098e 100644 --- a/test/integration/dialects/mysql/dao-factory.test.js +++ b/test/integration/dialects/mysql/dao-factory.test.js @@ -16,7 +16,7 @@ if (dialect === 'mysql') { username: { type: DataTypes.STRING, unique: true }, }, { timestamps: false }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.rawAttributes)).to.deep.equal({ + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.getAttributes())).to.deep.equal({ // note: UNIQUE is not specified here because it is only specified if the option passed to attributesToSQL is // 'unique: true'. // Model.init normalizes the 'unique' to ensure a consistent index, and createTableQuery handles adding @@ -30,49 +30,49 @@ if (dialect === 'mysql') { const User = this.sequelize.define(`User${Support.rand()}`, { username: { type: DataTypes.STRING, defaultValue: 'foo' }, }, { timestamps: false }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.rawAttributes)).to.deep.equal({ username: 'VARCHAR(255) DEFAULT \'foo\'', id: 'INTEGER NOT NULL auto_increment PRIMARY KEY' }); + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.getAttributes())).to.deep.equal({ username: 'VARCHAR(255) DEFAULT \'foo\'', id: 'INTEGER NOT NULL auto_increment PRIMARY KEY' }); }); it('handles extended attributes (null)', function () { const User = this.sequelize.define(`User${Support.rand()}`, { username: { type: DataTypes.STRING, allowNull: false }, }, { timestamps: false }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.rawAttributes)).to.deep.equal({ username: 'VARCHAR(255) NOT NULL', id: 'INTEGER NOT NULL auto_increment PRIMARY KEY' }); + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.getAttributes())).to.deep.equal({ username: 'VARCHAR(255) NOT NULL', id: 'INTEGER NOT NULL auto_increment PRIMARY KEY' }); }); it('handles extended attributes (primaryKey)', function () { const User = this.sequelize.define(`User${Support.rand()}`, { username: { type: DataTypes.STRING, primaryKey: true }, }, { timestamps: false }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.rawAttributes)).to.deep.equal({ username: 'VARCHAR(255) PRIMARY KEY' }); + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.getAttributes())).to.deep.equal({ username: 'VARCHAR(255) PRIMARY KEY' }); }); it('adds timestamps', function () { const User1 = this.sequelize.define(`User${Support.rand()}`, {}); const User2 = this.sequelize.define(`User${Support.rand()}`, {}, { timestamps: true }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User1.rawAttributes)).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', updatedAt: 'DATETIME(6) NOT NULL', createdAt: 'DATETIME(6) NOT NULL' }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User2.rawAttributes)).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', updatedAt: 'DATETIME(6) NOT NULL', createdAt: 'DATETIME(6) NOT NULL' }); + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User1.getAttributes())).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', updatedAt: 'DATETIME(6) NOT NULL', createdAt: 'DATETIME(6) NOT NULL' }); + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User2.getAttributes())).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', updatedAt: 'DATETIME(6) NOT NULL', createdAt: 'DATETIME(6) NOT NULL' }); }); it('adds deletedAt if paranoid', function () { const User = this.sequelize.define(`User${Support.rand()}`, {}, { paranoid: true }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.rawAttributes)).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', deletedAt: 'DATETIME(6)', updatedAt: 'DATETIME(6) NOT NULL', createdAt: 'DATETIME(6) NOT NULL' }); + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.getAttributes())).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', deletedAt: 'DATETIME(6)', updatedAt: 'DATETIME(6) NOT NULL', createdAt: 'DATETIME(6) NOT NULL' }); }); it('underscores timestamps if underscored', function () { const User = this.sequelize.define(`User${Support.rand()}`, {}, { paranoid: true, underscored: true }); - expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.rawAttributes)).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', deleted_at: 'DATETIME(6)', updated_at: 'DATETIME(6) NOT NULL', created_at: 'DATETIME(6) NOT NULL' }); + expect(this.sequelize.getQueryInterface().queryGenerator.attributesToSQL(User.getAttributes())).to.deep.equal({ id: 'INTEGER NOT NULL auto_increment PRIMARY KEY', deleted_at: 'DATETIME(6)', updated_at: 'DATETIME(6) NOT NULL', created_at: 'DATETIME(6) NOT NULL' }); }); it('omits text fields with defaultValues', function () { const User = this.sequelize.define(`User${Support.rand()}`, { name: { type: DataTypes.TEXT, defaultValue: 'helloworld' } }); - expect(User.rawAttributes.name.type.toString()).to.equal('TEXT'); + expect(User.getAttributes().name.type.toString()).to.equal('TEXT'); }); it('omits blobs fields with defaultValues', function () { const User = this.sequelize.define(`User${Support.rand()}`, { name: { type: DataTypes.STRING.BINARY, defaultValue: 'helloworld' } }); - expect(User.rawAttributes.name.type.toString()).to.equal('VARCHAR(255) BINARY'); + expect(User.getAttributes().name.type.toString()).to.equal('VARCHAR(255) BINARY'); }); }); diff --git a/test/integration/dialects/postgres/query.test.js b/test/integration/dialects/postgres/query.test.js index a18911d21c39..2f3aa2f91dc5 100644 --- a/test/integration/dialects/postgres/query.test.js +++ b/test/integration/dialects/postgres/query.test.js @@ -18,7 +18,7 @@ if (dialect.startsWith('postgres')) { const executeTest = async (options, test) => { const sequelize = Support.createSequelizeInstance(options); - const User = sequelize.define('User', { name: DataTypes.STRING, updatedAt: DataTypes.DATE }, { underscored: true }); + const User = sequelize.define('User', { name: DataTypes.STRING }, { underscored: true }); const Team = sequelize.define('Team', { name: DataTypes.STRING }); const Sponsor = sequelize.define('Sponsor', { name: DataTypes.STRING }); const Task = sequelize.define('Task', { title: DataTypes.STRING }); diff --git a/test/integration/hooks/hooks.test.js b/test/integration/hooks/hooks.test.js index 2c24462a155c..405a6c02acfa 100644 --- a/test/integration/hooks/hooks.test.js +++ b/test/integration/hooks/hooks.test.js @@ -58,7 +58,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { }); it('beforeDefine hook can alter attributes', function () { - expect(this.model.rawAttributes.type).to.be.ok; + expect(this.model.getAttributes().type).to.be.ok; }); it('afterDefine hook can alter options', function () { diff --git a/test/integration/include.test.js b/test/integration/include.test.js index 066b3bdca9cb..a15f450ad4f8 100644 --- a/test/integration/include.test.js +++ b/test/integration/include.test.js @@ -533,7 +533,7 @@ Instead of specifying a Model, either: }, ], order: [ - User.rawAttributes.id, + User.getAttributes().id, [Product, 'id'], ], }); diff --git a/test/integration/include/schema.test.js b/test/integration/include/schema.test.js index fc30ac31710c..bf027b17064f 100644 --- a/test/integration/include/schema.test.js +++ b/test/integration/include/schema.test.js @@ -298,7 +298,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { }, ], order: [ - [AccUser.rawAttributes.id, 'ASC'], + [AccUser.getAttributes().id, 'ASC'], ], }); diff --git a/test/integration/index.test.ts b/test/integration/index.test.ts index 469eedba3d1f..5b925a64bbec 100644 --- a/test/integration/index.test.ts +++ b/test/integration/index.test.ts @@ -86,7 +86,7 @@ describe(getTestDialectTeaser('Indexes'), () => { }); it('throws an error with missing column names', async () => { - sequelize.define('user', { + const User = sequelize.define('user', { username: DataTypes.STRING, first_name: DataTypes.STRING, last_name: DataTypes.STRING, @@ -94,14 +94,8 @@ describe(getTestDialectTeaser('Indexes'), () => { indexes: [{ name: 'user_username', fields: ['username'], include: ['first_name', 'last_name', 'email'], unique: true }], }); - try { - await sequelize.sync({ force: true }); - expect.fail('This should have failed'); - } catch (error: any) { - expect(error).to.be.instanceOf(DatabaseError); - expect(error.message).to.match(/\s|^Column name 'email' does not exist in the target table or view.$/); - } - + await expect(User.sync({ force: true })) + .to.be.rejectedWith(DatabaseError, /\s|^Column name 'email' does not exist in the target table or view.$/); }); it('throws an error with invalid column type', async () => { diff --git a/test/integration/instance/save.test.js b/test/integration/instance/save.test.js index 8736eab67be2..80fe13fd34c3 100644 --- a/test/integration/instance/save.test.js +++ b/test/integration/instance/save.test.js @@ -455,14 +455,6 @@ describe(Support.getTestDialectTeaser('Instance'), () => { it('works with `allowNull: false` on createdAt and updatedAt columns', async function () { const User2 = this.sequelize.define('User2', { username: DataTypes.STRING, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - }, }, { timestamps: true }); await User2.sync(); diff --git a/test/integration/instance/update.test.js b/test/integration/instance/update.test.js index ae7771c79adf..b095d103f58c 100644 --- a/test/integration/instance/update.test.js +++ b/test/integration/instance/update.test.js @@ -134,8 +134,6 @@ describe(Support.getTestDialectTeaser('Instance'), () => { name: DataTypes.STRING, bio: DataTypes.TEXT, email: DataTypes.STRING, - createdAt: { type: DataTypes.DATE(6), allowNull: false }, - updatedAt: { type: DataTypes.DATE(6), allowNull: false }, }, { timestamps: true, }); diff --git a/test/integration/instance/values.test.js b/test/integration/instance/values.test.js index 37c59b58f9e9..471bed2e637d 100644 --- a/test/integration/instance/values.test.js +++ b/test/integration/instance/values.test.js @@ -78,34 +78,6 @@ describe(Support.getTestDialectTeaser('DAO'), () => { expect(user.get('updated_at')).not.to.be.ok; }); - it('doesn\'t set value if not a dynamic setter or a model attribute', function () { - const User = this.sequelize.define('User', { - name: { type: DataTypes.STRING }, - email_hidden: { type: DataTypes.STRING }, - }, { - setterMethods: { - email_secret(value) { - this.set('email_hidden', value); - }, - }, - }); - - const user = User.build(); - - user.set({ - name: 'antonio banderaz', - email: 'antonio@banderaz.com', - email_secret: 'foo@bar.com', - }); - - user.set('email', 'antonio@banderaz.com'); - - expect(user.get('name')).to.equal('antonio banderaz'); - expect(user.get('email_hidden')).to.equal('foo@bar.com'); - expect(user.get('email')).not.to.be.ok; - expect(user.dataValues.email).not.to.be.ok; - }); - it('allows use of sequelize.fn and sequelize.col in date and bool fields', async function () { const User = this.sequelize.define('User', { d: DataTypes.DATE, @@ -245,47 +217,6 @@ describe(Support.getTestDialectTeaser('DAO'), () => { expect(product.get('price')).to.equal(1000); }); - it('should custom virtual getters in get(key)', function () { - const Product = this.sequelize.define('Product', { - priceInCents: { - type: DataTypes.FLOAT, - }, - }, { - getterMethods: { - price() { - return this.dataValues.priceInCents / 100; - }, - }, - }); - - const product = Product.build({ - priceInCents: 1000, - }); - expect(product.get('price')).to.equal(10); - }); - - it('should use custom getters in toJSON', function () { - const Product = this.sequelize.define('Product', { - price: { - type: DataTypes.STRING, - get() { - return this.dataValues.price * 100; - }, - }, - }, { - getterMethods: { - withTaxes() { - return this.get('price') * 1.25; - }, - }, - }); - - const product = Product.build({ - price: 10, - }); - expect(product.toJSON()).to.deep.equal({ withTaxes: 1250, price: 1000, id: null }); - }); - it('should work with save', async function () { const Contact = this.sequelize.define('Contact', { first: { type: DataTypes.STRING }, @@ -365,67 +296,6 @@ describe(Support.getTestDialectTeaser('DAO'), () => { expect(product.get({ clone: true }).title).to.be.ok; }); }); - - it('can pass parameters to getters', function () { - const Product = this.sequelize.define('product', { - title: DataTypes.STRING, - }, { - getterMethods: { - rating(key, options) { - if (options.apiVersion > 1) { - return 100; - } - - return 5; - }, - }, - }); - - const User = this.sequelize.define('user', { - first_name: DataTypes.STRING, - last_name: DataTypes.STRING, - }, { - getterMethods: { - height(key, options) { - if (options.apiVersion > 1) { - return 185; // cm - } - - return 6.06; // ft - }, - }, - }); - - Product.belongsTo(User); - - const product = Product.build({}, { - include: [ - User, - ], - }); - - product.set({ - id: 1, - title: 'Chair', - user: { - id: 1, - first_name: 'Jozef', - last_name: 'Hartinger', - }, - }); - - expect(product.get('rating')).to.equal(5); - expect(product.get('rating', { apiVersion: 2 })).to.equal(100); - - expect(product.get({ plain: true })).to.have.property('rating', 5); - expect(product.get({ plain: true }).user).to.have.property('height', 6.06); - expect(product.get({ plain: true, apiVersion: 1 })).to.have.property('rating', 5); - expect(product.get({ plain: true, apiVersion: 1 }).user).to.have.property('height', 6.06); - expect(product.get({ plain: true, apiVersion: 2 })).to.have.property('rating', 100); - expect(product.get({ plain: true, apiVersion: 2 }).user).to.have.property('height', 185); - - expect(product.get('user').get('height', { apiVersion: 2 })).to.equal(185); - }); }); describe('changed', () => { diff --git a/test/integration/model.test.js b/test/integration/model.test.js index d0c9a0e9e8ff..118b08645c2e 100644 --- a/test/integration/model.test.js +++ b/test/integration/model.test.js @@ -77,15 +77,6 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(await User.create({ id: 'My own ID!' })).to.have.property('id', 'My own ID!'); }); - it('throws an error if 2 autoIncrements are passed', function () { - expect(() => { - this.sequelize.define('UserWithTwoAutoIncrements', { - userid: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, - userscore: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, - }); - }).to.throw(Error, 'Invalid Instance definition. Only one autoincrement field allowed.'); - }); - it('throws an error if a custom model-wide validation is not a function', function () { expect(() => { this.sequelize.define('Foo', { @@ -98,27 +89,13 @@ describe(Support.getTestDialectTeaser('Model'), () => { }).to.throw(Error, 'Members of the validate option must be functions. Model: Foo, error with validate member notFunction'); }); - it('throws an error if a custom model-wide validation has the same name as a field', function () { - expect(() => { - this.sequelize.define('Foo', { - field: DataTypes.INTEGER, - }, { - validate: { - field() {}, - }, - }); - }).to.throw(Error, 'A model validator function must not have the same name as a field. Model: Foo, field/validation name: field'); - }); - it('should allow me to set a default value for createdAt and updatedAt', async function () { const UserTable = this.sequelize.define('UserCol', { aNumber: DataTypes.INTEGER, createdAt: { - type: DataTypes.DATE, defaultValue: dayjs('2012-01-01').toDate(), }, updatedAt: { - type: DataTypes.DATE, defaultValue: dayjs('2012-01-02').toDate(), }, }, { timestamps: true }); @@ -235,27 +212,6 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(user.deletedAtThisTime).to.exist; }); - it('returns proper defaultValues after save when setter is set', async function () { - const titleSetter = sinon.spy(); - const Task = this.sequelize.define('TaskBuild', { - title: { - type: DataTypes.STRING(50), - allowNull: false, - defaultValue: '', - }, - }, { - setterMethods: { - title: titleSetter, - }, - }); - - await Task.sync({ force: true }); - const record = await Task.build().save(); - expect(record.title).to.be.a('string'); - expect(record.title).to.equal(''); - expect(titleSetter.notCalled).to.be.ok; // The setter method should not be invoked for default values - }); - it('should work with both paranoid and underscored being true', async function () { const UserTable = this.sequelize.define('UserCol', { aNumber: DataTypes.INTEGER, @@ -672,63 +628,6 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(p.price).to.equal('answer = 42'); }); - it('attaches getter and setter methods from options', function () { - const Product = this.sequelize.define('ProductWithSettersAndGetters2', { - priceInCents: DataTypes.INTEGER, - }, { - setterMethods: { - price(value) { - this.dataValues.priceInCents = value * 100; - }, - }, - getterMethods: { - price() { - return `$${this.getDataValue('priceInCents') / 100}`; - }, - - priceInCents() { - return this.dataValues.priceInCents; - }, - }, - }); - - expect(Product.build({ price: 20 }).priceInCents).to.equal(20 * 100); - expect(Product.build({ priceInCents: 30 * 100 }).price).to.equal(`$${30}`); - }); - - it('attaches getter and setter methods from options only if not defined in attribute', function () { - const Product = this.sequelize.define('ProductWithSettersAndGetters3', { - price1: { - type: DataTypes.INTEGER, - set(v) { - this.setDataValue('price1', v * 10); - }, - }, - price2: { - type: DataTypes.INTEGER, - get() { - return this.getDataValue('price2') * 10; - }, - }, - }, { - setterMethods: { - price1(v) { - this.setDataValue('price1', v * 100); - }, - }, - getterMethods: { - price2() { - return `$${this.getDataValue('price2')}`; - }, - }, - }); - - const p = Product.build({ price1: 1, price2: 2 }); - - expect(p.price1).to.equal(10); - expect(p.price2).to.equal(20); - }); - describe('include', () => { it('should support basic includes', function () { const Product = this.sequelize.define('Product', { @@ -2449,7 +2348,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); it('uses a table name as a string and references the author table', async function () { - const authorIdColumn = { type: DataTypes.INTEGER, references: { model: 'authors', key: 'id' } }; + const authorIdColumn = { type: DataTypes.INTEGER, references: { table: 'authors', key: 'id' } }; const Post = this.sequelize.define('post', { title: DataTypes.STRING, authorId: authorIdColumn }); @@ -2467,14 +2366,19 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(foreignKeys[0].referencedColumnName).to.eq('id'); }); - it('emits an error event as the referenced table name is invalid', async function () { - const authorIdColumn = { type: DataTypes.INTEGER, references: { model: '4uth0r5', key: 'id' } }; - - const Post = this.sequelize.define('post', { title: DataTypes.STRING, authorId: authorIdColumn }); + it('throws an error if the referenced table name is invalid', async function () { + const Post = this.sequelize.define('post', { + title: DataTypes.STRING, + authorId: DataTypes.INTEGER, + }); this.Author.hasMany(Post); Post.belongsTo(this.Author); + // force Post.authorId to reference a table that does not exist + Post.modelDefinition.rawAttributes.authorId.references.table = '4uth0r5'; + Post.modelDefinition.refreshAttributes(); + try { // The posts table gets dropped in the before filter. await Post.sync(); @@ -2784,7 +2688,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { UserId: { type: DataTypes.STRING, references: { - model: 'Users', + tableName: 'Users', key: 'UUID', }, }, diff --git a/test/integration/model/attributes/field.test.js b/test/integration/model/attributes/field.test.js index 07fc438d8c1d..37cccdd24d1d 100644 --- a/test/integration/model/attributes/field.test.js +++ b/test/integration/model/attributes/field.test.js @@ -6,7 +6,6 @@ const sinon = require('sinon'); const expect = chai.expect; const Support = require('../../support'); const { DataTypes, Sequelize } = require('@sequelize/core'); -const omit = require('lodash/omit'); const dialect = Support.getTestDialect(); @@ -76,8 +75,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { text: { type: DataTypes.STRING, field: 'comment_text' }, notes: { type: DataTypes.STRING, field: 'notes' }, likes: { type: DataTypes.INTEGER, field: 'like_count' }, - createdAt: { type: DataTypes.DATE, field: 'created_at', allowNull: false }, - updatedAt: { type: DataTypes.DATE, field: 'updated_at', allowNull: false }, + createdAt: { field: 'created_at' }, + updatedAt: { field: 'updated_at' }, }, { tableName: 'comments', timestamps: true, @@ -490,7 +489,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { it('should sync foreign keys with custom field names', async function () { await this.sequelize.sync({ force: true }); const attrs = this.Task.tableAttributes; - expect(omit(attrs.user_id.references.model, 'toString')) + expect(attrs.user_id.references.table) .to.deep.equal({ tableName: 'users', schema: this.sequelize.dialect.getDefaultSchema(), delimiter: '.' }); expect(attrs.user_id.references.key).to.equal('userId'); }); diff --git a/test/integration/model/bulk-create.test.js b/test/integration/model/bulk-create.test.js index f7b64518a9b0..8d64251e4e7c 100644 --- a/test/integration/model/bulk-create.test.js +++ b/test/integration/model/bulk-create.test.js @@ -128,12 +128,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { field: 'user_type', }, createdAt: { - type: DataTypes.DATE, - allowNull: false, field: 'created_at', }, updatedAt: { - type: DataTypes.DATE, field: 'modified_at', }, }); @@ -881,11 +878,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { }, createdAt: { field: 'created_at', - type: DataTypes.DATE, }, updatedAt: { field: 'updated_at', - type: DataTypes.DATE, }, }); diff --git a/test/integration/model/bulk-create/include.test.js b/test/integration/model/bulk-create/include.test.js index 615be800ab85..f81cddafa7e7 100644 --- a/test/integration/model/bulk-create/include.test.js +++ b/test/integration/model/bulk-create/include.test.js @@ -483,7 +483,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { tag_id: { type: DataTypes.INTEGER, references: { - model: 'tags', + tableName: 'tags', key: 'id', }, }, diff --git a/test/integration/model/create.test.js b/test/integration/model/create.test.js index d42422a918b3..f458aff71958 100644 --- a/test/integration/model/create.test.js +++ b/test/integration/model/create.test.js @@ -753,12 +753,12 @@ describe(Support.getTestDialectTeaser('Model'), () => { password: DataTypes.STRING, created_time: { type: DataTypes.DATE(3), - allowNull: true, + allowNull: false, defaultValue: DataTypes.NOW, }, updated_time: { type: DataTypes.DATE(3), - allowNull: true, + allowNull: false, defaultValue: DataTypes.NOW, }, }, { @@ -1279,7 +1279,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { const book = await b.create(data); expect(book.title).to.equal(data.title); expect(book.author).to.equal(data.author); - expect(books[index].rawAttributes.id.type instanceof dataTypes[index]).to.be.ok; + expect(books[index].getAttributes().id.type instanceof dataTypes[index]).to.be.ok; })()); } diff --git a/test/integration/model/create/include.test.js b/test/integration/model/create/include.test.js index 6d8640ca77f1..bab893ba9fd0 100644 --- a/test/integration/model/create/include.test.js +++ b/test/integration/model/create/include.test.js @@ -352,7 +352,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { tag_id: { type: DataTypes.INTEGER, references: { - model: 'tags', + table: 'tags', key: 'id', }, }, diff --git a/test/integration/model/json.test.js b/test/integration/model/json.test.js index b71e2d4ca409..9bc805c5884a 100644 --- a/test/integration/model/json.test.js +++ b/test/integration/model/json.test.js @@ -19,9 +19,12 @@ describe(Support.getTestDialectTeaser('Model'), () => { beforeEach(async function () { this.Event = this.sequelize.define('Event', { data: { + // TODO: This should be JSONB, not JSON, because the auto-GIN index is only added + // to JSONB columns. This was accidentally changed by https://github.com/sequelize/sequelize/issues/7094 + // re-enable the index when fixed type: DataTypes.JSON, field: 'event_data', - index: true, + // index: true, }, json: DataTypes.JSON, }); diff --git a/test/integration/model/paranoid.test.js b/test/integration/model/paranoid.test.js index 59a4f2c1804b..e00e974051ae 100644 --- a/test/integration/model/paranoid.test.js +++ b/test/integration/model/paranoid.test.js @@ -60,7 +60,6 @@ describe(Support.getTestDialectTeaser('Model'), () => { type: DataTypes.STRING, }, deletedAt: { - type: DataTypes.DATE, allowNull: true, field: 'deleted_at', }, diff --git a/test/integration/model/schema.test.js b/test/integration/model/schema.test.js index c6f34a61fb2c..88e7eaaf87db 100644 --- a/test/integration/model/schema.test.js +++ b/test/integration/model/schema.test.js @@ -62,8 +62,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { describe('Add data via model.create, retrieve via model.findOne', () => { it('should be able to sync model without schema option', function () { - expect(this.RestaurantOne._schema).to.eq(current.dialect.getDefaultSchema()); - expect(this.RestaurantTwo._schema).to.equal(SCHEMA_TWO); + expect(this.RestaurantOne.table.schema).to.eq(current.dialect.getDefaultSchema()); + expect(this.RestaurantTwo.table.schema).to.equal(SCHEMA_TWO); }); it('should be able to insert data into default table using create', async function () { diff --git a/test/integration/model/sync.test.js b/test/integration/model/sync.test.js index a230f9d19fa7..4a0ec10646ee 100644 --- a/test/integration/model/sync.test.js +++ b/test/integration/model/sync.test.js @@ -182,7 +182,7 @@ describe(getTestDialectTeaser('Model.sync & Sequelize#sync'), () => { await User.sync({ alter: true }); const alterResults = await getNonPrimaryIndexes(User); - expect(syncResults).to.deep.eq(alterResults, '"alter" should not create new indexes if they already exist.'); + expect(alterResults).to.deep.eq(syncResults, '"alter" should not create new indexes if they already exist.'); }); it('creates one unique index per unique:true columns, and per entry in options.indexes', async () => { @@ -576,17 +576,18 @@ describe(getTestDialectTeaser('Model.sync & Sequelize#sync'), () => { { const aFks = await sequelize.queryInterface.getForeignKeyReferencesForTable(A.getTableName()); - expect(aFks.length).to.eq(1); + expect(aFks).to.have.length(1); expect(aFks[0].deferrable).to.eq(Deferrable.INITIALLY_IMMEDIATE); } - A.rawAttributes.BId.references.deferrable = Deferrable.INITIALLY_DEFERRED; + A.modelDefinition.rawAttributes.BId.references.deferrable = Deferrable.INITIALLY_DEFERRED; + A.modelDefinition.refreshAttributes(); await sequelize.sync({ alter: true }); { - const aFks = await sequelize.queryInterface.getForeignKeyReferencesForTable(A.getTableName()); + const aFks = await sequelize.queryInterface.getForeignKeyReferencesForTable(A.table); - expect(aFks.length).to.eq(1); + expect(aFks).to.have.length(1); expect(aFks[0].deferrable).to.eq(Deferrable.INITIALLY_DEFERRED); } }); diff --git a/test/integration/model/upsert.test.js b/test/integration/model/upsert.test.js index 921c9073b24d..22eb21889035 100644 --- a/test/integration/model/upsert.test.js +++ b/test/integration/model/upsert.test.js @@ -547,7 +547,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { deletedAt: { type: DataTypes.DATE, primaryKey: true, - allowNull: false, + allowNull: true, defaultValue: Number.POSITIVE_INFINITY, }, }, { diff --git a/test/integration/query-interface.test.js b/test/integration/query-interface.test.js index 60384b54f9bf..54883897599c 100644 --- a/test/integration/query-interface.test.js +++ b/test/integration/query-interface.test.js @@ -347,7 +347,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { await this.queryInterface.addColumn('users', 'level_id', { type: DataTypes.INTEGER, references: { - model: 'level', + table: 'level', key: 'id', }, onUpdate: 'cascade', @@ -449,14 +449,14 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { admin: { type: DataTypes.INTEGER, references: { - model: 'users', + table: 'users', key: 'id', }, }, operator: { type: DataTypes.INTEGER, references: { - model: 'users', + table: 'users', key: 'id', }, onUpdate: 'cascade', @@ -464,7 +464,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { owner: { type: DataTypes.INTEGER, references: { - model: 'users', + table: 'users', key: 'id', }, onUpdate: 'cascade', diff --git a/test/integration/query-interface/changeColumn.test.js b/test/integration/query-interface/changeColumn.test.js index 6876e158a577..fc1d92f350f1 100644 --- a/test/integration/query-interface/changeColumn.test.js +++ b/test/integration/query-interface/changeColumn.test.js @@ -164,7 +164,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { await this.queryInterface.changeColumn('users', 'level_id', { type: DataTypes.INTEGER, references: { - model: 'level', + table: 'level', key: 'id', }, onUpdate: 'cascade', @@ -192,7 +192,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { await this.queryInterface.changeColumn('users', 'level_id', { type: DataTypes.INTEGER, references: { - model: 'level', + table: 'level', key: 'id', }, onUpdate: 'cascade', @@ -245,10 +245,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { // The tests below address these problems // TODO: run in all dialects if (dialect === 'sqlite') { - it('should not remove unique constraints when adding or modifying columns', async function () { - await this.queryInterface.createTable({ - tableName: 'Foos', - }, { + it('should not loose indexes & unique constraints when adding or modifying columns', async function () { + await this.queryInterface.createTable('foos', { id: { allowNull: false, autoIncrement: true, @@ -265,30 +263,40 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { unique: true, type: DataTypes.STRING, }, + birthday: { + allowNull: false, + type: DataTypes.DATEONLY, + }, }); - await this.queryInterface.addColumn('Foos', 'phone', { + await this.queryInterface.addIndex('foos', ['birthday']); + const initialIndexes = await this.queryInterface.showIndex('foos'); + let table = await this.queryInterface.describeTable('foos'); + expect(table.email.unique).to.equal(true, '(0) email column should be unique'); + expect(table.name.unique).to.equal(true, '(0) name column should be unique'); + + await this.queryInterface.addColumn('foos', 'phone', { type: DataTypes.STRING, defaultValue: null, allowNull: true, }); - let table = await this.queryInterface.describeTable({ - tableName: 'Foos', - }); + expect(await this.queryInterface.showIndex('foos')).to.deep.equal(initialIndexes, 'addColumn should not modify indexes'); + + table = await this.queryInterface.describeTable('foos'); expect(table.phone.allowNull).to.equal(true, '(1) phone column should allow null values'); expect(table.phone.defaultValue).to.equal(null, '(1) phone column should have a default value of null'); expect(table.email.unique).to.equal(true, '(1) email column should remain unique'); expect(table.name.unique).to.equal(true, '(1) name column should remain unique'); - await this.queryInterface.changeColumn('Foos', 'email', { + await this.queryInterface.changeColumn('foos', 'email', { type: DataTypes.STRING, allowNull: true, }); - table = await this.queryInterface.describeTable({ - tableName: 'Foos', - }); + expect(await this.queryInterface.showIndex('foos')).to.deep.equal(initialIndexes, 'changeColumn should not modify indexes'); + + table = await this.queryInterface.describeTable('foos'); expect(table.email.allowNull).to.equal(true, '(2) email column should allow null values'); expect(table.email.unique).to.equal(true, '(2) email column should remain unique'); expect(table.name.unique).to.equal(true, '(2) name column should remain unique'); @@ -391,7 +399,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { allowNull: false, references: { key: 'id', - model: 'level', + table: 'level', }, onDelete: 'CASCADE', onUpdate: 'CASCADE', @@ -408,7 +416,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { allowNull: true, references: { key: 'id', - model: 'level', + table: 'level', }, onDelete: 'CASCADE', onUpdate: 'CASCADE', @@ -440,7 +448,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { allowNull: false, references: { key: 'id', - model: 'level', + table: 'level', }, onDelete: 'CASCADE', onUpdate: 'CASCADE', diff --git a/test/integration/query-interface/removeColumn.test.js b/test/integration/query-interface/removeColumn.test.js index 881a6929422b..4cd827919fca 100644 --- a/test/integration/query-interface/removeColumn.test.js +++ b/test/integration/query-interface/removeColumn.test.js @@ -37,7 +37,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { manager: { type: DataTypes.INTEGER, references: { - model: 'users', + table: 'users', key: 'id', }, }, @@ -121,7 +121,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { allowNull: false, references: { key: 'id', - model: 'level', + table: 'level', }, onDelete: 'CASCADE', onUpdate: 'CASCADE', @@ -193,7 +193,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { allowNull: false, references: { key: 'id', - model: 'level', + table: 'level', }, onDelete: 'CASCADE', onUpdate: 'CASCADE', diff --git a/test/integration/sequelize.test.js b/test/integration/sequelize.test.js index 5dcb2b996cdd..a097fb85d46f 100644 --- a/test/integration/sequelize.test.js +++ b/test/integration/sequelize.test.js @@ -233,63 +233,6 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { }); }); - describe('set', () => { - it('should be configurable with global functions', function () { - const defaultSetterMethod = sinon.spy(); - const overrideSetterMethod = sinon.spy(); - const defaultGetterMethod = sinon.spy(); - const overrideGetterMethod = sinon.spy(); - const customSetterMethod = sinon.spy(); - const customOverrideSetterMethod = sinon.spy(); - const customGetterMethod = sinon.spy(); - const customOverrideGetterMethod = sinon.spy(); - - this.sequelize.options.define = { - setterMethods: { - default: defaultSetterMethod, - override: overrideSetterMethod, - }, - getterMethods: { - default: defaultGetterMethod, - override: overrideGetterMethod, - }, - }; - const testEntity = this.sequelize.define('TestEntity', {}, { - setterMethods: { - custom: customSetterMethod, - override: customOverrideSetterMethod, - }, - getterMethods: { - custom: customGetterMethod, - override: customOverrideGetterMethod, - }, - }); - - // Create Instance to test - const instance = testEntity.build(); - - // Call Getters - instance.default; - instance.custom; - instance.override; - - expect(defaultGetterMethod).to.have.been.calledOnce; - expect(customGetterMethod).to.have.been.calledOnce; - expect(overrideGetterMethod.callCount).to.be.eql(0); - expect(customOverrideGetterMethod).to.have.been.calledOnce; - - // Call Setters - instance.default = 'test'; - instance.custom = 'test'; - instance.override = 'test'; - - expect(defaultSetterMethod).to.have.been.calledOnce; - expect(customSetterMethod).to.have.been.calledOnce; - expect(overrideSetterMethod.callCount).to.be.eql(0); - expect(customOverrideSetterMethod).to.have.been.calledOnce; - }); - }); - if (['mysql', 'mariadb'].includes(dialect)) { describe('set', () => { it('should return an promised error if transaction isn\'t defined', async function () { @@ -640,9 +583,9 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { for (const option of Object.keys(customAttributes[attribute])) { const optionValue = customAttributes[attribute][option]; if (typeof optionValue === 'function' && optionValue() instanceof DataTypes.ABSTRACT) { - expect(Picture.rawAttributes[attribute][option] instanceof optionValue).to.be.ok; + expect(Picture.getAttributes()[attribute][option] instanceof optionValue).to.be.ok; } else { - expect(Picture.rawAttributes[attribute][option]).to.be.equal(optionValue); + expect(Picture.getAttributes()[attribute][option]).to.be.equal(optionValue); } } } diff --git a/test/integration/sequelize/deferrable.test.js b/test/integration/sequelize/deferrable.test.js index 8da1208180dc..bf1c2039915b 100644 --- a/test/integration/sequelize/deferrable.test.js +++ b/test/integration/sequelize/deferrable.test.js @@ -48,13 +48,13 @@ if (Support.sequelize.dialect.supports.deferrableConstraints) { expect(task.user_id).to.equal(1); }); - it('does not allow the violation of the foreign key constraint if the transaction is not deffered', async function () { + it('does not allow the violation of the foreign key constraint if the transaction is not deferred', async function () { await expect(this.run(Sequelize.Deferrable.INITIALLY_IMMEDIATE, { deferrable: undefined, })).to.eventually.be.rejectedWith(Sequelize.ForeignKeyConstraintError); }); - it('allows the violation of the foreign key constraint if the transaction deferres only the foreign key constraint', async function () { + it('allows the violation of the foreign key constraint if the transaction deferred only the foreign key constraint', async function () { const taskTableName = `tasks_${Support.rand()}`; const task = await this @@ -92,7 +92,7 @@ if (Support.sequelize.dialect.supports.deferrableConstraints) { allowNull: false, type: DataTypes.INTEGER, references: { - model: userTableName, + table: userTableName, key: 'id', deferrable, }, diff --git a/test/integration/sequelize/drop.test.ts b/test/integration/sequelize/drop.test.ts index 4bfb5da3db37..7a4887885654 100644 --- a/test/integration/sequelize/drop.test.ts +++ b/test/integration/sequelize/drop.test.ts @@ -9,7 +9,6 @@ describe('Sequelize#drop', () => { const A = sequelize.define('A', { BId: { type: DataTypes.INTEGER, - // @ts-expect-error -- TODO: references requires a model to be specified. We should move reference.deferrable to be an option of foreignKey in belongsTo. references: { deferrable: Deferrable.INITIALLY_IMMEDIATE, }, @@ -19,7 +18,6 @@ describe('Sequelize#drop', () => { const B = sequelize.define('B', { AId: { type: DataTypes.INTEGER, - // @ts-expect-error -- TODO: references requires a model to be specified. We should move reference.deferrable to be an option of foreignKey in belongsTo. references: { deferrable: Deferrable.INITIALLY_IMMEDIATE, }, diff --git a/test/smoke/smoketests.test.js b/test/smoke/smoketests.test.js index 7b07429ad2d1..bf6597999d88 100644 --- a/test/smoke/smoketests.test.js +++ b/test/smoke/smoketests.test.js @@ -36,7 +36,7 @@ describe(Support.getTestDialectTeaser('Smoke Tests'), () => { it('gets all associated objects with all fields', async function () { const john = await this.User.findOne({ where: { username: 'John' } }); const tasks = await john.getTasks(); - for (const attr of Object.keys(tasks[0].rawAttributes)) { + for (const attr of Object.keys(tasks[0].getAttributes())) { expect(tasks[0]).to.have.property(attr); } }); diff --git a/test/types/hooks.ts b/test/types/hooks.ts index 564dd1c3286b..68bc89d5c928 100644 --- a/test/types/hooks.ts +++ b/test/types/hooks.ts @@ -14,7 +14,7 @@ import type { } from '@sequelize/core/_non-semver-use-at-your-own-risk_/associations'; import type { AbstractQuery } from '@sequelize/core/_non-semver-use-at-your-own-risk_/dialects/abstract/query.js'; import type { ValidationOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/instance-validator'; -import type { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-typescript.js'; +import type { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-hooks.js'; import type { SemiDeepWritable } from './type-helpers/deep-writable'; { diff --git a/test/types/model.ts b/test/types/model.ts index 13cbeadb0318..ea03eead4394 100644 --- a/test/types/model.ts +++ b/test/types/model.ts @@ -157,11 +157,6 @@ MyModel.init({ ], sequelize, tableName: 'my_model', - getterMethods: { - multiply() { - return this.int * 2; - }, - }, }); /** diff --git a/test/types/models/user.ts b/test/types/models/user.ts index bfa6edad70af..f2cb38c81535 100644 --- a/test/types/models/user.ts +++ b/test/types/models/user.ts @@ -62,16 +62,6 @@ User.init( }, { version: true, - getterMethods: { - a() { - return 1; - }, - }, - setterMethods: { - b(val: string) { - this.username = val; - }, - }, scopes: { custom(a: number) { return { diff --git a/test/types/query-interface.ts b/test/types/query-interface.ts index 4589c75d17d0..2d0a2c587a17 100644 --- a/test/types/query-interface.ts +++ b/test/types/query-interface.ts @@ -20,7 +20,7 @@ async function test() { onUpdate: 'CASCADE', references: { key: 'id', - model: 'another_table_name', + table: 'another_table_name', }, type: DataTypes.INTEGER, }, @@ -29,21 +29,15 @@ async function test() { onUpdate: 'CASCADE', references: { key: 'id', - model: { schema: '', tableName: 'another_table_name' }, + table: { schema: '', tableName: 'another_table_name' }, }, type: DataTypes.INTEGER, }, - createdAt: { - type: DataTypes.DATE, - }, id: { autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER, }, - updatedAt: { - type: DataTypes.DATE, - }, }, { charset: 'latin1', // default: null @@ -51,7 +45,6 @@ async function test() { engine: 'MYISAM', // default: 'InnoDB' uniqueKeys: { test: { - customIndex: true, fields: ['attr2', 'attr3'], }, }, diff --git a/test/unit/associations/belongs-to-many.test.ts b/test/unit/associations/belongs-to-many.test.ts index f8e32830510d..299fe85c5e19 100644 --- a/test/unit/associations/belongs-to-many.test.ts +++ b/test/unit/associations/belongs-to-many.test.ts @@ -208,7 +208,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { User.belongsToMany(Task, { through: 'user_task1' }); - expect(sequelize.models.user_task1.rawAttributes).to.contain.all.keys(['createdAt', 'updatedAt']); + expect(sequelize.models.user_task1.getAttributes()).to.contain.all.keys(['createdAt', 'updatedAt']); }); it('allows me to override the global timestamps option', () => { @@ -217,7 +217,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { User.belongsToMany(Task, { through: { model: 'user_task2', timestamps: false } }); - expect(sequelize.models.user_task2.rawAttributes).not.to.contain.any.keys(['createdAt', 'updatedAt']); + expect(sequelize.models.user_task2.getAttributes()).not.to.contain.any.keys(['createdAt', 'updatedAt']); }); it('follows the global timestamps false option', () => { @@ -232,7 +232,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { User.belongsToMany(Task, { through: 'user_task3' }); - expect(sequelize2.models.user_task3.rawAttributes).not.to.contain.any.keys(['createdAt', 'updatedAt']); + expect(sequelize2.models.user_task3.getAttributes()).not.to.contain.any.keys(['createdAt', 'updatedAt']); }); }); @@ -361,7 +361,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(Places.otherKey).to.equal('place_id'); expect(Users.otherKey).to.equal('user_id'); - expect(Object.keys(UserPlace.rawAttributes).length).to.equal(3); // Defined primary key and two foreign keys + expect(Object.keys(UserPlace.getAttributes()).length).to.equal(3); // Defined primary key and two foreign keys }); it('should infer foreign keys (camelCase)', () => { @@ -371,8 +371,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(Children.foreignKey).to.equal('ParentId'); expect(Children.otherKey).to.equal('ChildId'); - expect(PersonChildren.rawAttributes[Children.foreignKey]).to.be.ok; - expect(PersonChildren.rawAttributes[Children.otherKey]).to.be.ok; + expect(PersonChildren.getAttributes()[Children.foreignKey]).to.be.ok; + expect(PersonChildren.getAttributes()[Children.otherKey]).to.be.ok; }); it('should infer foreign keys (snake_case)', () => { @@ -382,10 +382,10 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(Children.foreignKey).to.equal('ParentId'); expect(Children.otherKey).to.equal('ChildId'); - expect(PersonChildren.rawAttributes[Children.foreignKey]).to.be.ok; - expect(PersonChildren.rawAttributes[Children.otherKey]).to.be.ok; - expect(PersonChildren.rawAttributes[Children.foreignKey].field).to.equal('parent_id'); - expect(PersonChildren.rawAttributes[Children.otherKey].field).to.equal('child_id'); + expect(PersonChildren.getAttributes()[Children.foreignKey]).to.be.ok; + expect(PersonChildren.getAttributes()[Children.otherKey]).to.be.ok; + expect(PersonChildren.getAttributes()[Children.foreignKey].field).to.equal('parent_id'); + expect(PersonChildren.getAttributes()[Children.otherKey].field).to.equal('child_id'); }); it('should create non-null foreign keys by default', () => { @@ -394,7 +394,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { const association = A.belongsToMany(B, { through: 'AB' }); - const attributes = association.throughModel.rawAttributes; + const attributes = association.throughModel.getAttributes(); expect(attributes.AId.allowNull).to.be.false; expect(attributes.BId.allowNull).to.be.false; }); @@ -409,7 +409,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { otherKey: { allowNull: true }, }); - const attributes = association.throughModel.rawAttributes; + const attributes = association.throughModel.getAttributes(); expect(attributes.AId.allowNull).to.be.true; expect(attributes.BId.allowNull).to.be.true; }); @@ -420,7 +420,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { const association = A.belongsToMany(B, { through: 'AB', foreignKey: {} }); - const attributes = association.throughModel.rawAttributes; + const attributes = association.throughModel.getAttributes(); expect(attributes.AId.onDelete).to.eq('CASCADE'); expect(attributes.BId.onDelete).to.eq('CASCADE'); }); @@ -467,7 +467,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(Places.targetKey).to.equal('place_id'); expect(Users.targetKey).to.equal('user_id', 'Users.targetKey is invalid'); - expect(Object.keys(UserPlace.rawAttributes).length).to.equal(3); // Defined primary key and two foreign keys + expect(Object.keys(UserPlace.getAttributes()).length).to.equal(3); // Defined primary key and two foreign keys }); }); @@ -505,8 +505,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(TagProducts.fromThroughToSource.foreignKey).to.equal(TagProducts.foreignKey); expect(TagProducts.fromThroughToTarget.foreignKey).to.equal(TagProducts.otherKey); - expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4); - expect(Object.keys(ProductTag.rawAttributes).sort()).to.deep.equal(['id', 'priority', 'productId', 'tagId'].sort()); + expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4); + expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(['id', 'priority', 'productId', 'tagId'].sort()); }); it('should setup hasMany relations to source and target from join model with defined foreign/other keys', () => { @@ -542,8 +542,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(TagProducts.fromSourceToThrough.foreignKey).to.equal(TagProducts.foreignKey); expect(TagProducts.fromTargetToThrough.foreignKey).to.equal(TagProducts.otherKey); - expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4); - expect(Object.keys(ProductTag.rawAttributes).sort()).to.deep.equal(['id', 'priority', 'tagId', 'productId'].sort()); + expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4); + expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(['id', 'priority', 'tagId', 'productId'].sort()); }); it('should setup hasOne relations to source and target from join model with defined foreign/other keys', () => { @@ -579,8 +579,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(TagProducts.fromSourceToThroughOne.foreignKey).to.equal(TagProducts.foreignKey); expect(TagProducts.fromTargetToThroughOne.foreignKey).to.equal(TagProducts.otherKey); - expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4); - expect(Object.keys(ProductTag.rawAttributes).sort()).to.deep.equal(['id', 'priority', 'productId', 'tagId'].sort()); + expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4); + expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(['id', 'priority', 'productId', 'tagId'].sort()); }); it('should setup hasOne relations to source and target from join model with defined source keys', () => { @@ -621,8 +621,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(ProductTags.fromSourceToThroughOne.sourceKey).to.equal(ProductTags.sourceKey); expect(ProductTags.fromTargetToThroughOne.sourceKey).to.equal(ProductTags.targetKey); - expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4); - expect(Object.keys(ProductTag.rawAttributes).sort()).to.deep.equal(['id', 'priority', 'ProductProductSecondaryId', 'TagTagSecondaryId'].sort()); + expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4); + expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(['id', 'priority', 'ProductProductSecondaryId', 'TagTagSecondaryId'].sort()); }); it('should setup belongsTo relations to source and target from join model with only foreign keys defined', () => { @@ -658,8 +658,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(TagProducts.fromThroughToSource.foreignKey).to.equal(TagProducts.foreignKey); expect(TagProducts.fromThroughToTarget.foreignKey).to.equal(TagProducts.otherKey); - expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4); - expect(Object.keys(ProductTag.rawAttributes).sort()).to.deep.equal(['id', 'priority', 'product_ID', 'tag_ID'].sort()); + expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4); + expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(['id', 'priority', 'product_ID', 'tag_ID'].sort()); }); it('should setup hasOne relations to source and target from join model with only foreign keys defined', () => { @@ -695,8 +695,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(TagProducts.fromSourceToThroughOne.foreignKey).to.equal(TagProducts.foreignKey); expect(TagProducts.fromTargetToThroughOne.foreignKey).to.equal(TagProducts.otherKey); - expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4); - expect(Object.keys(ProductTag.rawAttributes).sort()).to.deep.equal(['id', 'priority', 'product_ID', 'tag_ID'].sort()); + expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4); + expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(['id', 'priority', 'product_ID', 'tag_ID'].sort()); }); it('should setup belongsTo relations to source and target from join model with no foreign keys defined', () => { @@ -732,8 +732,8 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(TagProducts.fromThroughToSource.foreignKey).to.equal(TagProducts.foreignKey); expect(TagProducts.fromThroughToTarget.foreignKey).to.equal(TagProducts.otherKey); - expect(Object.keys(ProductTag.rawAttributes).length).to.equal(4); - expect(Object.keys(ProductTag.rawAttributes).sort()).to.deep.equal(['id', 'priority', 'ProductId', 'TagId'].sort()); + expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4); + expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(['id', 'priority', 'ProductId', 'TagId'].sort()); }); }); @@ -810,7 +810,7 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(UserFollowers.foreignKey).to.eq('FollowingId'); expect(UserFollowers.otherKey).to.eq('FollowerId'); - expect(Object.keys(UserFollower.rawAttributes).length).to.equal(3); + expect(Object.keys(UserFollower.getAttributes()).length).to.equal(3); }); it('works with singular and plural name for self-associations', () => { @@ -863,14 +863,14 @@ describe(getTestDialectTeaser('belongsToMany'), () => { const MyGroups = User.associations.MyGroups as BelongsToMany; const throughModel = MyUsers.through.model; - expect(Object.keys(throughModel.rawAttributes).sort()) + expect(Object.keys(throughModel.getAttributes()).sort()) .to.deep.equal(['UserId', 'GroupId', 'createdAt', 'updatedAt'].sort()); expect(throughModel === MyGroups.through.model); - expect(throughModel.rawAttributes.UserId.onUpdate).to.equal('RESTRICT'); - expect(throughModel.rawAttributes.UserId.onDelete).to.equal('SET NULL'); - expect(throughModel.rawAttributes.GroupId.onUpdate).to.equal('SET NULL'); - expect(throughModel.rawAttributes.GroupId.onDelete).to.equal('RESTRICT'); + expect(throughModel.getAttributes().UserId.onUpdate).to.equal('RESTRICT'); + expect(throughModel.getAttributes().UserId.onDelete).to.equal('SET NULL'); + expect(throughModel.getAttributes().GroupId.onUpdate).to.equal('SET NULL'); + expect(throughModel.getAttributes().GroupId.onDelete).to.equal('RESTRICT'); }); it('work properly when through is a model', () => { @@ -901,13 +901,13 @@ describe(getTestDialectTeaser('belongsToMany'), () => { const Through = MyUsers.through.model; - expect(Object.keys(Through.rawAttributes).sort()) + expect(Object.keys(Through.getAttributes()).sort()) .to.deep.equal(['UserId', 'GroupId', 'createdAt', 'updatedAt'].sort()); - expect(Through.rawAttributes.UserId.onUpdate).to.equal('RESTRICT', 'UserId.onUpdate should have been RESTRICT'); - expect(Through.rawAttributes.UserId.onDelete).to.equal('SET NULL', 'UserId.onDelete should have been SET NULL'); - expect(Through.rawAttributes.GroupId.onUpdate).to.equal('SET NULL', 'GroupId.OnUpdate should have been SET NULL'); - expect(Through.rawAttributes.GroupId.onDelete).to.equal('RESTRICT', 'GroupId.onDelete should have been RESTRICT'); + expect(Through.getAttributes().UserId.onUpdate).to.equal('RESTRICT', 'UserId.onUpdate should have been RESTRICT'); + expect(Through.getAttributes().UserId.onDelete).to.equal('SET NULL', 'UserId.onDelete should have been SET NULL'); + expect(Through.getAttributes().GroupId.onUpdate).to.equal('SET NULL', 'GroupId.OnUpdate should have been SET NULL'); + expect(Through.getAttributes().GroupId.onDelete).to.equal('RESTRICT', 'GroupId.onDelete should have been RESTRICT'); }); it('makes the foreign keys primary keys', () => { @@ -924,11 +924,13 @@ describe(getTestDialectTeaser('belongsToMany'), () => { const Through = association.throughModel; - expect(Object.keys(Through.rawAttributes).sort()).to.deep.equal(['createdAt', 'updatedAt', 'GroupId', 'UserId'].sort()); - expect(Through.rawAttributes.UserId.primaryKey).to.be.true; - expect(Through.rawAttributes.GroupId.primaryKey).to.be.true; - expect(Through.rawAttributes.UserId.unique).to.be.undefined; - expect(Through.rawAttributes.GroupId.unique).to.be.undefined; + expect(Object.keys(Through.getAttributes()).sort()).to.deep.equal(['createdAt', 'updatedAt', 'GroupId', 'UserId'].sort()); + expect(Through.getAttributes().UserId.primaryKey).to.be.true; + expect(Through.getAttributes().GroupId.primaryKey).to.be.true; + // @ts-expect-error -- this property does not exist after normalization + expect(Through.getAttributes().UserId.unique).to.be.undefined; + // @ts-expect-error -- this property does not exist after normalization + expect(Through.getAttributes().GroupId.unique).to.be.undefined; }); it('generates unique identifier with very long length', () => { @@ -967,9 +969,19 @@ describe(getTestDialectTeaser('belongsToMany'), () => { const Through = MyUsers.through.model; expect(Through === MyGroups.through.model); - expect(Object.keys(Through.rawAttributes).sort()).to.deep.equal(['id', 'createdAt', 'updatedAt', 'id_user_very_long_field', 'id_group_very_long_field'].sort()); - expect(Through.rawAttributes.id_user_very_long_field.unique).to.deep.equal([{ name: 'table_user_group_with_very_long_name_id_group_very_long_field_id_user_very_long_field_unique' }]); - expect(Through.rawAttributes.id_group_very_long_field.unique).to.deep.equal([{ name: 'table_user_group_with_very_long_name_id_group_very_long_field_id_user_very_long_field_unique' }]); + expect(Object.keys(Through.getAttributes()).sort()).to.deep.equal(['id', 'createdAt', 'updatedAt', 'id_user_very_long_field', 'id_group_very_long_field'].sort()); + + expect(Through.getIndexes()).to.deep.equal([{ + name: 'table_user_group_with_very_long_name_id_group_very_long_field_id_user_very_long_field_unique', + unique: true, + fields: ['id_user_very_long_field', 'id_group_very_long_field'], + column: 'id_user_very_long_field', + }]); + + // @ts-expect-error -- this property does not exist after normalization + expect(Through.getAttributes().id_user_very_long_field.unique).to.be.undefined; + // @ts-expect-error -- this property does not exist after normalization + expect(Through.getAttributes().id_group_very_long_field.unique).to.be.undefined; }); it('generates unique identifier with custom name', () => { @@ -1010,8 +1022,18 @@ describe(getTestDialectTeaser('belongsToMany'), () => { expect(MyUsers.through.model === UserGroup); expect(MyGroups.through.model === UserGroup); - expect(UserGroup.rawAttributes.id_user_very_long_field.unique).to.deep.equal([{ name: 'custom_user_group_unique' }]); - expect(UserGroup.rawAttributes.id_group_very_long_field.unique).to.deep.equal([{ name: 'custom_user_group_unique' }]); + + expect(UserGroup.getIndexes()).to.deep.equal([{ + name: 'custom_user_group_unique', + unique: true, + fields: ['id_user_very_long_field', 'id_group_very_long_field'], + column: 'id_user_very_long_field', + }]); + + // @ts-expect-error -- this property does not exist after normalization + expect(UserGroup.getAttributes().id_user_very_long_field.unique).to.be.undefined; + // @ts-expect-error -- this property does not exist after normalization + expect(UserGroup.getAttributes().id_group_very_long_field.unique).to.be.undefined; }); }); diff --git a/test/unit/associations/belongs-to.test.ts b/test/unit/associations/belongs-to.test.ts index c75aa7f4f4ba..41ac156cfb3d 100644 --- a/test/unit/associations/belongs-to.test.ts +++ b/test/unit/associations/belongs-to.test.ts @@ -74,7 +74,7 @@ describe(getTestDialectTeaser('belongsTo'), () => { BarProject.belongsTo(BarUser, { foreignKey: 'userId' }); - expect(BarProject.rawAttributes.userId.allowNull).to.eq(undefined, 'allowNull should be undefined'); + expect(BarProject.getAttributes().userId.allowNull).to.eq(undefined, 'allowNull should be undefined'); }); it('sets the foreign key default onDelete to CASCADE if allowNull: false', async () => { @@ -83,14 +83,13 @@ describe(getTestDialectTeaser('belongsTo'), () => { Task.belongsTo(User, { foreignKey: { allowNull: false } }); - expect(Task.rawAttributes.UserId.onDelete).to.eq('CASCADE'); + expect(Task.getAttributes().UserId.onDelete).to.eq('CASCADE'); }); it(`does not overwrite the 'deferrable' option set in Model.init`, () => { const A = sequelize.define('A', { BId: { type: DataTypes.INTEGER, - // @ts-expect-error -- TODO: 'references' requires a model to be specified. We should move reference.deferrable to be an option of foreignKey in belongsTo. references: { deferrable: Deferrable.INITIALLY_IMMEDIATE, }, @@ -101,7 +100,7 @@ describe(getTestDialectTeaser('belongsTo'), () => { A.belongsTo(B); - expect(A.rawAttributes.BId.references?.deferrable).to.equal(Deferrable.INITIALLY_IMMEDIATE); + expect(A.getAttributes().BId.references?.deferrable).to.equal(Deferrable.INITIALLY_IMMEDIATE); }); describe('association hooks', () => { diff --git a/test/unit/associations/dont-modify-options.test.js b/test/unit/associations/dont-modify-options.test.js index bc817989b4ad..6b6b481cd01b 100644 --- a/test/unit/associations/dont-modify-options.test.js +++ b/test/unit/associations/dont-modify-options.test.js @@ -29,7 +29,7 @@ describe(Support.getTestDialectTeaser('associations'), () => { this.A.belongsTo(this.B, reqValidForeignKey); this.A.belongsTo(this.C, reqValidForeignKey); - expect(this.A.rawAttributes.CId.type instanceof this.C.rawAttributes.id.type.constructor); + expect(this.A.getAttributes().CId.type instanceof this.C.getAttributes().id.type.constructor); }); it('should not be overwritten for belongsToMany', function () { @@ -37,7 +37,7 @@ describe(Support.getTestDialectTeaser('associations'), () => { this.B.belongsToMany(this.A, reqValidForeignKey); this.A.belongsTo(this.C, reqValidForeignKey); - expect(this.A.rawAttributes.CId.type instanceof this.C.rawAttributes.id.type.constructor); + expect(this.A.getAttributes().CId.type instanceof this.C.getAttributes().id.type.constructor); }); it('should not be overwritten for hasOne', function () { @@ -45,7 +45,7 @@ describe(Support.getTestDialectTeaser('associations'), () => { this.B.hasOne(this.A, reqValidForeignKey); this.A.belongsTo(this.C, reqValidForeignKey); - expect(this.A.rawAttributes.CId.type instanceof this.C.rawAttributes.id.type.constructor); + expect(this.A.getAttributes().CId.type instanceof this.C.getAttributes().id.type.constructor); }); it('should not be overwritten for hasMany', function () { @@ -53,7 +53,7 @@ describe(Support.getTestDialectTeaser('associations'), () => { this.B.hasMany(this.A, reqValidForeignKey); this.A.belongsTo(this.C, reqValidForeignKey); - expect(this.A.rawAttributes.CId.type instanceof this.C.rawAttributes.id.type.constructor); + expect(this.A.getAttributes().CId.type instanceof this.C.getAttributes().id.type.constructor); }); }); }); diff --git a/test/unit/associations/has-one.test.ts b/test/unit/associations/has-one.test.ts index e5fbf3f7615c..42ff33ddfe3b 100644 --- a/test/unit/associations/has-one.test.ts +++ b/test/unit/associations/has-one.test.ts @@ -1,7 +1,6 @@ import assert from 'node:assert'; import { expect } from 'chai'; import each from 'lodash/each'; -import omit from 'lodash/omit'; import sinon from 'sinon'; import { DataTypes } from '@sequelize/core'; import type { ModelStatic } from '@sequelize/core'; @@ -48,11 +47,11 @@ describe(getTestDialectTeaser('hasOne'), () => { const association1 = User.hasOne(Task); expect(association1.foreignKey).to.equal('UserId'); - expect(Task.rawAttributes.UserId).not.to.be.empty; + expect(Task.getAttributes().UserId).not.to.be.empty; const association2 = User.hasOne(Task, { as: 'Shabda' }); expect(association2.foreignKey).to.equal('UserId'); - expect(Task.rawAttributes.UserId).not.to.be.empty; + expect(Task.getAttributes().UserId).not.to.be.empty; }); it('should not override custom methods with association mixin', () => { @@ -92,15 +91,15 @@ describe(getTestDialectTeaser('hasOne'), () => { }, }); - expect(Profile.rawAttributes.uid).to.be.ok; + expect(Profile.getAttributes().uid).to.be.ok; - const model = Profile.rawAttributes.uid.references?.model; + const model = Profile.getAttributes().uid.references?.table; assert(typeof model === 'object'); - expect(omit(model, ['toString'])).to.deep.equal(omit(User.getTableName(), ['toString'])); + expect(model).to.deep.equal(User.table); - expect(Profile.rawAttributes.uid.references?.key).to.equal('id'); - expect(Profile.rawAttributes.uid.allowNull).to.be.false; + expect(Profile.getAttributes().uid.references?.key).to.equal('id'); + expect(Profile.getAttributes().uid.allowNull).to.be.false; }); it('works when taking a column directly from the object', () => { @@ -117,15 +116,15 @@ describe(getTestDialectTeaser('hasOne'), () => { }, }); - User.hasOne(Profile, { foreignKey: Profile.rawAttributes.user_id }); + User.hasOne(Profile, { foreignKey: Profile.getAttributes().user_id }); - expect(Profile.rawAttributes.user_id).to.be.ok; - const targetTable = Profile.rawAttributes.user_id.references?.model; + expect(Profile.getAttributes().user_id).to.be.ok; + const targetTable = Profile.getAttributes().user_id.references?.table; assert(typeof targetTable === 'object'); - expect(omit(targetTable, 'toString')).to.deep.equal(omit(User.getTableName(), 'toString')); - expect(Profile.rawAttributes.user_id.references?.key).to.equal('uid'); - expect(Profile.rawAttributes.user_id.allowNull).to.be.false; + expect(targetTable).to.deep.equal(User.table); + expect(Profile.getAttributes().user_id.references?.key).to.equal('uid'); + expect(Profile.getAttributes().user_id.allowNull).to.be.false; }); it('works when merging with an existing definition', () => { @@ -144,14 +143,14 @@ describe(getTestDialectTeaser('hasOne'), () => { User.hasOne(Project, { foreignKey: { allowNull: false } }); - expect(Project.rawAttributes.userUid).to.be.ok; - expect(Project.rawAttributes.userUid.allowNull).to.be.false; - const targetTable = Project.rawAttributes.userUid.references?.model; + expect(Project.getAttributes().userUid).to.be.ok; + expect(Project.getAttributes().userUid.allowNull).to.be.false; + const targetTable = Project.getAttributes().userUid.references?.table; assert(typeof targetTable === 'object'); - expect(omit(targetTable, 'toString')).to.deep.equal(omit(User.getTableName(), 'toString')); - expect(Project.rawAttributes.userUid.references?.key).to.equal('uid'); - expect(Project.rawAttributes.userUid.defaultValue).to.equal(42); + expect(targetTable).to.deep.equal(User.table); + expect(Project.getAttributes().userUid.references?.key).to.equal('uid'); + expect(Project.getAttributes().userUid.defaultValue).to.equal(42); }); }); @@ -161,7 +160,7 @@ describe(getTestDialectTeaser('hasOne'), () => { User.hasOne(Task, { foreignKey: { allowNull: false } }); - expect(Task.rawAttributes.UserId.onDelete).to.eq('CASCADE'); + expect(Task.getAttributes().UserId.onDelete).to.eq('CASCADE'); }); it('should throw an error if an association clashes with the name of an already define attribute', () => { diff --git a/test/unit/data-types/_utils.ts b/test/unit/data-types/_utils.ts index cf4089b6dcb1..26ba6a2e4ad7 100644 --- a/test/unit/data-types/_utils.ts +++ b/test/unit/data-types/_utils.ts @@ -7,7 +7,7 @@ export const testDataTypeSql = createTester((it, description: string, dataType: let result: Error | string; try { - result = sequelize.normalizeDataType(dataType).toSql({ dialect: sequelize.dialect }); + result = typeof dataType === 'string' ? dataType : sequelize.normalizeDataType(dataType).toSql({ dialect: sequelize.dialect }); } catch (error) { assert(error instanceof Error); result = error; diff --git a/test/unit/data-types/misc-data-types.test.ts b/test/unit/data-types/misc-data-types.test.ts index 7b6fc43f23a2..e5665c537652 100644 --- a/test/unit/data-types/misc-data-types.test.ts +++ b/test/unit/data-types/misc-data-types.test.ts @@ -46,7 +46,7 @@ describe('DataTypes.ENUM', () => { anEnum: DataTypes.ENUM('value 1', 'value 2'), }); - const enumType = User.rawAttributes.anEnum.type; + const enumType = User.getAttributes().anEnum.type; assert(typeof enumType !== 'string'); expectsql(enumType.toSql({ dialect }), { diff --git a/test/unit/decorators/attribute.test.ts b/test/unit/decorators/attribute.test.ts index 6537378eb2c4..cad0c7c3f375 100644 --- a/test/unit/decorators/attribute.test.ts +++ b/test/unit/decorators/attribute.test.ts @@ -181,25 +181,19 @@ describe(`@Attribute legacy decorator`, () => { expect(User.getIndexes()).to.deep.equal([ { fields: ['firstName', 'country'], - msg: null, - column: 'country', - customIndex: true, + column: 'firstName', unique: true, name: 'firstName-country', }, { fields: ['firstName', 'lastName'], - msg: null, - column: 'lastName', - customIndex: true, + column: 'firstName', unique: true, name: 'firstName-lastName', }, { fields: ['firstName'], - msg: null, column: 'firstName', - customIndex: true, unique: true, name: 'users_first_name_unique', }, diff --git a/test/unit/decorators/hooks.test.ts b/test/unit/decorators/hooks.test.ts index fe614a8eefd5..9f738dfa1b5a 100644 --- a/test/unit/decorators/hooks.test.ts +++ b/test/unit/decorators/hooks.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { Model } from '@sequelize/core'; -import type { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-typescript.js'; +import type { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-hooks.js'; import { AfterAssociate, AfterBulkCreate, @@ -34,7 +34,10 @@ import { BeforeUpsert, BeforeValidate, ValidationFailed, + BeforeDefinitionRefresh, + AfterDefinitionRefresh, } from '@sequelize/core/decorators-legacy'; +import { sequelize } from '../../support'; // map of hook name to hook decorator const hookMap: Partial> = { @@ -70,6 +73,8 @@ const hookMap: Partial> = { beforeUpsert: BeforeUpsert, beforeValidate: BeforeValidate, validationFailed: ValidationFailed, + beforeDefinitionRefresh: BeforeDefinitionRefresh, + afterDefinitionRefresh: AfterDefinitionRefresh, }; for (const [hookName, decorator] of Object.entries(hookMap)) { @@ -80,6 +85,8 @@ for (const [hookName, decorator] of Object.entries(hookMap)) { static myHook() {} } + sequelize.addModels([MyModel]); + expect(MyModel.hasHooks(hookName as keyof ModelHooks)).to.eq(true, `hook ${hookName} incorrectly registered its hook`); }); @@ -89,9 +96,16 @@ for (const [hookName, decorator] of Object.entries(hookMap)) { static myHook() {} } + sequelize.addModels([MyModel]); + expect(MyModel.hasHooks(hookName as keyof ModelHooks)).to.eq(true, `hook ${hookName} incorrectly registered its hook`); + const hookCount = MyModel.hooks.getListenerCount(hookName as keyof ModelHooks); + MyModel.removeHook(hookName as keyof ModelHooks, 'my-hook'); - expect(MyModel.hasHooks(hookName as keyof ModelHooks)).to.eq(false, `hook ${hookName} should be possible to remove by name`); + + const newHookCount = MyModel.hooks.getListenerCount(hookName as keyof ModelHooks); + + expect(newHookCount).to.eq(hookCount - 1, `hook ${hookName} should be possible to remove by name`); }); it('supports symbol methods', () => { @@ -100,6 +114,8 @@ for (const [hookName, decorator] of Object.entries(hookMap)) { static [Symbol('myHook')]() {} } + sequelize.addModels([MyModel]); + expect(MyModel.hasHooks(hookName as keyof ModelHooks)).to.eq(true, `hook ${hookName} incorrectly registered its hook`); }); @@ -110,6 +126,8 @@ for (const [hookName, decorator] of Object.entries(hookMap)) { nonStaticMethod() {} } + sequelize.addModels([MyModel]); + return MyModel; }).to.throw(Error, /This decorator can only be used on static properties/); }); @@ -121,6 +139,8 @@ for (const [hookName, decorator] of Object.entries(hookMap)) { static nonMethod = 'abc'; } + sequelize.addModels([MyModel]); + return MyModel; }).to.throw(Error, /is not a method/); }); diff --git a/test/unit/decorators/table.test.ts b/test/unit/decorators/table.test.ts index 801df8551286..93f055c07e94 100644 --- a/test/unit/decorators/table.test.ts +++ b/test/unit/decorators/table.test.ts @@ -70,17 +70,15 @@ describe(`@Table legacy decorator`, () => { expect(User.getIndexes()).to.deep.equal([ { + column: 'createdAt', fields: ['createdAt'], name: 'users_created_at', - parser: null, - type: '', }, { + column: 'id', fields: ['id'], unique: true, name: 'users_id_unique', - parser: null, - type: '', }, ]); }); @@ -123,52 +121,4 @@ describe(`@Table legacy decorator`, () => { return User; }).to.throw(); }); - - it('merges setterMethods', () => { - function one() {} - - function two() {} - - @Table({ setterMethods: { one } }) - @Table({ setterMethods: { two } }) - class User extends Model {} - - sequelize.addModels([User]); - - expect(User.options.setterMethods).to.deep.equal({ one, two }); - }); - - it('rejects conflicting setterMethods', () => { - expect(() => { - @Table({ setterMethods: { one: () => {} } }) - @Table({ setterMethods: { one: () => {} } }) - class User extends Model {} - - return User; - }).to.throw(); - }); - - it('merges getterMethods', () => { - function one() {} - - function two() {} - - @Table({ getterMethods: { one } }) - @Table({ getterMethods: { two } }) - class User extends Model {} - - sequelize.addModels([User]); - - expect(User.options.getterMethods).to.deep.equal({ one, two }); - }); - - it('rejects conflicting getterMethods', () => { - expect(() => { - @Table({ getterMethods: { one: () => {} } }) - @Table({ getterMethods: { one: () => {} } }) - class User extends Model {} - - return User; - }).to.throw(); - }); }); diff --git a/test/unit/dialects/db2/query-generator.test.js b/test/unit/dialects/db2/query-generator.test.js index abb39d34742f..be043a8de0e9 100644 --- a/test/unit/dialects/db2/query-generator.test.js +++ b/test/unit/dialects/db2/query-generator.test.js @@ -69,23 +69,23 @@ if (dialect === 'db2') { }, // New references style { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id")' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("pk")' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id") ON DELETE CASCADE' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id") ON UPDATE RESTRICT' }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) DEFAULT 1 REFERENCES "Bar" ("id") ON DELETE CASCADE ON UPDATE RESTRICT' }, }, ], @@ -132,7 +132,7 @@ if (dialect === 'db2') { expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255), "otherId" INTEGER, FOREIGN KEY ("otherId") REFERENCES otherTable (id) ON DELETE CASCADE ON UPDATE NO ACTION);', }, { - arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'], customIndex: true }] }], + arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'] }] }], expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL, CONSTRAINT "uniq_myTable_title_name" UNIQUE ("title", "name"));', }, ], diff --git a/test/unit/dialects/mariadb/query-generator.test.js b/test/unit/dialects/mariadb/query-generator.test.js index 59007f05cb45..de78ca815a72 100644 --- a/test/unit/dialects/mariadb/query-generator.test.js +++ b/test/unit/dialects/mariadb/query-generator.test.js @@ -79,23 +79,23 @@ if (dialect === 'mariadb') { }, // New references style { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`)' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`pk`)' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT' }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL auto_increment DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT' }, }, ], @@ -142,7 +142,7 @@ if (dialect === 'mariadb') { expectation: 'CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER, FOREIGN KEY (`otherId`) REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE=InnoDB;', }, { - arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'], customIndex: true }] }], + arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'] }] }], expectation: 'CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), UNIQUE `uniq_myTable_title_name` (`title`, `name`)) ENGINE=InnoDB;', }, { @@ -401,18 +401,6 @@ if (dialect === 'mariadb') { expectation: 'SELECT `test`.* FROM (SELECT * FROM `myTable` AS `test` HAVING `creationYear` > 2002) AS `test`;', context: QueryGenerator, needsSequelize: true, - }, { - title: 'Contains fields with "." characters.', - arguments: ['myTable', { - attributes: ['foo.bar.baz'], - model: { - rawAttributes: { - 'foo.bar.baz': {}, - }, - }, - }], - expectation: 'SELECT `foo.bar.baz` FROM `myTable`;', - context: QueryGenerator, }, ], diff --git a/test/unit/dialects/mysql/query-generator.test.js b/test/unit/dialects/mysql/query-generator.test.js index 5dfab1a637bc..5cb68794dcca 100644 --- a/test/unit/dialects/mysql/query-generator.test.js +++ b/test/unit/dialects/mysql/query-generator.test.js @@ -79,23 +79,23 @@ if (dialect === 'mysql') { }, // New references style { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`)' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`pk`)' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT' }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL auto_increment DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT' }, }, ], @@ -142,7 +142,7 @@ if (dialect === 'mysql') { expectation: 'CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER, FOREIGN KEY (`otherId`) REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE=InnoDB;', }, { - arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'], customIndex: true }] }], + arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'] }] }], expectation: 'CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), UNIQUE `uniq_myTable_title_name` (`title`, `name`)) ENGINE=InnoDB;', }, { @@ -401,18 +401,6 @@ if (dialect === 'mysql') { expectation: 'SELECT `test`.* FROM (SELECT * FROM `myTable` AS `test` HAVING `creationYear` > 2002) AS `test`;', context: QueryGenerator, needsSequelize: true, - }, { - title: 'Contains fields with "." characters.', - arguments: ['myTable', { - attributes: ['foo.bar.baz'], - model: { - rawAttributes: { - 'foo.bar.baz': {}, - }, - }, - }], - expectation: 'SELECT `foo.bar.baz` FROM `myTable`;', - context: QueryGenerator, }, ], diff --git a/test/unit/dialects/postgres/query-generator.test.js b/test/unit/dialects/postgres/query-generator.test.js index 37e3af0e1547..1037a23584f0 100644 --- a/test/unit/dialects/postgres/query-generator.test.js +++ b/test/unit/dialects/postgres/query-generator.test.js @@ -67,49 +67,49 @@ if (dialect.startsWith('postgres')) { }, // New references style { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id")' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("pk")' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id") ON DELETE CASCADE' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id") ON UPDATE RESTRICT' }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES "Bar" ("id") ON DELETE CASCADE ON UPDATE RESTRICT' }, }, // Variants when quoteIdentifiers is false { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES Bar (id)' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES Bar (pk)' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES Bar (id) ON DELETE CASCADE' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES Bar (id) ON UPDATE RESTRICT' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES Bar (id) ON DELETE CASCADE ON UPDATE RESTRICT' }, context: { options: { quoteIdentifiers: false } }, }, diff --git a/test/unit/dialects/snowflake/query-generator.test.js b/test/unit/dialects/snowflake/query-generator.test.js index 6642fed35cf0..6b21cf4d566d 100644 --- a/test/unit/dialects/snowflake/query-generator.test.js +++ b/test/unit/dialects/snowflake/query-generator.test.js @@ -79,49 +79,49 @@ if (dialect === 'snowflake') { }, // New references style { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id")' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("pk")' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id") ON DELETE CASCADE' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES "Bar" ("id") ON UPDATE RESTRICT' }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL AUTOINCREMENT DEFAULT 1 REFERENCES "Bar" ("id") ON DELETE CASCADE ON UPDATE RESTRICT' }, }, // Variants when quoteIdentifiers is false { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES Bar (id)' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES Bar (pk)' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES Bar (id) ON DELETE CASCADE' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES Bar (id) ON UPDATE RESTRICT' }, context: { options: { quoteIdentifiers: false } }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL AUTOINCREMENT DEFAULT 1 REFERENCES Bar (id) ON DELETE CASCADE ON UPDATE RESTRICT' }, context: { options: { quoteIdentifiers: false } }, }, @@ -169,7 +169,7 @@ if (dialect === 'snowflake') { expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255), "otherId" INTEGER, FOREIGN KEY ("otherId") REFERENCES "otherTable" ("id") ON DELETE CASCADE ON UPDATE NO ACTION);', }, { - arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'], customIndex: true }] }], + arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'] }] }], expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255), UNIQUE "uniq_myTable_title_name" ("title", "name"));', }, // Variants when quoteIdentifiers is false @@ -224,7 +224,7 @@ if (dialect === 'snowflake') { context: { options: { quoteIdentifiers: false } }, }, { - arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'], customIndex: true }] }], + arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'] }] }], expectation: 'CREATE TABLE IF NOT EXISTS myTable (title VARCHAR(255), name VARCHAR(255), UNIQUE uniq_myTable_title_name (title, name));', context: { options: { quoteIdentifiers: false } }, }, @@ -491,18 +491,6 @@ if (dialect === 'snowflake') { expectation: 'SELECT "test".* FROM (SELECT * FROM "myTable" AS "test" HAVING "creationYear" > 2002) AS "test";', context: QueryGenerator, needsSequelize: true, - }, { - title: 'Contains fields with "." characters.', - arguments: ['myTable', { - attributes: ['foo.bar.baz'], - model: { - rawAttributes: { - 'foo.bar.baz': {}, - }, - }, - }], - expectation: 'SELECT "foo.bar.baz" FROM "myTable";', - context: QueryGenerator, }, // Variants when quoteIdentifiers is false @@ -755,18 +743,6 @@ if (dialect === 'snowflake') { expectation: 'SELECT test.* FROM (SELECT * FROM myTable AS test HAVING creationYear > 2002) AS test;', context: { options: { quoteIdentifiers: false } }, needsSequelize: true, - }, { - title: 'Contains fields with "." characters.', - arguments: ['myTable', { - attributes: ['foo.bar.baz'], - model: { - rawAttributes: { - 'foo.bar.baz': {}, - }, - }, - }], - expectation: 'SELECT "foo.bar.baz" FROM myTable;', - context: { options: { quoteIdentifiers: false } }, }, ], diff --git a/test/unit/dialects/sqlite/query-generator.test.js b/test/unit/dialects/sqlite/query-generator.test.js index ab457d2dd135..ba8c78b0a550 100644 --- a/test/unit/dialects/sqlite/query-generator.test.js +++ b/test/unit/dialects/sqlite/query-generator.test.js @@ -55,23 +55,23 @@ if (dialect === 'sqlite') { // New references style { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' } } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`)' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar', key: 'pk' } } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar', key: 'pk' } } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`pk`)' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onDelete: 'CASCADE' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onDelete: 'CASCADE' } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE' }, }, { - arguments: [{ id: { type: 'INTEGER', references: { model: 'Bar' }, onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', references: { table: 'Bar' }, onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT' }, }, { - arguments: [{ id: { type: 'INTEGER', allowNull: false, defaultValue: 1, references: { model: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], + arguments: [{ id: { type: 'INTEGER', allowNull: false, defaultValue: 1, references: { table: 'Bar' }, onDelete: 'CASCADE', onUpdate: 'RESTRICT' } }], expectation: { id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT' }, }, ], @@ -118,7 +118,7 @@ if (dialect === 'sqlite') { expectation: 'CREATE TABLE IF NOT EXISTS `myTable` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255));', }, { - arguments: ['myTable', { id: 'INTEGER PRIMARY KEY AUTOINCREMENT', name: 'VARCHAR(255)', surname: 'VARCHAR(255)' }, { uniqueKeys: { uniqueConstraint: { fields: ['name', 'surname'], customIndex: true } } }], + arguments: ['myTable', { id: 'INTEGER PRIMARY KEY AUTOINCREMENT', name: 'VARCHAR(255)', surname: 'VARCHAR(255)' }, { uniqueKeys: { uniqueConstraint: { fields: ['name', 'surname'] } } }], // SQLITE does not respect the index name when the index is created through CREATE TABLE // As such, Sequelize's createTable does not add the constraint in the Sequelize Dialect. // Instead, `sequelize.sync` calls CREATE INDEX after the table has been created, diff --git a/test/unit/hooks.test.js b/test/unit/hooks.test.js index 1dee685cefd5..4db1a3f55b79 100644 --- a/test/unit/hooks.test.js +++ b/test/unit/hooks.test.js @@ -226,41 +226,28 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { }); }); - describe('using define hooks', () => { - beforeEach(function () { - this.beforeCreate = sinon.spy(); - this.sequelize = Support.createSequelizeInstance({ - define: { - hooks: { - beforeCreate: this.beforeCreate, - }, - }, - }); - }); - - it('runs the global hook when no hook is passed', async function () { - const Model = this.sequelize.define('M', {}, { + it('registers both the global define hook, and the local hook', async () => { + const globalHook = sinon.spy(); + const sequelize = Support.createSequelizeInstance({ + define: { hooks: { - beforeUpdate: _.noop, // Just to make sure we can define other hooks without overwriting the global one + beforeCreate: globalHook, }, - }); - - await Model.runHooks('beforeCreate'); - expect(this.beforeCreate).to.have.been.calledOnce; + }, }); - it('does not run the global hook when the model specifies its own hook', async function () { - const localHook = sinon.spy(); - const Model = this.sequelize.define('M', {}, { - hooks: { - beforeCreate: localHook, - }, - }); + const localHook = sinon.spy(); - await Model.runHooks('beforeCreate'); - expect(this.beforeCreate).not.to.have.been.called; - expect(localHook).to.have.been.calledOnce; + const Model = sequelize.define('M', {}, { + hooks: { + beforeUpdate: _.noop, // Just to make sure we can define other hooks without overwriting the global one + beforeCreate: localHook, + }, }); + + await Model.runHooks('beforeCreate'); + expect(globalHook).to.have.been.calledOnce; + expect(localHook).to.have.been.calledOnce; }); }); diff --git a/test/unit/instance/build.test.js b/test/unit/instance/build.test.js index ee36250a8646..ec62fc6b3a62 100644 --- a/test/unit/instance/build.test.js +++ b/test/unit/instance/build.test.js @@ -42,11 +42,8 @@ describe(Support.getTestDialectTeaser('Instance'), () => { }); const instance = Model.build({ ip: '127.0.0.1', ip2: '0.0.0.0' }); - expect(instance.get('created_time')).to.be.ok; - expect(instance.get('created_time')).to.be.an.instanceof(Date); - - expect(instance.get('updated_time')).to.be.ok; - expect(instance.get('updated_time')).to.be.an.instanceof(Date); + expect(instance.get('created_time')).to.be.an.instanceof(Date, 'created_time should be a date'); + expect(instance.get('updated_time')).to.be.an.instanceof(Date, 'updated_time should be a date'); await instance.validate(); }); diff --git a/test/unit/instance/decrement.test.js b/test/unit/instance/decrement.test.js index dfd988a96c74..7c94f6089dd1 100644 --- a/test/unit/instance/decrement.test.js +++ b/test/unit/instance/decrement.test.js @@ -19,7 +19,6 @@ describe(Support.getTestDialectTeaser('Instance'), () => { }); describe('options tests', () => { - let stub; let instance; const Model = current.define('User', { id: { type: DataTypes.BIGINT, @@ -28,6 +27,7 @@ describe(Support.getTestDialectTeaser('Instance'), () => { }, }); + let stub; before(() => { stub = sinon.stub(current, 'queryRaw').resolves( { @@ -41,11 +41,9 @@ describe(Support.getTestDialectTeaser('Instance'), () => { stub.restore(); }); - it('should allow decrements even if options are not given', () => { - instance = Model.build({ id: 3 }, { isNewRecord: false }); - expect(() => { - instance.decrement(['id']); - }).to.not.throw(); + it('should allow decrements even if options are not given', async () => { + const instance = Model.build({ id: 3 }, { isNewRecord: false }); + await expect(instance.decrement(['id'])).to.be.fulfilled; }); }); }); diff --git a/test/unit/instance/reload.test.js b/test/unit/instance/reload.test.js index 4e043d1487c5..8836f946eb46 100644 --- a/test/unit/instance/reload.test.js +++ b/test/unit/instance/reload.test.js @@ -26,9 +26,7 @@ describe(Support.getTestDialectTeaser('Instance'), () => { primaryKey: true, autoIncrement: true, }, - deletedAt: { - type: DataTypes.DATE, - }, + deletedAt: {}, }, { paranoid: true, }); @@ -46,11 +44,9 @@ describe(Support.getTestDialectTeaser('Instance'), () => { stub.restore(); }); - it('should allow reloads even if options are not given', () => { + it('should allow reloads even if options are not given', async () => { instance = Model.build({ id: 1 }, { isNewRecord: false }); - expect(() => { - instance.reload(); - }).to.not.throw(); + await expect(instance.reload()).to.be.fulfilled; }); }); }); diff --git a/test/unit/instance/restore.test.js b/test/unit/instance/restore.test.js index 60b44016d4db..0e97ada5038c 100644 --- a/test/unit/instance/restore.test.js +++ b/test/unit/instance/restore.test.js @@ -26,9 +26,7 @@ describe(Support.getTestDialectTeaser('Instance'), () => { primaryKey: true, autoIncrement: true, }, - deletedAt: { - type: DataTypes.DATE, - }, + deletedAt: {}, }, { paranoid: true, }); diff --git a/test/unit/model/define.test.js b/test/unit/model/define.test.js index 517e927d6d19..8b6969b0a5e1 100644 --- a/test/unit/model/define.test.js +++ b/test/unit/model/define.test.js @@ -20,14 +20,14 @@ describe(Support.getTestDialectTeaser('Model'), () => { underscored: true, }); - expect(User.rawAttributes).to.haveOwnProperty('createdAt'); - expect(User.rawAttributes).to.haveOwnProperty('updatedAt'); + expect(User.getAttributes()).to.haveOwnProperty('createdAt'); + expect(User.getAttributes()).to.haveOwnProperty('updatedAt'); - expect(User._timestampAttributes.createdAt).to.equal('createdAt'); - expect(User._timestampAttributes.updatedAt).to.equal('updatedAt'); + expect(User.modelDefinition.timestampAttributeNames.createdAt).to.equal('createdAt'); + expect(User.modelDefinition.timestampAttributeNames.updatedAt).to.equal('updatedAt'); - expect(User.rawAttributes).not.to.have.property('created_at'); - expect(User.rawAttributes).not.to.have.property('updated_at'); + expect(User.getAttributes()).not.to.have.property('created_at'); + expect(User.getAttributes()).not.to.have.property('updated_at'); }); it('should throw only when id is added but primaryKey is not set', () => { @@ -46,8 +46,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { }, }); - expect(Bar.rawAttributes).to.have.property('id'); - expect(Bar.rawAttributes.id.primaryKey).to.equal(true); + expect(Bar.getAttributes()).to.have.property('id'); + expect(Bar.getAttributes().id.primaryKey).to.equal(true); }); it('allows creating an "id" field explicitly marked as non primary key', () => { @@ -58,8 +58,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { }, }); - expect(Baz.rawAttributes).to.have.property('id'); - expect(Baz.rawAttributes.id.primaryKey).to.equal(false); + expect(Baz.getAttributes()).to.have.property('id'); + expect(Baz.getAttributes().id.primaryKey).to.equal(false); expect(Baz.primaryKeys).to.deep.eq({}); }); @@ -68,13 +68,13 @@ describe(Support.getTestDialectTeaser('Model'), () => { noPrimaryKey: true, }); - expect(User.rawAttributes).not.to.have.property('id'); + expect(User.getAttributes()).not.to.have.property('id'); }); it('should add the default `id` field PK if noPrimary is not set and no PK has been defined manually', () => { const User = current.define('User', {}); - expect(User.rawAttributes).to.have.property('id'); + expect(User.getAttributes()).to.have.property('id'); }); it('should not add the default `id` field PK if PK has been defined manually', () => { @@ -85,7 +85,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }, }); - expect(User.rawAttributes).not.to.have.property('id'); + expect(User.getAttributes()).not.to.have.property('id'); }); it('should support noPrimaryKey on Sequelize define option', () => { @@ -110,9 +110,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(User.getIndexes()).to.deep.equal([{ fields: ['firstName'], - msg: null, column: 'firstName', - customIndex: true, unique: true, name: 'users_first_name_unique', }]); @@ -132,9 +130,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(User.getIndexes()).to.deep.equal([{ fields: ['firstName', 'lastName'], - msg: null, - column: 'lastName', - customIndex: true, + column: 'firstName', unique: true, name: 'firstName-lastName', }]); @@ -159,23 +155,17 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(User.getIndexes()).to.deep.equal([ { fields: ['firstName'], - msg: null, column: 'firstName', - customIndex: true, unique: true, name: 'users_first_name_unique', }, { fields: ['firstName', 'lastName'], - msg: null, - column: 'lastName', - customIndex: true, + column: 'firstName', unique: true, name: 'firstName-lastName', }, { fields: ['firstName', 'country'], - msg: null, - column: 'country', - customIndex: true, + column: 'firstName', unique: true, name: 'firstName-country', }, @@ -261,7 +251,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }, }, }); - }).to.throw('Invalid definition for "user.name", "notNull" validator is only allowed with "allowNull:false"'); + }).to.throwWithCause(`"notNull" validator is only allowed with "allowNull:false"`); expect(() => { current.define('part', { @@ -274,7 +264,16 @@ describe(Support.getTestDialectTeaser('Model'), () => { }, }, }); - }).to.throw('Invalid definition for "part.name", "notNull" validator is only allowed with "allowNull:false"'); + }).to.throwWithCause(`"notNull" validator is only allowed with "allowNull:false"`); + }); + + it('throws an error if 2 autoIncrements are passed', function () { + expect(() => { + this.sequelize.define('UserWithTwoAutoIncrements', { + userid: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + userscore: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + }); + }).to.throwWithCause(`Only one autoIncrement attribute is allowed per model, but both 'userscore' and 'userid' are marked as autoIncrement.`); }); describe('datatype warnings', () => { diff --git a/test/unit/model/get-attributes.test.ts b/test/unit/model/get-attributes.test.ts index 18fbb1fdd0b6..fc5e1d634a27 100644 --- a/test/unit/model/get-attributes.test.ts +++ b/test/unit/model/get-attributes.test.ts @@ -1,9 +1,9 @@ import { expect } from 'chai'; import { DataTypes } from '@sequelize/core'; -import type { BuiltModelAttributeColumnOptions, DataType } from '@sequelize/core'; +import type { NormalizedAttributeOptions, DataType } from '@sequelize/core'; import { sequelize, getTestDialectTeaser } from '../../support'; -function assertDataType(property: BuiltModelAttributeColumnOptions, dataType: DataType) { +function assertDataType(property: NormalizedAttributeOptions, dataType: DataType) { expect(property.type).to.be.instanceof(dataType); } diff --git a/test/unit/model/indexes.test.js b/test/unit/model/indexes.test.js deleted file mode 100644 index 0178935500ce..000000000000 --- a/test/unit/model/indexes.test.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -const chai = require('chai'); - -const expect = chai.expect; -const Support = require('../../support'); - -const current = Support.sequelize; -const dialect = current.dialect; -const { DataTypes } = require('@sequelize/core'); - -describe(Support.getTestDialectTeaser('Model'), () => { - describe('indexes', () => { - if (dialect.supports.dataTypes.JSONB) { - it('should automatically set a gin index for JSONB indexes', () => { - const Model = current.define('event', { - eventData: { - type: DataTypes.JSONB, - index: true, - field: 'data', - }, - }); - - expect(Model.rawAttributes.eventData.index).not.to.equal(true); - expect(Model.getIndexes().length).to.equal(1); - expect(Model.getIndexes()[0].fields).to.eql(['data']); - expect(Model.getIndexes()[0].using).to.equal('gin'); - }); - } - - it('should set the unique property when type is unique', () => { - const Model = current.define('m', {}, { - indexes: [ - { - type: 'unique', - fields: ['name'], - }, - { - type: 'UNIQUE', - fields: ['name'], - }, - ], - }); - - expect(Model.getIndexes()[0].unique).to.eql(true); - expect(Model.getIndexes()[1].unique).to.eql(true); - }); - - it('should not set rawAttributes when indexes are defined via options', () => { - const User = current.define('User', { - username: DataTypes.STRING, - }, { - indexes: [{ - unique: true, - fields: ['username'], - }], - }); - - expect(User.rawAttributes.username.unique).to.be.undefined; - }); - - it('should not set rawAttributes when composite unique indexes are defined via options', () => { - const User = current.define('User', { - name: DataTypes.STRING, - address: DataTypes.STRING, - }, { - indexes: [{ - unique: 'users_name_address', - fields: ['name', 'address'], - }], - }); - - expect(User.rawAttributes.name.unique).to.be.undefined; - expect(User.rawAttributes.address.unique).to.be.undefined; - }); - }); -}); diff --git a/test/unit/model/indexes.test.ts b/test/unit/model/indexes.test.ts new file mode 100644 index 000000000000..442834266eec --- /dev/null +++ b/test/unit/model/indexes.test.ts @@ -0,0 +1,164 @@ +import { expect } from 'chai'; +import { DataTypes } from '@sequelize/core'; +import { sequelize } from '../../support'; + +const dialect = sequelize.dialect; + +describe('Model indexes', () => { + if (dialect.supports.dataTypes.JSONB) { + it('uses a gin index for JSONB attributes by default', () => { + const Model = sequelize.define('event', { + eventData: { + type: DataTypes.JSONB, + field: 'data', + index: true, + }, + }); + + expect(Model.getIndexes()).to.deep.eq([ + { + column: 'eventData', + fields: ['data'], + using: 'gin', + name: 'events_data', + }, + ]); + }); + } + + it('should set the unique property when type is unique', () => { + const Model = sequelize.define('m', {}, { + indexes: [ + { + type: 'unique', + fields: ['firstName'], + }, + { + type: 'UNIQUE', + fields: ['lastName'], + }, + ], + }); + + expect(Model.getIndexes()).to.deep.eq([ + { + fields: ['firstName'], + unique: true, + name: 'ms_first_name_unique', + }, + { + fields: ['lastName'], + unique: true, + name: 'ms_last_name_unique', + }, + ]); + }); + + // Model.getIndexes() is the only source of truth for indexes + it('does not copy model-level indexes to individual attributes', () => { + const User = sequelize.define('User', { + username: DataTypes.STRING, + }, { + indexes: [{ + unique: true, + fields: ['username'], + }], + }); + + // @ts-expect-error -- "unique" gets removed from built attributes + expect(User.getAttributes().username.unique).to.be.undefined; + }); + + it('supports declaring an index on an attribute', () => { + const User = sequelize.define('User', { + name: { + type: DataTypes.STRING, + index: true, + }, + }); + + expect(User.getIndexes()).to.deep.eq([ + { + column: 'name', + fields: ['name'], + name: 'users_name', + }, + ]); + }); + + it('merges indexes with the same name', () => { + const User = sequelize.define('User', { + firstName: { + type: DataTypes.STRING, + index: 'name', + }, + middleName: { + type: DataTypes.STRING, + index: { + name: 'name', + }, + }, + lastName: { + type: DataTypes.STRING, + index: 'name', + }, + }, { + indexes: [{ + name: 'name', + fields: ['nickname'], + }], + }); + + expect(User.getIndexes()).to.deep.eq([ + { + fields: ['nickname', 'firstName', 'middleName', 'lastName'], + name: 'name', + }, + ]); + }); + + it('throws if two indexes with the same name use incompatible options', () => { + expect(() => { + sequelize.define('User', { + firstName: { + type: DataTypes.STRING, + index: { + name: 'name', + unique: true, + }, + }, + lastName: { + type: DataTypes.STRING, + index: { + name: 'name', + unique: false, + }, + }, + }); + }).to.throw('Index "name" has conflicting options: "unique" was defined with different values true and false.'); + }); + + it('supports using index & unique at the same time', () => { + const User = sequelize.define('User', { + firstName: { + type: DataTypes.STRING, + unique: true, + index: true, + }, + }); + + expect(User.getIndexes()).to.deep.eq([ + { + fields: ['firstName'], + column: 'firstName', + unique: true, + name: 'users_first_name_unique', + }, + { + fields: ['firstName'], + column: 'firstName', + name: 'users_first_name', + }, + ]); + }); +}); diff --git a/test/unit/model/init.test.ts b/test/unit/model/init.test.ts index 40371f59f113..0079b66eb483 100644 --- a/test/unit/model/init.test.ts +++ b/test/unit/model/init.test.ts @@ -11,8 +11,4 @@ describe('Uninitialized model', () => { it('throws when .sequelize is accessed', () => { expect(() => Test.sequelize).to.throw(/has not been initialized/); }); - - it('does not throw if the method does not need Sequelize', () => { - expect(() => Test.beforeCreate(() => {})).not.to.throw(); - }); }); diff --git a/test/unit/model/remove-attribute.test.ts b/test/unit/model/remove-attribute.test.ts index 3537872a6007..a06a2d9345b1 100644 --- a/test/unit/model/remove-attribute.test.ts +++ b/test/unit/model/remove-attribute.test.ts @@ -10,12 +10,12 @@ describe(getTestDialectTeaser('Model'), () => { name: DataTypes.STRING, }); - expect(Model.primaryKeyAttribute).not.to.be.undefined; + expect(Model.primaryKeyAttribute).to.equal('id'); expect(size(Model.primaryKeys)).to.equal(1); Model.removeAttribute('id'); - expect(Model.primaryKeyAttribute).to.be.undefined; + expect(Model.primaryKeyAttribute).to.be.null; expect(size(Model.primaryKeys)).to.equal(0); }); diff --git a/test/unit/model/schema.test.ts b/test/unit/model/schema.test.ts index b945e70a6342..395043fe848a 100644 --- a/test/unit/model/schema.test.ts +++ b/test/unit/model/schema.test.ts @@ -19,16 +19,16 @@ describe(`${Support.getTestDialectTeaser('Model')}Schemas`, () => { describe('schema', () => { it('should work with no default schema', () => { - expect(Project._schema).to.equal(current.dialect.getDefaultSchema()); + expect(Project.table.schema).to.equal(current.dialect.getDefaultSchema()); }); it('should apply default schema from define', () => { - expect(Company._schema).to.equal('default'); + expect(Company.table.schema).to.equal('default'); }); it('returns the same model if the schema is equal', () => { // eslint-disable-next-line no-self-compare - assert(Project.withSchema('newSchema') === Project.withSchema('newSchema')); + assert(Project.withSchema('newSchema') === Project.withSchema('newSchema'), 'withSchema should have returned the same model if the schema is equal'); }); it('returns a new model if the schema is equal, but scope is different', () => { @@ -41,41 +41,41 @@ describe(`${Support.getTestDialectTeaser('Model')}Schemas`, () => { }); it('should be able to override the default schema', () => { - expect(Company.schema('newSchema')._schema).to.equal('newSchema'); + expect(Company.schema('newSchema').table.schema).to.equal('newSchema'); }); it('should be able nullify schema', () => { - expect(Company.schema(null)._schema).to.equal(current.dialect.getDefaultSchema()); + expect(Company.schema(null).table.schema).to.equal(current.dialect.getDefaultSchema()); }); it('should support multiple, coexistent schema models', () => { const schema1 = Company.schema('schema1'); const schema2 = Company.schema('schema1'); - expect(schema1._schema).to.equal('schema1'); - expect(schema2._schema).to.equal('schema1'); + expect(schema1.table.schema).to.equal('schema1'); + expect(schema2.table.schema).to.equal('schema1'); }); }); describe('schema delimiter', () => { it('should work with no default schema delimiter', () => { - expect(Project._schemaDelimiter).to.equal(''); + expect(Project.table.delimiter).to.equal('.'); }); it('should apply default schema delimiter from define', () => { - expect(Company._schemaDelimiter).to.equal('&'); + expect(Company.table.delimiter).to.equal('&'); }); it('should be able to override the default schema delimiter', () => { - expect(Company.schema(Company._schema, '^')._schemaDelimiter).to.equal('^'); + expect(Company.schema(Company.table.schema, '^').table.delimiter).to.equal('^'); }); it('should support multiple, coexistent schema delimiter models', () => { - const schema1 = Company.schema(Company._schema, '$'); - const schema2 = Company.schema(Company._schema, '#'); + const schema1 = Company.schema(Company.table.schema, '$'); + const schema2 = Company.schema(Company.table.schema, '#'); - expect(schema1._schemaDelimiter).to.equal('$'); - expect(schema2._schemaDelimiter).to.equal('#'); + expect(schema1.table.delimiter).to.equal('$'); + expect(schema2.table.delimiter).to.equal('#'); }); }); } diff --git a/test/unit/model/underscored.test.js b/test/unit/model/underscored.test.js index 59867e98c5d9..c2b6919324f2 100644 --- a/test/unit/model/underscored.test.js +++ b/test/unit/model/underscored.test.js @@ -32,60 +32,60 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); it('should properly set field when defining', function () { - expect(this.N.rawAttributes.id.field).to.equal('n_id'); - expect(this.M.rawAttributes.id.field).to.equal('m_id'); + expect(this.N.getAttributes().id.field).to.equal('n_id'); + expect(this.M.getAttributes().id.field).to.equal('m_id'); }); it('hasOne does not override already defined field', function () { - this.N.rawAttributes.mId = { + this.N.modelDefinition.rawAttributes.mId = { type: DataTypes.STRING(20), field: 'n_m_id', }; - this.N.refreshAttributes(); + this.N.modelDefinition.refreshAttributes(); - expect(this.N.rawAttributes.mId.field).to.equal('n_m_id'); + expect(this.N.getAttributes().mId.field).to.equal('n_m_id'); this.M.hasOne(this.N, { foreignKey: 'mId' }); - expect(this.N.rawAttributes.mId.field).to.equal('n_m_id'); + expect(this.N.getAttributes().mId.field).to.equal('n_m_id'); }); it('belongsTo does not override already defined field', function () { - this.N.rawAttributes.mId = { + this.N.modelDefinition.rawAttributes.mId = { type: DataTypes.STRING(20), field: 'n_m_id', }; - this.N.refreshAttributes(); + this.N.modelDefinition.refreshAttributes(); - expect(this.N.rawAttributes.mId.field).to.equal('n_m_id'); + expect(this.N.getAttributes().mId.field).to.equal('n_m_id'); this.N.belongsTo(this.M, { foreignKey: 'mId' }); - expect(this.N.rawAttributes.mId.field).to.equal('n_m_id'); + expect(this.N.getAttributes().mId.field).to.equal('n_m_id'); }); it('hasOne/belongsTo does not override already defined field', function () { - this.N.rawAttributes.mId = { + this.N.modelDefinition.rawAttributes.mId = { type: DataTypes.STRING(20), field: 'n_m_id', }; - this.N.refreshAttributes(); + this.N.modelDefinition.refreshAttributes(); - expect(this.N.rawAttributes.mId.field).to.equal('n_m_id'); + expect(this.N.getAttributes().mId.field).to.equal('n_m_id'); this.N.belongsTo(this.M, { foreignKey: 'mId' }); this.M.hasOne(this.N, { foreignKey: 'mId' }); - expect(this.N.rawAttributes.mId.field).to.equal('n_m_id'); + expect(this.N.getAttributes().mId.field).to.equal('n_m_id'); }); it('hasMany does not override already defined field', function () { - this.M.rawAttributes.nId = { + this.M.modelDefinition.rawAttributes.nId = { type: DataTypes.STRING(20), field: 'nana_id', }; - this.M.refreshAttributes(); + this.M.modelDefinition.refreshAttributes(); - expect(this.M.rawAttributes.nId.field).to.equal('nana_id'); + expect(this.M.getAttributes().nId.field).to.equal('nana_id'); this.N.hasMany(this.M, { foreignKey: 'nId' }); this.M.belongsTo(this.N, { foreignKey: 'nId' }); - expect(this.M.rawAttributes.nId.field).to.equal('nana_id'); + expect(this.M.getAttributes().nId.field).to.equal('nana_id'); }); it('belongsToMany does not override already defined field', function () { @@ -104,8 +104,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { this.N.belongsToMany(this.M, { through: this.NM, foreignKey: 'n_id', otherKey: 'm_id' }); - expect(this.NM.rawAttributes.n_id.field).to.equal('nana_id'); - expect(this.NM.rawAttributes.m_id.field).to.equal('mama_id'); + expect(this.NM.getAttributes().n_id.field).to.equal('nana_id'); + expect(this.NM.getAttributes().m_id.field).to.equal('mama_id'); }); }); }); diff --git a/test/unit/model/validation.test.js b/test/unit/model/validation.test.js index 05aeb856a6b6..36947981ce24 100644 --- a/test/unit/model/validation.test.js +++ b/test/unit/model/validation.test.js @@ -410,9 +410,9 @@ describe(Support.getTestDialectTeaser('InstanceValidator'), () => { describe('update', () => { it('should throw when passing string', async () => { - await expect(User.update({ - integer: 'jan', - }, { where: {} })).to.be.rejectedWith(Sequelize.ValidationError) + await expect( + User.update({ integer: 'jan' }, { where: {} }), + ).to.be.rejectedWith(Sequelize.ValidationError) .which.eventually.have.property('errors') .that.is.an('array') .with.lengthOf(1) diff --git a/test/unit/query-generator/insert-query.test.ts b/test/unit/query-generator/insert-query.test.ts index ccc90c673c91..117067344567 100644 --- a/test/unit/query-generator/insert-query.test.ts +++ b/test/unit/query-generator/insert-query.test.ts @@ -88,7 +88,7 @@ describe('QueryGenerator#insertQuery', () => { it('supports returning: true', () => { const { query } = queryGenerator.insertQuery(User.tableName, { firstName: 'John', - }, User.rawAttributes, { + }, User.getAttributes(), { returning: true, }); @@ -107,7 +107,7 @@ describe('QueryGenerator#insertQuery', () => { it('supports array of strings (column names)', () => { const { query } = queryGenerator.insertQuery(User.tableName, { firstName: 'John', - }, User.rawAttributes, { + }, User.getAttributes(), { returning: ['*', 'myColumn'], }); @@ -129,7 +129,7 @@ describe('QueryGenerator#insertQuery', () => { expectsql(() => { return queryGenerator.insertQuery(User.tableName, { firstName: 'John', - }, User.rawAttributes, { + }, User.getAttributes(), { returning: [literal('*')], }).query; }, { diff --git a/test/unit/sql/add-column.test.js b/test/unit/sql/add-column.test.js index bcdcd1c194db..e8ff231e240c 100644 --- a/test/unit/sql/add-column.test.js +++ b/test/unit/sql/add-column.test.js @@ -31,7 +31,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { return expectsql(sql.addColumnQuery(User.getTableName(), 'level_id', current.normalizeAttribute({ type: DataTypes.INTEGER, references: { - model: 'level', + table: 'level', key: 'id', }, onUpdate: 'cascade', diff --git a/test/unit/sql/change-column.test.js b/test/unit/sql/change-column.test.js index 3c17f9e44aef..b0b551b8e345 100644 --- a/test/unit/sql/change-column.test.js +++ b/test/unit/sql/change-column.test.js @@ -55,7 +55,7 @@ if (current.dialect.name !== 'sqlite') { return current.getQueryInterface().changeColumn(Model.getTableName(), 'level_id', { type: DataTypes.INTEGER, references: { - model: 'level', + table: 'level', key: 'id', }, onUpdate: 'cascade', diff --git a/test/unit/sql/create-table.test.js b/test/unit/sql/create-table.test.js index 4099b31d8be5..59171f552fef 100644 --- a/test/unit/sql/create-table.test.js +++ b/test/unit/sql/create-table.test.js @@ -23,7 +23,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { describe('with enums', () => { it('references enum in the right schema #3171', () => { - expectsql(sql.createTableQuery(FooUser.getTableName(), sql.attributesToSQL(FooUser.rawAttributes), {}), { + expectsql(sql.createTableQuery(FooUser.getTableName(), sql.attributesToSQL(FooUser.getAttributes()), {}), { sqlite: 'CREATE TABLE IF NOT EXISTS `foo.users` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `mood` TEXT);', db2: 'CREATE TABLE IF NOT EXISTS "foo"."users" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "mood" VARCHAR(255) CHECK ("mood" IN(\'happy\', \'sad\')), PRIMARY KEY ("id"));', postgres: 'CREATE TABLE IF NOT EXISTS "foo"."users" ("id" SERIAL , "mood" "foo"."enum_users_mood", PRIMARY KEY ("id"));', @@ -55,7 +55,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { BarProject.belongsTo(BarUser, { foreignKey: 'user_id' }); it('references right schema when adding foreign key #9029', () => { - expectsql(sql.createTableQuery(BarProject.getTableName(), sql.attributesToSQL(BarProject.rawAttributes), {}), { + expectsql(sql.createTableQuery(BarProject.getTableName(), sql.attributesToSQL(BarProject.getAttributes()), {}), { sqlite: 'CREATE TABLE IF NOT EXISTS `bar.projects` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user_id` INTEGER REFERENCES `bar.users` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE);', db2: 'CREATE TABLE IF NOT EXISTS "bar"."projects" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "user_id" INTEGER, PRIMARY KEY ("id"), FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") ON DELETE NO ACTION);', postgres: 'CREATE TABLE IF NOT EXISTS "bar"."projects" ("id" SERIAL , "user_id" INTEGER REFERENCES "bar"."users" ("id") ON DELETE NO ACTION ON UPDATE CASCADE, PRIMARY KEY ("id"));', @@ -87,7 +87,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }); it('references on primary key #9461', () => { - expectsql(sql.createTableQuery(Image.getTableName(), sql.attributesToSQL(Image.rawAttributes), {}), { + expectsql(sql.createTableQuery(Image.getTableName(), sql.attributesToSQL(Image.getAttributes()), {}), { sqlite: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER PRIMARY KEY AUTOINCREMENT REFERENCES `files` (`id`));', postgres: 'CREATE TABLE IF NOT EXISTS "images" ("id" SERIAL REFERENCES "files" ("id"), PRIMARY KEY ("id"));', db2: 'CREATE TABLE IF NOT EXISTS "images" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , PRIMARY KEY ("id"), FOREIGN KEY ("id") REFERENCES "files" ("id"));', diff --git a/test/unit/sql/enum.test.js b/test/unit/sql/enum.test.js index bd0ac6cd4039..39c429d9022c 100644 --- a/test/unit/sql/enum.test.js +++ b/test/unit/sql/enum.test.js @@ -33,22 +33,22 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }); it('properly quotes both the schema and the enum name', () => { - expect(sql.pgEnumName(PublicUser.getTableName(), 'mood', PublicUser.rawAttributes.mood.type)) + expect(sql.pgEnumName(PublicUser.getTableName(), 'mood', PublicUser.getAttributes().mood.type)) .to.equal('"public"."enum_users_mood"'); - expect(sql.pgEnumName(FooUser.getTableName(), 'theirMood', FooUser.rawAttributes.mood.type)) + expect(sql.pgEnumName(FooUser.getTableName(), 'theirMood', FooUser.getAttributes().mood.type)) .to.equal('"foo"."enum_users_theirMood"'); }); }); describe('pgEnum', () => { it('uses schema #3171', () => { - expectsql(sql.pgEnum(FooUser.getTableName(), 'mood', FooUser.rawAttributes.mood.type), { + expectsql(sql.pgEnum(FooUser.getTableName(), 'mood', FooUser.getAttributes().mood.type), { postgres: 'CREATE TYPE "foo"."enum_users_mood" AS ENUM(\'happy\', \'sad\');', }); }); it('does add schema when public', () => { - expectsql(sql.pgEnum(PublicUser.getTableName(), 'theirMood', PublicUser.rawAttributes.mood.type), { + expectsql(sql.pgEnum(PublicUser.getTableName(), 'theirMood', PublicUser.getAttributes().mood.type), { postgres: 'CREATE TYPE "public"."enum_users_theirMood" AS ENUM(\'happy\', \'sad\');', }); }); diff --git a/test/unit/sql/insert.test.js b/test/unit/sql/insert.test.js index dee63f08d2a3..6f54f7f33d9d 100644 --- a/test/unit/sql/insert.test.js +++ b/test/unit/sql/insert.test.js @@ -29,7 +29,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { returning: true, hasTrigger: true, }; - expectsql(sql.insertQuery(User.tableName, { user_name: 'triggertest' }, User.rawAttributes, options), + expectsql(sql.insertQuery(User.tableName, { user_name: 'triggertest' }, User.getAttributes(), options), { query: { ibmi: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("user_name") VALUES ($sequelize_1))', @@ -53,7 +53,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, }); - expectsql(sql.insertQuery(M.tableName, { id: 0 }, M.rawAttributes), + expectsql(sql.insertQuery(M.tableName, { id: 0 }, M.getAttributes()), { query: { mssql: 'SET IDENTITY_INSERT [ms] ON; INSERT INTO [ms] ([id]) VALUES ($sequelize_1); SET IDENTITY_INSERT [ms] OFF;', @@ -87,7 +87,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { timestamps: false, }); - expectsql(timezoneSequelize.dialect.queryGenerator.insertQuery(User.tableName, { date: new Date(Date.UTC(2015, 0, 20)) }, User.rawAttributes, {}), + expectsql(timezoneSequelize.dialect.queryGenerator.insertQuery(User.tableName, { date: new Date(Date.UTC(2015, 0, 20)) }, User.getAttributes(), {}), { query: { default: 'INSERT INTO [users] ([date]) VALUES ($sequelize_1);', @@ -114,7 +114,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { timestamps: false, }); - expectsql(current.dialect.queryGenerator.insertQuery(User.tableName, { date: new Date(Date.UTC(2015, 0, 20)) }, User.rawAttributes, {}), + expectsql(current.dialect.queryGenerator.insertQuery(User.tableName, { date: new Date(Date.UTC(2015, 0, 20)) }, User.getAttributes(), {}), { query: { ibmi: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("date") VALUES ($sequelize_1))', @@ -146,7 +146,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { timestamps: false, }); - expectsql(current.dialect.queryGenerator.insertQuery(User.tableName, { date: new Date(Date.UTC(2015, 0, 20, 1, 2, 3, 89)) }, User.rawAttributes, {}), + expectsql(current.dialect.queryGenerator.insertQuery(User.tableName, { date: new Date(Date.UTC(2015, 0, 20, 1, 2, 3, 89)) }, User.getAttributes(), {}), { query: { ibmi: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("date") VALUES ($sequelize_1))', @@ -181,7 +181,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { timestamps: false, }); - expectsql(sql.insertQuery(User.tableName, { user_name: 'null\0test' }, User.rawAttributes), + expectsql(sql.insertQuery(User.tableName, { user_name: 'null\0test' }, User.getAttributes()), { query: { ibmi: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("user_name") VALUES ($sequelize_1))', @@ -212,11 +212,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { field: 'pass_word', }, createdAt: { - type: DataTypes.DATE, field: 'created_at', }, updatedAt: { - type: DataTypes.DATE, field: 'updated_at', }, }, { @@ -224,7 +222,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }); // mapping primary keys to their "field" override values - const primaryKeys = User.primaryKeyAttributes.map(attr => User.rawAttributes[attr].field || attr); + const primaryKeys = User.primaryKeyAttributes.map(attr => User.getAttributes()[attr].field || attr); expectsql(sql.bulkInsertQuery(User.tableName, [{ user_name: 'testuser', pass_word: '12345' }], { updateOnDuplicate: ['user_name', 'pass_word', 'updated_at'], upsertKeys: primaryKeys }, User.fieldRawAttributesMap), { diff --git a/test/unit/sql/order.test.js b/test/unit/sql/order.test.js index 5365814b7ad1..39e29c0d0518 100644 --- a/test/unit/sql/order.test.js +++ b/test/unit/sql/order.test.js @@ -45,14 +45,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { allowNull: false, }, createdAt: { - type: DataTypes.DATE, field: 'created_at', - allowNull: false, }, updatedAt: { - type: DataTypes.DATE, field: 'updated_at', - allowNull: true, }, }, { tableName: 'user', @@ -72,14 +68,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { allowNull: false, }, createdAt: { - type: DataTypes.DATE, field: 'created_at', - allowNull: false, }, updatedAt: { - type: DataTypes.DATE, field: 'updated_at', - allowNull: true, }, }, { tableName: 'project', @@ -104,14 +96,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { allowNull: false, }, createdAt: { - type: DataTypes.DATE, field: 'created_at', - allowNull: false, }, updatedAt: { - type: DataTypes.DATE, field: 'updated_at', - allowNull: true, }, }, { tableName: 'project_user', @@ -136,14 +124,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { allowNull: false, }, createdAt: { - type: DataTypes.DATE, field: 'created_at', - allowNull: false, }, updatedAt: { - type: DataTypes.DATE, field: 'updated_at', - allowNull: true, }, }, { tableName: 'task', @@ -168,14 +152,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { allowNull: false, }, createdAt: { - type: DataTypes.DATE, field: 'created_at', - allowNull: false, }, updatedAt: { - type: DataTypes.DATE, field: 'updated_at', - allowNull: true, }, }, { tableName: 'subtask', diff --git a/test/unit/sql/select.test.js b/test/unit/sql/select.test.js index 3530f5c0e3ca..64fd90d8ab46 100644 --- a/test/unit/sql/select.test.js +++ b/test/unit/sql/select.test.js @@ -594,7 +594,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { subQuery: true, }, User), { default: 'SELECT [User].* FROM ' - + '(SELECT [User].[name], [User].[age], [User].[id] AS [id], [postaliasname].[id] AS [postaliasname.id], [postaliasname].[title] AS [postaliasname.title] FROM [User] AS [User] ' + + '(SELECT [User].[name], [User].[age], [User].[id], [postaliasname].[id] AS [postaliasname.id], [postaliasname].[title] AS [postaliasname.title] FROM [User] AS [User] ' + 'INNER JOIN [Post] AS [postaliasname] ON [User].[id] = [postaliasname].[user_id] ' + `WHERE ( SELECT [user_id] FROM [Post] AS [postaliasname] WHERE ([postaliasname].[user_id] = [User].[id])${sql.addLimitAndOffset({ limit: 1, tableAs: 'postaliasname' }, User)} ) IS NOT NULL) AS [User];`, }); @@ -619,7 +619,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { subQuery: true, }, User), { default: 'SELECT [User].* FROM ' - + '(SELECT [User].[name], [User].[age], [User].[id] AS [id], [postaliasname].[id] AS [postaliasname.id], [postaliasname].[title] AS [postaliasname.title] FROM [User] AS [User] ' + + '(SELECT [User].[name], [User].[age], [User].[id], [postaliasname].[id] AS [postaliasname.id], [postaliasname].[title] AS [postaliasname.title] FROM [User] AS [User] ' + 'INNER JOIN [Post] AS [postaliasname] ON [User].[id] = [postaliasname].[user_id] ' + `WHERE [postaliasname].[title] = ${sql.escape('test')} AND ( SELECT [user_id] FROM [Post] AS [postaliasname] WHERE ([postaliasname].[user_id] = [User].[id])${sql.addLimitAndOffset({ limit: 1, tableAs: 'postaliasname' }, User)} ) IS NOT NULL) AS [User];`, }); @@ -671,7 +671,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { subQuery: true, }, Company), { default: 'SELECT [Company].* FROM (' - + 'SELECT [Company].[name], [Company].[public], [Company].[id] AS [id] FROM [Company] AS [Company] ' + + 'SELECT [Company].[name], [Company].[public], [Company].[id] FROM [Company] AS [Company] ' + 'INNER JOIN [Users] AS [Users] ON [Company].[id] = [Users].[companyId] ' + 'INNER JOIN [Professions] AS [Users->Profession] ON [Users].[professionId] = [Users->Profession].[id] ' + `WHERE ([Company].[scopeId] IN (42)) AND [Users->Profession].[name] = ${sql.escape('test')} AND ( ` diff --git a/test/unit/sql/update.test.js b/test/unit/sql/update.test.js index 8d68d734378d..e7032ae959a9 100644 --- a/test/unit/sql/update.test.js +++ b/test/unit/sql/update.test.js @@ -24,7 +24,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { const options = { returning: false, }; - expectsql(sql.updateQuery(User.tableName, { user_name: 'triggertest' }, { id: 2 }, options, User.rawAttributes), + expectsql(sql.updateQuery(User.tableName, { user_name: 'triggertest' }, { id: 2 }, options, User.getAttributes()), { query: { db2: 'SELECT * FROM FINAL TABLE (UPDATE "users" SET "user_name"=$sequelize_1 WHERE "id" = $sequelize_2);', @@ -52,7 +52,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { returning: true, hasTrigger: true, }; - expectsql(sql.updateQuery(User.tableName, { user_name: 'triggertest' }, { id: 2 }, options, User.rawAttributes), + expectsql(sql.updateQuery(User.tableName, { user_name: 'triggertest' }, { id: 2 }, options, User.getAttributes()), { query: { ibmi: 'UPDATE "users" SET "user_name"=$sequelize_1 WHERE "id" = $sequelize_2', diff --git a/test/unit/utils/utils.test.ts b/test/unit/utils/utils.test.ts index 0abf7b87f17b..d2cee9f69720 100644 --- a/test/unit/utils/utils.test.ts +++ b/test/unit/utils/utils.test.ts @@ -249,11 +249,7 @@ describe('Utils', () => { describe('mapFinderOptions', () => { it('virtual attribute dependencies', () => { - expect(mapFinderOptions({ - attributes: [ - 'active', - ], - }, sequelize.define('User', { + const User = sequelize.define('User', { createdAt: { type: DataTypes.DATE, field: 'created_at', @@ -261,7 +257,11 @@ describe('Utils', () => { active: { type: new DataTypes.VIRTUAL(DataTypes.BOOLEAN, ['createdAt']), }, - })).attributes).to.eql([ + }); + + expect( + mapFinderOptions({ attributes: ['active'] }, User).attributes, + ).to.eql([ [ 'created_at', 'createdAt',