diff --git a/src/associations/helpers.ts b/src/associations/helpers.ts index 1e7648afd691..2514f8d8d847 100644 --- a/src/associations/helpers.ts +++ b/src/associations/helpers.ts @@ -27,7 +27,7 @@ export function checkNamingCollision(source: ModelStatic, associationName: export function addForeignKeyConstraints( newAttribute: ModelAttributeColumnOptions, - source: ModelStatic, + source: ModelStatic, options: AssociationOptions, key: string, ): void { @@ -239,7 +239,7 @@ export function defineAssociation< }); if (normalizedOptions.hooks) { - source.runHooks('beforeAssociate', { source, target, type, sequelize }, normalizedOptions); + source.hooks.runSync('beforeAssociate', { source, target, type, sequelize }, normalizedOptions); } let association; @@ -255,7 +255,7 @@ export function defineAssociation< } if (normalizedOptions.hooks) { - source.runHooks('afterAssociate', { source, target, type, association, sequelize }, normalizedOptions); + source.hooks.runSync('afterAssociate', { source, target, type, association, sequelize }, normalizedOptions); } checkNamingCollision(source, normalizedOptions.as); diff --git a/src/associations/index.ts b/src/associations/index.ts index ca0e106786c5..77cfcddbbf6c 100644 --- a/src/associations/index.ts +++ b/src/associations/index.ts @@ -9,13 +9,13 @@ export * from './has-one'; export * from './has-many'; export * from './belongs-to-many'; -export type BeforeAssociateEventData = { - source: ModelStatic, - target: ModelStatic, - sequelize: Sequelize, - type: Class, -}; +export interface BeforeAssociateEventData { + source: ModelStatic; + target: ModelStatic; + sequelize: Sequelize; + type: Class; +} -export type AfterAssociateEventData = BeforeAssociateEventData & { - association: Association, -}; +export interface AfterAssociateEventData extends BeforeAssociateEventData { + association: Association; +} diff --git a/src/dialects/abstract/connection-manager.ts b/src/dialects/abstract/connection-manager.ts index 3f0e59c0e6da..529f98a50d3b 100644 --- a/src/dialects/abstract/connection-manager.ts +++ b/src/dialects/abstract/connection-manager.ts @@ -279,9 +279,9 @@ export class AbstractConnectionManager { - await this.sequelize.runHooks('beforeConnect', config); + await this.sequelize.hooks.runAsync('beforeConnect', config); const connection = await this.connect(config); - await this.sequelize.runHooks('afterConnect', connection, config); + await this.sequelize.hooks.runAsync('afterConnect', connection, config); return connection; } @@ -293,8 +293,8 @@ export class AbstractConnectionManager { + ( + hookName: HookName, + ...args: HookConfig[HookName] extends (...args2: any) => any + ? Parameters + : never + ): Return; +} + +export function legacyBuildRunHook( + hookHandlerBuilder: HookHandlerBuilder, +): LegacyRunHookFunction { + + return async function runHooks( + this: object, + hookName: HookName, + ...args: HookConfig[HookName] extends (...args2: any) => any + ? Parameters + : never + ): Promise { + hooksReworked(); + + return hookHandlerBuilder.getFor(this).runAsync(hookName, ...args); + }; +} + +export interface LegacyAddAnyHookFunction { + /** + * Adds a hook listener + */ + (this: This, hookName: HookName, hook: HookConfig[HookName]): This; + + /** + * Adds a hook listener + * + * @param listenerName Provide a name for the hook function. It can be used to remove the hook later. + */ + ( + this: This, + hookName: HookName, + listenerName: string, + hook: HookConfig[HookName] + ): This; +} + +export function legacyBuildAddAnyHook( + hookHandlerBuilder: HookHandlerBuilder, +): LegacyAddAnyHookFunction { + + return function addHook( + this: This, + hookName: HookName, + listenerNameOrHook: HookConfig[HookName] | string, + hook?: HookConfig[HookName], + ): This { + hooksReworked(); + + if (hook) { + // @ts-expect-error + hookHandlerBuilder.getFor(this).addListener(hookName, hook, listenerNameOrHook); + } else { + // @ts-expect-error + hookHandlerBuilder.getFor(this).addListener(hookName, listenerNameOrHook); + } + + return this; + }; +} + +export interface LegacyAddHookFunction { + /** + * Adds a hook listener + */ + (this: This, hook: Fn): This; + + /** + * Adds a hook listener + * + * @param listenerName Provide a name for the hook function. It can be used to remove the hook later. + */ + (this: This, listenerName: string, hook: Fn): This; +} + +export function legacyBuildAddHook( + hookHandlerBuilder: HookHandlerBuilder, + hookName: HookName, +): LegacyAddHookFunction { + return function addHook( + this: This, + listenerNameOrHook: HookConfig[HookName] | string, + hook?: HookConfig[HookName], + ): This { + hooksReworked(); + + if (hook) { + // @ts-expect-error + hookHandlerBuilder.getFor(this).addListener(hookName, hook, listenerNameOrHook); + } else { + // @ts-expect-error + return hookHandlerBuilder.getFor(this).addListener(hookName, listenerNameOrHook); + } + + return this; + }; +} + +export function legacyBuildHasHook(hookHandlerBuilder: HookHandlerBuilder) { + return function hasHook(this: object, hookName: HookName): boolean { + hooksReworked(); + + return hookHandlerBuilder.getFor(this).hasListeners(hookName); + }; +} + +export function legacyBuildRemoveHook(hookHandlerBuilder: HookHandlerBuilder) { + return function removeHook( + this: object, + hookName: HookName, + listenerNameOrListener: HookConfig[HookName] | string, + ): void { + hooksReworked(); + + return hookHandlerBuilder.getFor(this).removeListener(hookName, listenerNameOrListener); + }; +} diff --git a/src/hooks.d.ts b/src/hooks.d.ts deleted file mode 100644 index a4d2a0138f02..000000000000 --- a/src/hooks.d.ts +++ /dev/null @@ -1,198 +0,0 @@ -import type { - BeforeAssociateEventData, - AfterAssociateEventData, - AssociationOptions, -} from './associations'; -import type { AbstractQuery } from './dialects/abstract/query'; -import type { ValidationOptions } from './instance-validator'; -import type { - Model, - BulkCreateOptions, - CountOptions, - CreateOptions, - DestroyOptions, FindOptions, - InstanceDestroyOptions, - InstanceRestoreOptions, - InstanceUpdateOptions, - ModelAttributes, - ModelOptions, RestoreOptions, UpdateOptions, UpsertOptions, - Attributes, CreationAttributes, ModelStatic, -} from './model'; -import type { Config, Options, Sequelize, SyncOptions, QueryOptions } from './sequelize'; -import type { DeepWriteable } from './utils'; - -export type HookReturn = Promise | void; - -/** - * Options for Model.init. We mostly duplicate the Hooks here, since there is no way to combine the two - * interfaces. - */ -export interface ModelHooks { - beforeValidate(instance: M, options: ValidationOptions): HookReturn; - afterValidate(instance: M, options: ValidationOptions): HookReturn; - validationFailed(instance: M, options: ValidationOptions, error: unknown): HookReturn; - beforeCreate(attributes: M, options: CreateOptions): HookReturn; - afterCreate(attributes: M, options: CreateOptions): HookReturn; - beforeDestroy(instance: M, options: InstanceDestroyOptions): HookReturn; - afterDestroy(instance: M, options: InstanceDestroyOptions): HookReturn; - beforeRestore(instance: M, options: InstanceRestoreOptions): HookReturn; - afterRestore(instance: M, options: InstanceRestoreOptions): HookReturn; - beforeUpdate(instance: M, options: InstanceUpdateOptions): HookReturn; - afterUpdate(instance: M, options: InstanceUpdateOptions): HookReturn; - beforeUpsert(attributes: M, options: UpsertOptions): HookReturn; - afterUpsert(attributes: [ M, boolean | null ], options: UpsertOptions): HookReturn; - beforeSave( - instance: M, - options: InstanceUpdateOptions | CreateOptions - ): HookReturn; - afterSave( - instance: M, - options: InstanceUpdateOptions | CreateOptions - ): HookReturn; - beforeBulkCreate(instances: M[], options: BulkCreateOptions): HookReturn; - afterBulkCreate(instances: readonly M[], options: BulkCreateOptions): HookReturn; - beforeBulkDestroy(options: DestroyOptions): HookReturn; - afterBulkDestroy(options: DestroyOptions): HookReturn; - beforeBulkRestore(options: RestoreOptions): HookReturn; - afterBulkRestore(options: RestoreOptions): HookReturn; - beforeBulkUpdate(options: UpdateOptions): HookReturn; - afterBulkUpdate(options: UpdateOptions): HookReturn; - beforeFind(options: FindOptions): HookReturn; - beforeCount(options: CountOptions): HookReturn; - beforeFindAfterExpandIncludeAll(options: FindOptions): HookReturn; - beforeFindAfterOptions(options: FindOptions): HookReturn; - afterFind(instancesOrInstance: readonly M[] | M | null, options: FindOptions): HookReturn; - beforeSync(options: SyncOptions): HookReturn; - afterSync(options: SyncOptions): HookReturn; - beforeBulkSync(options: SyncOptions): HookReturn; - afterBulkSync(options: SyncOptions): HookReturn; - beforeQuery(options: QueryOptions, query: AbstractQuery): HookReturn; - afterQuery(options: QueryOptions, query: AbstractQuery): HookReturn; - beforeAssociate(data: BeforeAssociateEventData, options: AssociationOptions): HookReturn; - afterAssociate(data: AfterAssociateEventData, options: AssociationOptions): HookReturn; -} - -export interface SequelizeHooks< - M extends Model = Model, - TAttributes extends {} = any, - TCreationAttributes extends {} = TAttributes, -> extends ModelHooks { - beforeDefine(attributes: ModelAttributes, options: ModelOptions): void; - afterDefine(model: ModelStatic): void; - beforeInit(config: Config, options: Options): void; - afterInit(sequelize: Sequelize): void; - beforeConnect(config: DeepWriteable): HookReturn; - afterConnect(connection: unknown, config: Config): HookReturn; - beforeDisconnect(connection: unknown): HookReturn; - afterDisconnect(connection: unknown): HookReturn; -} - -/** - * Virtual class for deduplication - */ -export class Hooks< - M extends Model = Model, - TModelAttributes extends {} = any, - TCreationAttributes extends {} = TModelAttributes, -> { - /** - * A dummy variable that doesn't exist on the real object. This exists so - * Typescript can infer the type of the attributes in static functions. Don't - * try to access this! - */ - _model: M; - /** - * A similar dummy variable that doesn't exist on the real object. Do not - * try to access this in real code. - * - * @deprecated This property will become a Symbol in v7 to prevent collisions. - * Use Attributes instead of this property to be forward-compatible. - */ - _attributes: TModelAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts) - /** - * A similar dummy variable that doesn't exist on the real object. Do not - * try to access this in real code. - * - * @deprecated This property will become a Symbol in v7 to prevent collisions. - * Use CreationAttributes instead of this property to be forward-compatible. - */ - _creationAttributes: TCreationAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts) - - /** - * Add a hook to the model - * - * @param name Provide a name for the hook function. It can be used to remove the hook later or to order - * hooks based on some sort of priority system in the future. - */ - static addHook< - H extends Hooks, - K extends keyof SequelizeHooks, CreationAttributes>, - >( - this: HooksStatic, - hookType: K, - name: string, - fn: SequelizeHooks, CreationAttributes>[K] - ): HooksCtor; - static addHook< - H extends Hooks, - K extends keyof SequelizeHooks, CreationAttributes>, - >( - this: HooksStatic, - hookType: K, - fn: SequelizeHooks, CreationAttributes>[K] - ): HooksCtor; - - /** - * Remove hook from the model - */ - static removeHook( - this: HooksStatic, - hookType: keyof SequelizeHooks, CreationAttributes>, - name: string, - ): HooksCtor; - - /** - * Check whether the mode has any hooks of this type - */ - static hasHook( - this: HooksStatic, - hookType: keyof SequelizeHooks, CreationAttributes>, - ): boolean; - static hasHooks( - this: HooksStatic, - hookType: keyof SequelizeHooks, CreationAttributes>, - ): boolean; - - /** - * Add a hook to the model - * - * @param name Provide a name for the hook function. It can be used to remove the hook later or to order - * hooks based on some sort of priority system in the future. - */ - addHook>( - hookType: K, - name: string, - fn: SequelizeHooks[K] - ): this; - addHook>( - hookType: K, fn: SequelizeHooks[K]): this; - /** - * Remove hook from the model - */ - removeHook>( - hookType: K, - name: string - ): this; - - /** - * Check whether the mode has any hooks of this type - */ - hasHook>(hookType: K): boolean; - hasHooks>(hookType: K): boolean; - - runHooks(name: string, ...params: unknown[]): Promise; -} - -export type HooksCtor = typeof Hooks & { new(): H }; - -export type HooksStatic = { new(): H }; diff --git a/src/hooks.js b/src/hooks.js deleted file mode 100644 index 23953d6ed8bc..000000000000 --- a/src/hooks.js +++ /dev/null @@ -1,603 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const { logger } = require('./utils/logger'); - -const debug = logger.debugContext('hooks'); - -const hookTypes = { - beforeValidate: { params: 2 }, - afterValidate: { params: 2 }, - validationFailed: { params: 3 }, - beforeCreate: { params: 2 }, - afterCreate: { params: 2 }, - beforeDestroy: { params: 2 }, - afterDestroy: { params: 2 }, - beforeRestore: { params: 2 }, - afterRestore: { params: 2 }, - beforeUpdate: { params: 2 }, - afterUpdate: { params: 2 }, - beforeSave: { params: 2, proxies: ['beforeUpdate', 'beforeCreate'] }, - afterSave: { params: 2, proxies: ['afterUpdate', 'afterCreate'] }, - beforeUpsert: { params: 2 }, - afterUpsert: { params: 2 }, - beforeBulkCreate: { params: 2 }, - afterBulkCreate: { params: 2 }, - beforeBulkDestroy: { params: 1 }, - afterBulkDestroy: { params: 1 }, - beforeBulkRestore: { params: 1 }, - afterBulkRestore: { params: 1 }, - beforeBulkUpdate: { params: 1 }, - afterBulkUpdate: { params: 1 }, - beforeFind: { params: 1 }, - beforeFindAfterExpandIncludeAll: { params: 1 }, - beforeFindAfterOptions: { params: 1 }, - afterFind: { params: 2 }, - beforeCount: { params: 1 }, - beforeDefine: { params: 2, sync: true, noModel: true }, - afterDefine: { params: 1, sync: true, noModel: true }, - beforeInit: { params: 2, sync: true, noModel: true }, - afterInit: { params: 1, sync: true, noModel: true }, - beforeAssociate: { params: 2, sync: true }, - afterAssociate: { params: 2, sync: true }, - beforeConnect: { params: 1, noModel: true }, - afterConnect: { params: 2, noModel: true }, - beforeDisconnect: { params: 1, noModel: true }, - afterDisconnect: { params: 1, noModel: true }, - beforeSync: { params: 1 }, - afterSync: { params: 1 }, - beforeBulkSync: { params: 1 }, - afterBulkSync: { params: 1 }, - beforeQuery: { params: 2 }, - afterQuery: { params: 2 }, -}; -export const hooks = hookTypes; - -/** - * get array of current hook and its proxies combined - * - * @param {string} hookType any hook type @see {@link hookTypes} - * - * @private - */ -const getProxiedHooks = hookType => (hookTypes[hookType].proxies - ? hookTypes[hookType].proxies.concat(hookType) - : [hookType]); - -function getHooks(hooked, hookType) { - return (hooked.options.hooks || {})[hookType] || []; -} - -const Hooks = { - /** - * Process user supplied hooks definition - * - * @param {object} hooks hooks definition - * - * @private - * @memberof Sequelize - * @memberof Sequelize.Model - */ - _setupHooks(hooks) { - this.options.hooks = {}; - _.map(hooks || {}, (hooksArray, hookName) => { - if (!Array.isArray(hooksArray)) { - hooksArray = [hooksArray]; - } - - for (const hookFn of hooksArray) { - this.addHook(hookName, hookFn); - } - }); - }, - - async runHooks(hooks, ...hookArgs) { - if (!hooks) { - throw new Error('runHooks requires at least 1 argument'); - } - - let hookType; - - if (typeof hooks === 'string') { - hookType = hooks; - hooks = getHooks(this, hookType); - - if (this.sequelize) { - hooks = hooks.concat(getHooks(this.sequelize, hookType)); - } - } - - if (!Array.isArray(hooks)) { - hooks = [hooks]; - } - - // synchronous hooks - if (hookTypes[hookType] && hookTypes[hookType].sync) { - for (let hook of hooks) { - if (typeof hook === 'object') { - hook = hook.fn; - } - - debug(`running hook(sync) ${hookType}`); - hook.apply(this, hookArgs); - } - - return; - } - - // asynchronous hooks (default) - for (let hook of hooks) { - if (typeof hook === 'object') { - hook = hook.fn; - } - - debug(`running hook ${hookType}`); - await hook.apply(this, hookArgs); - } - }, - - /** - * Add a hook to the model - * - * @param {string} hookType hook name @see {@link hookTypes} - * @param {string|Function} [name] Provide a name for the hook function. It can be used to remove the hook later or to order hooks based on some sort of priority system in the future. - * @param {Function} fn The hook function - * - * @memberof Sequelize - * @memberof Sequelize.Model - */ - addHook(hookType, name, fn) { - if (typeof name === 'function') { - fn = name; - name = null; - } - - debug(`adding hook ${hookType}`); - // check for proxies, add them too - hookType = getProxiedHooks(hookType); - - for (const type of hookType) { - const hooks = getHooks(this, type); - hooks.push(name ? { name, fn } : fn); - this.options.hooks[type] = hooks; - } - - return this; - }, - - /** - * Remove hook from the model - * - * @param {string} hookType @see {@link hookTypes} - * @param {string|Function} name name of hook or function reference which was attached - * - * @memberof Sequelize - * @memberof Sequelize.Model - */ - removeHook(hookType, name) { - const isReference = typeof name === 'function'; - - if (!this.hasHook(hookType)) { - return this; - } - - debug(`removing hook ${hookType}`); - - // check for proxies, add them too - hookType = getProxiedHooks(hookType); - - for (const type of hookType) { - this.options.hooks[type] = this.options.hooks[type].filter(hook => { - if (isReference && typeof hook === 'function') { - return hook !== name; // check if same method - } - - if (!isReference && typeof hook === 'object') { - return hook.name !== name; - } - - return true; - }); - } - - return this; - }, - - /** - * Check whether the mode has any hooks of this type - * - * @param {string} hookType @see {@link hookTypes} - * - * @alias hasHooks - * - * @memberof Sequelize - * @memberof Sequelize.Model - */ - hasHook(hookType) { - return this.options.hooks[hookType] && this.options.hooks[hookType].length > 0; - }, -}; -Hooks.hasHooks = Hooks.hasHook; - -export function applyTo(target, isModel = false) { - _.mixin(target, Hooks); - - for (const hook of Object.keys(hookTypes)) { - if (isModel && hookTypes[hook].noModel) { - continue; - } - - target[hook] = function (name, callback) { - return this.addHook(hook, name, callback); - }; - } -} - -/** - * A hook that is run before validation - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * @name beforeValidate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after validation - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * @name afterValidate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run when validation fails - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options, error. Error is the - * SequelizeValidationError. If the callback throws an error, it will replace the original validation error. - * @name validationFailed - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before creating a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with attributes, options - * @name beforeCreate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after creating a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with attributes, options - * @name afterCreate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before creating or updating a single instance, It proxies `beforeCreate` and `beforeUpdate` - * - * @param {string} name - * @param {Function} fn A callback function that is called with attributes, options - * @name beforeSave - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before upserting - * - * @param {string} name - * @param {Function} fn A callback function that is called with attributes, options - * @name beforeUpsert - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after upserting - * - * @param {string} name - * @param {Function} fn A callback function that is called with the result of upsert(), options - * @name afterUpsert - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after creating or updating a single instance, It proxies `afterCreate` and `afterUpdate` - * - * @param {string} name - * @param {Function} fn A callback function that is called with attributes, options - * @name afterSave - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before destroying a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * - * @name beforeDestroy - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after destroying a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * - * @name afterDestroy - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before restoring a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * - * @name beforeRestore - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after restoring a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * - * @name afterRestore - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before updating a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * @name beforeUpdate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after updating a single instance - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance, options - * @name afterUpdate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before creating instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with instances, options - * @name beforeBulkCreate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after creating instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with instances, options - * @name afterBulkCreate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before destroying instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * - * @name beforeBulkDestroy - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after destroying instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * - * @name afterBulkDestroy - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before restoring instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * - * @name beforeBulkRestore - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after restoring instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * - * @name afterBulkRestore - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before updating instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * @name beforeBulkUpdate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after updating instances in bulk - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * @name afterBulkUpdate - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before a find (select) query - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * @name beforeFind - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * @name beforeFindAfterExpandIncludeAll - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before a find (select) query, after all option parsing is complete - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * @name beforeFindAfterOptions - * @memberof Sequelize.Model - */ - -/** - * A hook that is run after a find (select) query - * - * @param {string} name - * @param {Function} fn A callback function that is called with instance(s), options - * @name afterFind - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before a count query - * - * @param {string} name - * @param {Function} fn A callback function that is called with options - * @name beforeCount - * @memberof Sequelize.Model - */ - -/** - * A hook that is run before a define call - * - * @param {string} name - * @param {Function} fn A callback function that is called with attributes, options - * @name beforeDefine - * @memberof Sequelize - */ - -/** - * A hook that is run after a define call - * - * @param {string} name - * @param {Function} fn A callback function that is called with factory - * @name afterDefine - * @memberof Sequelize - */ - -/** - * A hook that is run before Sequelize() call - * - * @param {string} name - * @param {Function} fn A callback function that is called with config, options - * @name beforeInit - * @memberof Sequelize - */ - -/** - * A hook that is run after Sequelize() call - * - * @param {string} name - * @param {Function} fn A callback function that is called with sequelize - * @name afterInit - * @memberof Sequelize - */ - -/** - * A hook that is run before a connection is created - * - * @param {string} name - * @param {Function} fn A callback function that is called with config passed to connection - * @name beforeConnect - * @memberof Sequelize - */ - -/** - * A hook that is run after a connection is created - * - * @param {string} name - * @param {Function} fn A callback function that is called with the connection object and the config passed to connection - * @name afterConnect - * @memberof Sequelize - */ - -/** - * A hook that is run before a connection is disconnected - * - * @param {string} name - * @param {Function} fn A callback function that is called with the connection object - * @name beforeDisconnect - * @memberof Sequelize - */ - -/** - * A hook that is run after a connection is disconnected - * - * @param {string} name - * @param {Function} fn A callback function that is called with the connection object - * @name afterDisconnect - * @memberof Sequelize - */ - -/** - * A hook that is run before Model.sync call - * - * @param {string} name - * @param {Function} fn A callback function that is called with options passed to Model.sync - * @name beforeSync - * @memberof Sequelize - */ - -/** - * A hook that is run after Model.sync call - * - * @param {string} name - * @param {Function} fn A callback function that is called with options passed to Model.sync - * @name afterSync - * @memberof Sequelize - */ - -/** - * A hook that is run before sequelize.sync call - * - * @param {string} name - * @param {Function} fn A callback function that is called with options passed to sequelize.sync - * @name beforeBulkSync - * @memberof Sequelize - */ - -/** - * A hook that is run after sequelize.sync call - * - * @param {string} name - * @param {Function} fn A callback function that is called with options passed to sequelize.sync - * @name afterBulkSync - * @memberof Sequelize - */ diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 000000000000..f1390c7705ae --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,193 @@ +import type { Nullish, AllowArray } from './utils/index.js'; +import { Multimap } from './utils/multimap.js'; + +export type AsyncHookReturn = Promise | void; + +type HookParameters = Hook extends (...args2: any) => any + ? Parameters + : never; + +type OnRunHook = ( + eventTarget: object, + isAsync: boolean, + hookName: HookName, + args: HookParameters +) => AsyncHookReturn; + +/** + * @internal + */ +export class HookHandler { + #validHookNames: Array; + #eventTarget: object; + #listeners = new Multimap, callback: HookConfig[keyof HookConfig] }>(); + #onRunHook: OnRunHook | undefined; + + constructor( + eventTarget: object, + validHookNames: Array, + onRunHook?: OnRunHook, + ) { + this.#eventTarget = eventTarget; + this.#validHookNames = validHookNames; + this.#onRunHook = onRunHook; + } + + removeListener( + hookName: HookName, + listenerOrListenerName: string | HookConfig[HookName], + ): void { + this.#assertValidHookName(hookName); + + if (typeof listenerOrListenerName === 'string') { + const listener = this.#getNamedListener(hookName, listenerOrListenerName); + if (listener) { + this.#listeners.delete(hookName, listener); + } + } else { + const listeners = this.#listeners.getAll(hookName); + for (const listener of listeners) { + if (listener.callback === listenerOrListenerName) { + this.#listeners.delete(hookName, listener); + } + } + } + } + + removeAllListeners() { + this.#listeners.clear(); + } + + #getNamedListener( + hookName: HookName, + listenerName: string, + ): { listenerName: Nullish, callback: HookConfig[keyof HookConfig] } | null { + const listeners = this.#listeners.getAll(hookName); + for (const listener of listeners) { + if (listener.listenerName === listenerName) { + return listener; + } + } + + return null; + } + + hasListeners(hookName: keyof HookConfig): boolean { + this.#assertValidHookName(hookName); + + return this.#listeners.count(hookName) > 0; + } + + runSync( + hookName: HookName, + ...args: HookConfig[HookName] extends (...args2: any) => any + ? Parameters + : never + ): void { + this.#assertValidHookName(hookName); + + const listeners = this.#listeners.getAll(hookName); + for (const listener of listeners) { + // @ts-expect-error + const out = listener.callback(...args); + + if (out && 'then' in out) { + throw new Error(`${listener.listenerName ? `Listener ${listener.listenerName}` : `An unnamed listener`} of hook ${String(hookName)} on ${getName(this.#eventTarget)} returned a Promise, but the hook is synchronous.`); + } + } + + if (this.#onRunHook) { + void this.#onRunHook(this.#eventTarget, false, hookName, args); + } + } + + async runAsync( + hookName: HookName, + ...args: HookConfig[HookName] extends (...args2: any) => any + ? Parameters + : never + ): Promise { + this.#assertValidHookName(hookName); + + const listeners = this.#listeners.getAll(hookName); + for (const listener of listeners) { + /* eslint-disable no-await-in-loop */ + // @ts-expect-error + await listener.callback(...args); + /* eslint-enable no-await-in-loop */ + } + + if (this.#onRunHook) { + await this.#onRunHook(this.#eventTarget, true, hookName, args); + } + } + + addListener( + hookName: HookName, + listener: HookConfig[HookName], + listenerName?: string, + ): void { + this.#assertValidHookName(hookName); + + if (listenerName) { + const existingListener = this.#getNamedListener(hookName, listenerName); + + if (existingListener) { + throw new Error(`Named listener ${listenerName} already exists for hook ${String(hookName)} on ${getName(this.#eventTarget)}.`); + } + } + + this.#listeners.append(hookName, { callback: listener, listenerName }); + } + + addListeners(listeners: { + [Key in keyof HookConfig]?: AllowArray + }) { + for (const hookName of this.#validHookNames) { + const hookListeners = listeners[hookName]; + if (!hookListeners) { + continue; + } + + const hookListenersArray = Array.isArray(hookListeners) ? hookListeners : [hookListeners]; + for (const listener of hookListenersArray) { + this.addListener(hookName, listener); + } + } + } + + #assertValidHookName(hookName: any) { + if (!this.#validHookNames.includes(hookName)) { + throw new Error(`Target ${getName(this.#eventTarget)} does not support a hook named "${String(hookName)}".`); + } + } +} + +export class HookHandlerBuilder { + #validHookNames: Array; + #hookHandlers = new WeakMap>(); + #onRunHook: OnRunHook | undefined; + + constructor(validHookNames: Array, onRunHook?: OnRunHook) { + this.#validHookNames = validHookNames; + this.#onRunHook = onRunHook; + } + + getFor(target: object): HookHandler { + let hookHandler = this.#hookHandlers.get(target); + if (!hookHandler) { + hookHandler = new HookHandler(target, this.#validHookNames, this.#onRunHook); + this.#hookHandlers.set(target, hookHandler); + } + + return hookHandler; + } +} + +function getName(obj: object) { + if (typeof obj === 'function') { + return `[class ${obj.name}]`; + } + + return `[instance ${obj.constructor.name}]`; +} diff --git a/src/instance-validator.js b/src/instance-validator.js index 1b2098a26632..154c25c65f13 100644 --- a/src/instance-validator.js +++ b/src/instance-validator.js @@ -106,17 +106,16 @@ export class InstanceValidator { * @private */ async _validateAndRunHooks() { - const runHooks = this.modelInstance.constructor.runHooks.bind(this.modelInstance.constructor); - await runHooks('beforeValidate', this.modelInstance, this.options); + await this.modelInstance.constructor.hooks.runAsync('beforeValidate', this.modelInstance, this.options); try { await this._validate(); } catch (error) { - const newError = await runHooks('validationFailed', this.modelInstance, this.options, error); + const newError = await this.modelInstance.constructor.hooks.runAsync('validationFailed', this.modelInstance, this.options, error); throw newError || error; } - await runHooks('afterValidate', this.modelInstance, this.options); + await this.modelInstance.constructor.hooks.runAsync('afterValidate', this.modelInstance, this.options); return this.modelInstance; } diff --git a/src/model-typescript.ts b/src/model-typescript.ts new file mode 100644 index 000000000000..50830da9a626 --- /dev/null +++ b/src/model-typescript.ts @@ -0,0 +1,199 @@ +import { + legacyBuildAddAnyHook, + legacyBuildAddHook, + legacyBuildHasHook, + 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 type { + AfterAssociateEventData, + AssociationOptions, + BeforeAssociateEventData, + BulkCreateOptions, + CountOptions, + CreateOptions, + DestroyOptions, + FindOptions, + InstanceDestroyOptions, + InstanceRestoreOptions, + InstanceUpdateOptions, + Model, ModelStatic, + RestoreOptions, + SyncOptions, + UpdateOptions, + UpsertOptions, +} 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; + + /** + * 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; +} + +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.'); + } + + if (isAsync) { + await model.sequelize.hooks.runAsync(hookName, ...args); + } else { + model.sequelize.hooks.runSync(hookName, ...args); + } +}); + +// 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 hooks() { + return staticModelHooks.getFor(this); + } + + static addHook = legacyBuildAddAnyHook(staticModelHooks); + static hasHook = legacyBuildHasHook(staticModelHooks); + static hasHooks = legacyBuildHasHook(staticModelHooks); + static removeHook = legacyBuildRemoveHook(staticModelHooks); + static runHooks = legacyBuildRunHook(staticModelHooks); + + static beforeValidate = legacyBuildAddHook(staticModelHooks, 'beforeValidate'); + static afterValidate = legacyBuildAddHook(staticModelHooks, 'afterValidate'); + static validationFailed = legacyBuildAddHook(staticModelHooks, 'validationFailed'); + + static beforeCreate = legacyBuildAddHook(staticModelHooks, 'beforeCreate'); + static afterCreate = legacyBuildAddHook(staticModelHooks, 'afterCreate'); + + static beforeDestroy = legacyBuildAddHook(staticModelHooks, 'beforeDestroy'); + static afterDestroy = legacyBuildAddHook(staticModelHooks, 'afterDestroy'); + + static beforeRestore = legacyBuildAddHook(staticModelHooks, 'beforeRestore'); + static afterRestore = legacyBuildAddHook(staticModelHooks, 'afterRestore'); + + static beforeUpdate = legacyBuildAddHook(staticModelHooks, 'beforeUpdate'); + static afterUpdate = legacyBuildAddHook(staticModelHooks, 'afterUpdate'); + + static beforeUpsert = legacyBuildAddHook(staticModelHooks, 'beforeUpsert'); + static afterUpsert = legacyBuildAddHook(staticModelHooks, 'afterUpsert'); + + static beforeSave = legacyBuildAddHook(staticModelHooks, 'beforeSave'); + static afterSave = legacyBuildAddHook(staticModelHooks, 'afterSave'); + + static beforeBulkCreate = legacyBuildAddHook(staticModelHooks, 'beforeBulkCreate'); + static afterBulkCreate = legacyBuildAddHook(staticModelHooks, 'afterBulkCreate'); + + static beforeBulkDestroy = legacyBuildAddHook(staticModelHooks, 'beforeBulkDestroy'); + static afterBulkDestroy = legacyBuildAddHook(staticModelHooks, 'afterBulkDestroy'); + + static beforeBulkRestore = legacyBuildAddHook(staticModelHooks, 'beforeBulkRestore'); + static afterBulkRestore = legacyBuildAddHook(staticModelHooks, 'afterBulkRestore'); + + static beforeBulkUpdate = legacyBuildAddHook(staticModelHooks, 'beforeBulkUpdate'); + static afterBulkUpdate = legacyBuildAddHook(staticModelHooks, 'afterBulkUpdate'); + + static beforeCount = legacyBuildAddHook(staticModelHooks, 'beforeCount'); + + static beforeFind = legacyBuildAddHook(staticModelHooks, 'beforeFind'); + static beforeFindAfterExpandIncludeAll = legacyBuildAddHook(staticModelHooks, 'beforeFindAfterExpandIncludeAll'); + static beforeFindAfterOptions = legacyBuildAddHook(staticModelHooks, 'beforeFindAfterOptions'); + static afterFind = legacyBuildAddHook(staticModelHooks, 'afterFind'); + + static beforeSync = legacyBuildAddHook(staticModelHooks, 'beforeSync'); + static afterSync = legacyBuildAddHook(staticModelHooks, 'afterSync'); + + static beforeAssociate = legacyBuildAddHook(staticModelHooks, 'beforeAssociate'); + static afterAssociate = legacyBuildAddHook(staticModelHooks, 'afterAssociate'); +} diff --git a/src/model.d.ts b/src/model.d.ts index 35af88d58204..8c8f95fd1463 100644 --- a/src/model.d.ts +++ b/src/model.d.ts @@ -1,8 +1,5 @@ import type { Class } from 'type-fest'; import type { - AfterAssociateEventData, - AssociationOptions, - BeforeAssociateEventData, Association, BelongsTo, BelongsToMany, @@ -16,10 +13,10 @@ import type { import type { DataType } from './data-types'; import type { Deferrable } from './deferrable'; import type { IndexOptions, TableName } from './dialects/abstract/query-interface'; -import type { HookReturn, ModelHooks } from './hooks'; -import { Hooks } from './hooks'; import type { IndexHints } from './index-hints'; import type { ValidationOptions } from './instance-validator'; +import type { ModelHooks } from './model-typescript.js'; +import { ModelTypeScript } from './model-typescript.js'; import type { Sequelize, SyncOptions, QueryOptions } from './sequelize'; import type { AllowArray, @@ -2072,7 +2069,7 @@ export interface ModelGetOptions { } export abstract class Model - extends Hooks, TModelAttributes, TCreationAttributes> { + extends ModelTypeScript { /** * A dummy variable that doesn't exist on the real object. This exists so * Typescript can infer the type of the attributes in static functions. Don't @@ -2896,374 +2893,6 @@ export abstract class Model> ): Promise<[affectedRows: M[], affectedCount?: number]>; - /** - * A hook that is run before validation - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeValidate( - this: ModelStatic, - name: string, - fn: (instance: M, options: ValidationOptions) => HookReturn - ): void; - static beforeValidate( - this: ModelStatic, - fn: (instance: M, options: ValidationOptions) => HookReturn - ): void; - - /** - * A hook that is run after validation - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterValidate( - this: ModelStatic, - name: string, - fn: (instance: M, options: ValidationOptions) => HookReturn - ): void; - static afterValidate( - this: ModelStatic, - fn: (instance: M, options: ValidationOptions) => HookReturn - ): void; - - /** - * A hook that is run before creating a single instance - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - static beforeCreate( - this: ModelStatic, - name: string, - fn: (instance: M, options: CreateOptions>) => HookReturn - ): void; - static beforeCreate( - this: ModelStatic, - fn: (instance: M, options: CreateOptions>) => HookReturn - ): void; - - /** - * A hook that is run after creating a single instance - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - static afterCreate( - this: ModelStatic, - name: string, - fn: (instance: M, options: CreateOptions>) => HookReturn - ): void; - static afterCreate( - this: ModelStatic, - fn: (instance: M, options: CreateOptions>) => HookReturn - ): void; - - /** - * A hook that is run before destroying a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeDestroy( - this: ModelStatic, - name: string, - fn: (instance: M, options: InstanceDestroyOptions) => HookReturn - ): void; - static beforeDestroy( - this: ModelStatic, - fn: (instance: M, options: InstanceDestroyOptions) => HookReturn - ): void; - - /** - * A hook that is run after destroying a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterDestroy( - this: ModelStatic, - name: string, - fn: (instance: M, options: InstanceDestroyOptions) => HookReturn - ): void; - static afterDestroy( - this: ModelStatic, - fn: (instance: M, options: InstanceDestroyOptions) => HookReturn - ): void; - - /** - * A hook that is run before updating a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeUpdate( - this: ModelStatic, - name: string, - fn: (instance: M, options: UpdateOptions>) => HookReturn - ): void; - static beforeUpdate( - this: ModelStatic, - fn: (instance: M, options: UpdateOptions>) => HookReturn - ): void; - - /** - * A hook that is run after updating a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterUpdate( - this: ModelStatic, - name: string, - fn: (instance: M, options: UpdateOptions>) => HookReturn - ): void; - static afterUpdate( - this: ModelStatic, - fn: (instance: M, options: UpdateOptions>) => HookReturn - ): void; - - /** - * A hook that is run before creating or updating a single instance, It proxies `beforeCreate` and `beforeUpdate` - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeSave( - this: ModelStatic, - name: string, - fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn - ): void; - static beforeSave( - this: ModelStatic, - fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn - ): void; - - /** - * A hook that is run after creating or updating a single instance, It proxies `afterCreate` and `afterUpdate` - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterSave( - this: ModelStatic, - name: string, - fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn - ): void; - static afterSave( - this: ModelStatic, - fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn - ): void; - - /** - * A hook that is run before creating instances in bulk - * - * @param name - * @param fn A callback function that is called with instances, options - */ - static beforeBulkCreate( - this: ModelStatic, - name: string, - fn: (instances: M[], options: BulkCreateOptions>) => HookReturn - ): void; - static beforeBulkCreate( - this: ModelStatic, - fn: (instances: M[], options: BulkCreateOptions>) => HookReturn - ): void; - - /** - * A hook that is run after creating instances in bulk - * - * @param name - * @param fn A callback function that is called with instances, options - */ - static afterBulkCreate( - this: ModelStatic, - name: string, - fn: (instances: readonly M[], options: BulkCreateOptions>) => HookReturn - ): void; - static afterBulkCreate( - this: ModelStatic, - fn: (instances: readonly M[], options: BulkCreateOptions>) => HookReturn - ): void; - - /** - * A hook that is run before destroying instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeBulkDestroy( - this: ModelStatic, - name: string, fn: (options: BulkCreateOptions>) => HookReturn): void; - static beforeBulkDestroy( - this: ModelStatic, - fn: (options: BulkCreateOptions>) => HookReturn - ): void; - - /** - * A hook that is run after destroying instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static afterBulkDestroy( - this: ModelStatic, - name: string, fn: (options: DestroyOptions>) => HookReturn - ): void; - static afterBulkDestroy( - this: ModelStatic, - fn: (options: DestroyOptions>) => HookReturn - ): void; - - /** - * A hook that is run after updating instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeBulkUpdate( - this: ModelStatic, - name: string, fn: (options: UpdateOptions>) => HookReturn - ): void; - static beforeBulkUpdate( - this: ModelStatic, - fn: (options: UpdateOptions>) => HookReturn - ): void; - - /** - * A hook that is run after updating instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static afterBulkUpdate( - this: ModelStatic, - name: string, fn: (options: UpdateOptions>) => HookReturn - ): void; - static afterBulkUpdate( - this: ModelStatic, - fn: (options: UpdateOptions>) => HookReturn - ): void; - - /** - * A hook that is run before a find (select) query - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeFind( - this: ModelStatic, - name: string, fn: (options: FindOptions>) => HookReturn - ): void; - static beforeFind( - this: ModelStatic, - fn: (options: FindOptions>) => HookReturn - ): void; - - /** - * A hook that is run before a count query - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeCount( - this: ModelStatic, - name: string, fn: (options: CountOptions>) => HookReturn - ): void; - static beforeCount( - this: ModelStatic, - fn: (options: CountOptions>) => HookReturn - ): void; - - /** - * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeFindAfterExpandIncludeAll( - this: ModelStatic, - name: string, fn: (options: FindOptions>) => HookReturn - ): void; - static beforeFindAfterExpandIncludeAll( - this: ModelStatic, - fn: (options: FindOptions>) => HookReturn - ): void; - - /** - * A hook that is run before a find (select) query, after all option parsing is complete - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeFindAfterOptions( - this: ModelStatic, - name: string, fn: (options: FindOptions>) => HookReturn - ): void; - static beforeFindAfterOptions( - this: ModelStatic, - fn: (options: FindOptions>) => void - ): HookReturn; - - /** - * A hook that is run after a find (select) query - * - * @param name - * @param fn A callback function that is called with instance(s), options - */ - static afterFind( - this: ModelStatic, - name: string, - fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions>) => HookReturn - ): void; - static afterFind( - this: ModelStatic, - fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions>) => HookReturn - ): void; - - /** - * A hook that is run before sequelize.sync call - * - * @param fn A callback function that is called with options passed to sequelize.sync - */ - static beforeBulkSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - static beforeBulkSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run after sequelize.sync call - * - * @param fn A callback function that is called with options passed to sequelize.sync - */ - static afterBulkSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - static afterBulkSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run before Model.sync call - * - * @param fn A callback function that is called with options passed to Model.sync - */ - static beforeSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - static beforeSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run after Model.sync call - * - * @param fn A callback function that is called with options passed to Model.sync - */ - static afterSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - static afterSync(fn: (options: SyncOptions) => HookReturn): void; - - static beforeAssociate(name: string, fn: (data: BeforeAssociateEventData, options: AssociationOptions) => void): void; - static beforeAssociate(fn: (data: BeforeAssociateEventData, options: AssociationOptions) => void): void; - - static afterAssociate(name: string, fn: (data: BeforeAssociateEventData, options: AssociationOptions) => void): void; - static afterAssociate(fn: (data: BeforeAssociateEventData, options: AssociationOptions) => void): void; - - static runHooks(name: 'beforeAssociate', data: BeforeAssociateEventData, options: AssociationOptions): void; - static runHooks(name: 'afterAssociate', data: AfterAssociateEventData, options: AssociationOptions): void; - /** * Creates a 1:1 association between this model (the source) and the provided target. * The foreign key is added on the target model. @@ -3824,7 +3453,7 @@ type InternalInferAttributeKeysFromFields(modelClass: ModelStatic, attributes: CreationAttributes) {} * ``` */ -export type CreationAttributes = MakeNullishOptional; +export type CreationAttributes = MakeNullishOptional; /** * Returns the creation attributes of a given Model. @@ -3837,6 +3466,6 @@ export type CreationAttributes = MakeNullishOptional(modelClass: ModelStatic, attribute: keyof Attributes) {} * ``` */ -export type Attributes = M['_attributes']; +export type Attributes = M['_attributes']; -export type AttributeNames = Extract; +export type AttributeNames = Extract; diff --git a/src/model.js b/src/model.js index 47ddddce55ef..de867c1b3d90 100644 --- a/src/model.js +++ b/src/model.js @@ -1,5 +1,6 @@ 'use strict'; +import { ModelTypeScript } from './model-typescript'; import { isModelStatic, isSameInitialModel } from './utils/model-utils'; const assert = require('assert'); @@ -47,9 +48,8 @@ const nonCascadingOptions = ['include', 'attributes', 'originalAttributes', 'ord * used for custom getters. * * @see {Sequelize#define} for more information about getters and setters - * @mixes Hooks */ -export class Model { +export class Model extends ModelTypeScript { static get queryInterface() { return this.sequelize.getQueryInterface(); } @@ -80,6 +80,8 @@ export class Model { * `set` */ constructor(values = {}, options = {}) { + super(); + if (!this.constructor._overwrittenAttributesChecked) { this.constructor._overwrittenAttributesChecked = true; @@ -879,7 +881,7 @@ Specify a different name for either index to resolve this issue.`); schema: globalOptions.schema, }, options); - this.sequelize.runHooks('beforeDefine', attributes, options); + this.sequelize.hooks.runSync('beforeDefine', attributes, options); if (options.modelName !== this.name) { Object.defineProperty(this, 'name', { value: options.modelName }); @@ -910,7 +912,9 @@ Specify a different name for either index to resolve this issue.`); } this.associations = Object.create(null); - this._setupHooks(options.hooks); + if (options.hooks) { + this.hooks.addListeners(options.hooks); + } // TODO: use private field this.underscored = this.options.underscored; @@ -1031,7 +1035,7 @@ Specify a different name for either index to resolve this issue.`); this._scopeNames = ['defaultScope']; this.sequelize.modelManager.addModel(this); - this.sequelize.runHooks('afterDefine', this); + this.sequelize.hooks.runSync('afterDefine', this); return this; } @@ -1287,7 +1291,7 @@ Specify a different name for either index to resolve this issue.`); const rawAttributes = this.fieldRawAttributesMap; if (options.hooks) { - await this.runHooks('beforeSync', options); + await this.hooks.runAsync('beforeSync', options); } const tableName = this.getTableName(options); @@ -1405,7 +1409,7 @@ Specify a different name for either index to resolve this issue.`); } if (options.hooks) { - await this.runHooks('afterSync', options); + await this.hooks.runAsync('afterSync', options); } return this; @@ -1743,7 +1747,7 @@ Specify a different name for either index to resolve this issue.`); this._injectScope(options); if (options.hooks) { - await this.runHooks('beforeFind', options); + await this.hooks.runAsync('beforeFind', options); this._conformIncludes(options, this); } @@ -1751,7 +1755,7 @@ Specify a different name for either index to resolve this issue.`); this._expandIncludeAll(options, options.model); if (options.hooks) { - await this.runHooks('beforeFindAfterExpandIncludeAll', options); + await this.hooks.runAsync('beforeFindAfterExpandIncludeAll', options); } options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes); @@ -1786,13 +1790,13 @@ Specify a different name for either index to resolve this issue.`); options = this._paranoidClause(this, options); if (options.hooks) { - await this.runHooks('beforeFindAfterOptions', options); + await this.hooks.runAsync('beforeFindAfterOptions', options); } const selectOptions = { ...options, tableNames: Object.keys(tableNames) }; const results = await this.queryInterface.select(this, this.getTableName(selectOptions), selectOptions); if (options.hooks) { - await this.runHooks('afterFind', results, options); + await this.hooks.runAsync('afterFind', results, options); } // rejectOnEmpty mode @@ -2053,7 +2057,7 @@ Specify a different name for either index to resolve this issue.`); options.raw = true; if (options.hooks) { - await this.runHooks('beforeCount', options); + await this.hooks.runAsync('beforeCount', options); } let col = options.col || '*'; @@ -2538,7 +2542,7 @@ Specify a different name for either index to resolve this issue.`); } if (options.hooks) { - await this.runHooks('beforeUpsert', values, options); + await this.hooks.runAsync('beforeUpsert', values, options); } const result = await this.queryInterface.upsert(this.getTableName(options), insertValues, updateValues, instance.where(), options); @@ -2547,7 +2551,7 @@ Specify a different name for either index to resolve this issue.`); record.isNewRecord = false; if (options.hooks) { - await this.runHooks('afterUpsert', result, options); + await this.hooks.runAsync('afterUpsert', result, options); return result; } @@ -2640,7 +2644,7 @@ Specify a different name for either index to resolve this issue.`); // Run before hook if (options.hooks) { - await model.runHooks('beforeBulkCreate', instances, options); + await model.hooks.runAsync('beforeBulkCreate', instances, options); } // Validate @@ -2897,7 +2901,7 @@ Specify a different name for either index to resolve this issue.`); // Run after hook if (options.hooks) { - await model.runHooks('afterBulkCreate', instances, options); + await model.hooks.runAsync('afterBulkCreate', instances, options); } return instances; @@ -2959,7 +2963,7 @@ Specify a different name for either index to resolve this issue.`); // Run before hook if (options.hooks) { - await this.runHooks('beforeBulkDestroy', options); + await this.hooks.runAsync('beforeBulkDestroy', options); } let instances; @@ -2967,7 +2971,9 @@ Specify a different name for either index to resolve this issue.`); if (options.individualHooks) { instances = await this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark }); - await Promise.all(instances.map(instance => this.runHooks('beforeDestroy', instance, options))); + await Promise.all(instances.map(instance => { + return this.hooks.runAsync('beforeDestroy', instance, options); + })); } let result; @@ -2992,13 +2998,15 @@ Specify a different name for either index to resolve this issue.`); // Run afterDestroy hook on each record individually if (options.individualHooks) { await Promise.all( - instances.map(instance => this.runHooks('afterDestroy', instance, options)), + instances.map(instance => { + return this.hooks.runAsync('afterDestroy', instance, options); + }), ); } // Run after hook if (options.hooks) { - await this.runHooks('afterBulkDestroy', options); + await this.hooks.runAsync('afterBulkDestroy', options); } return result; @@ -3032,7 +3040,7 @@ Specify a different name for either index to resolve this issue.`); // Run before hook if (options.hooks) { - await this.runHooks('beforeBulkRestore', options); + await this.hooks.runAsync('beforeBulkRestore', options); } let instances; @@ -3040,7 +3048,9 @@ Specify a different name for either index to resolve this issue.`); if (options.individualHooks) { instances = await this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark, paranoid: false }); - await Promise.all(instances.map(instance => this.runHooks('beforeRestore', instance, options))); + await Promise.all(instances.map(instance => { + return this.hooks.runAsync('beforeRestore', instance, options); + })); } // Run undelete query @@ -3055,13 +3065,15 @@ Specify a different name for either index to resolve this issue.`); // Run afterDestroy hook on each record individually if (options.individualHooks) { await Promise.all( - instances.map(instance => this.runHooks('afterRestore', instance, options)), + instances.map(instance => { + return this.hooks.runAsync('afterRestore', instance, options); + }), ); } // Run after hook if (options.hooks) { - await this.runHooks('afterBulkRestore', options); + await this.hooks.runAsync('afterBulkRestore', options); } return result; @@ -3146,7 +3158,7 @@ Specify a different name for either index to resolve this issue.`); // Run before hook if (options.hooks) { options.attributes = values; - await this.runHooks('beforeBulkUpdate', options); + await this.hooks.runAsync('beforeBulkUpdate', options); values = options.attributes; delete options.attributes; } @@ -3182,7 +3194,8 @@ Specify a different name for either index to resolve this issue.`); }); // Run beforeUpdate hook - await this.runHooks('beforeUpdate', instance, options); + await this.hooks.runAsync('beforeUpdate', instance, options); + await this.hooks.runAsync('beforeSave', instance, options); if (!different) { const thisChangedValues = {}; _.forIn(instance.dataValues, (newValue, attr) => { @@ -3247,14 +3260,17 @@ Specify a different name for either index to resolve this issue.`); } if (options.individualHooks) { - await Promise.all(instances.map(instance => this.runHooks('afterUpdate', instance, options))); + await Promise.all(instances.map(async instance => { + await this.hooks.runAsync('afterUpdate', instance, options); + await this.hooks.runAsync('afterSave', instance, options); + })); result[1] = instances; } // Run after hook if (options.hooks) { options.attributes = values; - await this.runHooks('afterBulkUpdate', options); + await this.hooks.runAsync('afterBulkUpdate', options); delete options.attributes; } @@ -3375,7 +3391,8 @@ Instead of specifying a Model, either: * @param {object} options increment options * @param {object} options.where conditions hash * - * @returns {Promise} an array of affected rows and affected count with `options.returning` true, whenever supported by dialect + * @returns {Promise} an array of affected rows and affected count with `options.returning` true, + * whenever supported by dialect */ static async increment(fields, options) { options = options || {}; @@ -3929,7 +3946,8 @@ Instead of specifying a Model, either: * Validates this instance, and if the validation passes, persists it to the database. * * Returns a Promise that resolves to the saved instance (or rejects with a {@link ValidationError}, - * which will have a property for each of the fields for which the validation failed, with the error message for that field). + * which will have a property for each of the fields for which the validation failed, with the error message for that + * field). * * This method is optimized to perform an UPDATE only into the fields that changed. * If nothing has changed, no SQL query will be performed. @@ -4042,7 +4060,8 @@ Instead of specifying a Model, either: ignoreChanged = _.without(ignoreChanged, updatedAtAttr); } - await this.constructor.runHooks(`before${hook}`, this, options); + await this.constructor.hooks.runAsync(`before${hook}`, this, options); + await this.constructor.hooks.runAsync(`beforeSave`, this, options); if (options.defaultFields && !this.isNewRecord) { afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged)); @@ -4203,7 +4222,8 @@ Instead of specifying a Model, either: // Run after hook if (options.hooks) { - await this.constructor.runHooks(`after${hook}`, result, options); + await this.constructor.hooks.runAsync(`after${hook}`, result, options); + await this.constructor.hooks.runAsync(`afterSave`, result, options); } for (const field of options.fields) { @@ -4320,7 +4340,7 @@ Instead of specifying a Model, either: // Run before hook if (options.hooks) { - await this.constructor.runHooks('beforeDestroy', this, options); + await this.constructor.hooks.runAsync('beforeDestroy', this, options); } const where = this.where(true); @@ -4346,7 +4366,7 @@ Instead of specifying a Model, either: // Run after hook if (options.hooks) { - await this.constructor.runHooks('afterDestroy', this, options); + await this.constructor.hooks.runAsync('afterDestroy', this, options); } return result; @@ -4398,7 +4418,7 @@ Instead of specifying a Model, either: // Run before hook if (options.hooks) { - await this.constructor.runHooks('beforeRestore', this, options); + await this.constructor.hooks.runAsync('beforeRestore', this, options); } const deletedAtCol = this.constructor._timestampAttributes.deletedAt; @@ -4409,7 +4429,7 @@ Instead of specifying a Model, either: const result = await this.save({ ...options, hooks: false, omitNull: false }); // Run after hook if (options.hooks) { - await this.constructor.runHooks('afterRestore', this, options); + await this.constructor.hooks.runAsync('afterRestore', this, options); return result; } @@ -4674,5 +4694,3 @@ function combineWheresWithAnd(whereA, whereB) { [Op.and]: [unpackedA, unpackedB].flat(), }; } - -Hooks.applyTo(Model, true); diff --git a/src/sequelize-typescript.ts b/src/sequelize-typescript.ts new file mode 100644 index 000000000000..8080cc3c0738 --- /dev/null +++ b/src/sequelize-typescript.ts @@ -0,0 +1,173 @@ +import type { AbstractQuery } from './dialects/abstract/query.js'; +import { + legacyBuildHasHook, + legacyBuildAddAnyHook, + legacyBuildRunHook, + legacyBuildRemoveHook, + legacyBuildAddHook, +} 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 { ConnectionOptions, Options } from './sequelize.js'; +import type { ModelAttributes, ModelOptions, ModelStatic, QueryOptions, Sequelize, SyncOptions } from '.'; + +export interface SequelizeHooks extends ModelHooks { + /** + * A hook that is run at the start of {@link Sequelize#define} and {@link Model.init} + */ + beforeDefine(attributes: ModelAttributes, options: ModelOptions): void; + + /** + * A hook that is run at the end of {@link Sequelize#define} and {@link Model.init} + */ + afterDefine(model: ModelStatic): void; + + /** + * A hook that is run before a connection is created + */ + beforeConnect(config: ConnectionOptions): AsyncHookReturn; + // TODO: set type of Connection once DataType-TS PR is merged + + /** + * A hook that is run after a connection is created + */ + afterConnect(connection: unknown, config: ConnectionOptions): AsyncHookReturn; + + /** + * A hook that is run before a connection is disconnected + */ + beforeDisconnect(connection: unknown): AsyncHookReturn; + + /** + * A hook that is run after a connection is disconnected + */ + afterDisconnect(connection: unknown): AsyncHookReturn; + beforeQuery(options: QueryOptions, query: AbstractQuery): AsyncHookReturn; + afterQuery(options: QueryOptions, query: AbstractQuery): AsyncHookReturn; + + /** + * A hook that is run at the start of {@link Sequelize#sync} + */ + beforeBulkSync(options: SyncOptions): AsyncHookReturn; + + /** + * A hook that is run at the end of {@link Sequelize#sync} + */ + afterBulkSync(options: SyncOptions): AsyncHookReturn; +} + +export interface StaticSequelizeHooks { + /** + * A hook that is run at the beginning of the creation of a Sequelize instance. + */ + beforeInit(options: Options): void; + + /** + * A hook that is run at the end of the creation of a Sequelize instance. + */ + afterInit(sequelize: Sequelize): void; +} + +const staticSequelizeHooks = new HookHandlerBuilder([ + 'beforeInit', 'afterInit', +]); + +const instanceSequelizeHooks = new HookHandlerBuilder([ + 'beforeQuery', 'afterQuery', + 'beforeBulkSync', 'afterBulkSync', + 'beforeConnect', 'afterConnect', + 'beforeDisconnect', 'afterDisconnect', + 'beforeDefine', 'afterDefine', + ...validModelHooks, +]); + +// 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 SequelizeTypeScript { + static get hooks(): HookHandler { + return staticSequelizeHooks.getFor(this); + } + + static addHook = legacyBuildAddAnyHook(staticSequelizeHooks); + static removeHook = legacyBuildRemoveHook(staticSequelizeHooks); + static hasHook = legacyBuildHasHook(staticSequelizeHooks); + static hasHooks = legacyBuildHasHook(staticSequelizeHooks); + static runHooks = legacyBuildRunHook(staticSequelizeHooks); + + static beforeInit = legacyBuildAddHook(staticSequelizeHooks, 'beforeInit'); + static afterInit = legacyBuildAddHook(staticSequelizeHooks, 'afterInit'); + + get hooks(): HookHandler { + return instanceSequelizeHooks.getFor(this); + } + + addHook = legacyBuildAddAnyHook(instanceSequelizeHooks); + removeHook = legacyBuildRemoveHook(instanceSequelizeHooks); + hasHook = legacyBuildHasHook(instanceSequelizeHooks); + hasHooks = legacyBuildHasHook(instanceSequelizeHooks); + runHooks = legacyBuildRunHook(instanceSequelizeHooks); + + beforeQuery = legacyBuildAddHook(instanceSequelizeHooks, 'beforeQuery'); + afterQuery = legacyBuildAddHook(instanceSequelizeHooks, 'afterQuery'); + + beforeBulkSync = legacyBuildAddHook(instanceSequelizeHooks, 'beforeBulkSync'); + afterBulkSync = legacyBuildAddHook(instanceSequelizeHooks, 'afterBulkSync'); + + beforeConnect = legacyBuildAddHook(instanceSequelizeHooks, 'beforeConnect'); + afterConnect = legacyBuildAddHook(instanceSequelizeHooks, 'afterConnect'); + + beforeDisconnect = legacyBuildAddHook(instanceSequelizeHooks, 'beforeDisconnect'); + afterDisconnect = legacyBuildAddHook(instanceSequelizeHooks, 'afterDisconnect'); + + beforeDefine = legacyBuildAddHook(instanceSequelizeHooks, 'beforeDefine'); + afterDefine = legacyBuildAddHook(instanceSequelizeHooks, 'afterDefine'); + + beforeValidate = legacyBuildAddHook(instanceSequelizeHooks, 'beforeValidate'); + afterValidate = legacyBuildAddHook(instanceSequelizeHooks, 'afterValidate'); + validationFailed = legacyBuildAddHook(instanceSequelizeHooks, 'validationFailed'); + + beforeCreate = legacyBuildAddHook(instanceSequelizeHooks, 'beforeCreate'); + afterCreate = legacyBuildAddHook(instanceSequelizeHooks, 'afterCreate'); + + beforeDestroy = legacyBuildAddHook(instanceSequelizeHooks, 'beforeDestroy'); + afterDestroy = legacyBuildAddHook(instanceSequelizeHooks, 'afterDestroy'); + + beforeRestore = legacyBuildAddHook(instanceSequelizeHooks, 'beforeRestore'); + afterRestore = legacyBuildAddHook(instanceSequelizeHooks, 'afterRestore'); + + beforeUpdate = legacyBuildAddHook(instanceSequelizeHooks, 'beforeUpdate'); + afterUpdate = legacyBuildAddHook(instanceSequelizeHooks, 'afterUpdate'); + + beforeUpsert = legacyBuildAddHook(instanceSequelizeHooks, 'beforeUpsert'); + afterUpsert = legacyBuildAddHook(instanceSequelizeHooks, 'afterUpsert'); + + beforeSave = legacyBuildAddHook(instanceSequelizeHooks, 'beforeSave'); + afterSave = legacyBuildAddHook(instanceSequelizeHooks, 'afterSave'); + + beforeBulkCreate = legacyBuildAddHook(instanceSequelizeHooks, 'beforeBulkCreate'); + afterBulkCreate = legacyBuildAddHook(instanceSequelizeHooks, 'afterBulkCreate'); + + beforeBulkDestroy = legacyBuildAddHook(instanceSequelizeHooks, 'beforeBulkDestroy'); + afterBulkDestroy = legacyBuildAddHook(instanceSequelizeHooks, 'afterBulkDestroy'); + + beforeBulkRestore = legacyBuildAddHook(instanceSequelizeHooks, 'beforeBulkRestore'); + afterBulkRestore = legacyBuildAddHook(instanceSequelizeHooks, 'afterBulkRestore'); + + beforeBulkUpdate = legacyBuildAddHook(instanceSequelizeHooks, 'beforeBulkUpdate'); + afterBulkUpdate = legacyBuildAddHook(instanceSequelizeHooks, 'afterBulkUpdate'); + + beforeCount = legacyBuildAddHook(instanceSequelizeHooks, 'beforeCount'); + + beforeFind = legacyBuildAddHook(instanceSequelizeHooks, 'beforeFind'); + beforeFindAfterExpandIncludeAll = legacyBuildAddHook(instanceSequelizeHooks, 'beforeFindAfterExpandIncludeAll'); + beforeFindAfterOptions = legacyBuildAddHook(instanceSequelizeHooks, 'beforeFindAfterOptions'); + afterFind = legacyBuildAddHook(instanceSequelizeHooks, 'afterFind'); + + beforeSync = legacyBuildAddHook(instanceSequelizeHooks, 'beforeSync'); + afterSync = legacyBuildAddHook(instanceSequelizeHooks, 'afterSync'); + + beforeAssociate = legacyBuildAddHook(instanceSequelizeHooks, 'beforeAssociate'); + afterAssociate = legacyBuildAddHook(instanceSequelizeHooks, 'afterAssociate'); +} diff --git a/src/sequelize.d.ts b/src/sequelize.d.ts index 5eb8b2e05b83..f44323279021 100644 --- a/src/sequelize.d.ts +++ b/src/sequelize.d.ts @@ -1,26 +1,17 @@ import type { AbstractDialect } from './dialects/abstract'; import type { AbstractConnectionManager } from './dialects/abstract/connection-manager'; import type { QueryInterface, ColumnsDescription } from './dialects/abstract/query-interface'; -import type { HookReturn, SequelizeHooks } from './hooks'; -import { Hooks } from './hooks'; -import type { ValidationOptions } from './instance-validator'; import type { - BulkCreateOptions, - CreateOptions, DestroyOptions, DropOptions, - FindOptions, - InstanceDestroyOptions, Logging, Model, ModelAttributeColumnOptions, ModelAttributes, ModelOptions, - UpdateOptions, WhereOperators, Hookable, ModelStatic, - CreationAttributes, Attributes, ColumnReference, Transactionable, @@ -28,7 +19,9 @@ import type { WhereAttributeHashValue, } from './model'; import type { ModelManager } from './model-manager'; -import type { Cast, Col, DeepWriteable, Fn, Json, Literal, Where } from './utils'; +import { SequelizeTypeScript } from './sequelize-typescript.js'; +import type { SequelizeHooks } from './sequelize-typescript.js'; +import type { Cast, Col, Fn, Json, Literal, Where } from './utils'; import type { QueryTypes, Transaction, TransactionOptions, TRANSACTION_TYPES, ISOLATION_LEVELS, PartlyRequired, Op, DataTypes } from '.'; /** @@ -397,7 +390,7 @@ export interface Options extends Logging { /** * Sets global permanent hooks. */ - hooks?: Partial>; + hooks?: Partial; /** * Set to `true` to automatically minify aliases generated by sequelize. @@ -552,7 +545,7 @@ export interface QueryOptionsWithModel extends QueryOptions { * should also be installed in your project. You don't need to import it however, as * sequelize will take care of that. */ -export class Sequelize extends Hooks { +export class Sequelize extends SequelizeTypeScript { // -------------------- Utilities ------------------------------------------------------------------------ @@ -652,321 +645,6 @@ export class Sequelize extends Hooks { static Op: typeof Op; static DataTypes: typeof DataTypes; - /** - * A hook that is run before validation - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeValidate(name: string, fn: (instance: Model, options: ValidationOptions) => void): void; - static beforeValidate(fn: (instance: Model, options: ValidationOptions) => void): void; - - /** - * A hook that is run after validation - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterValidate(name: string, fn: (instance: Model, options: ValidationOptions) => void): void; - static afterValidate(fn: (instance: Model, options: ValidationOptions) => void): void; - - /** - * A hook that is run before creating a single instance - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - static beforeCreate(name: string, fn: (attributes: Model, options: CreateOptions) => void): void; - static beforeCreate(fn: (attributes: Model, options: CreateOptions) => void): void; - - /** - * A hook that is run after creating a single instance - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - static afterCreate(name: string, fn: (attributes: Model, options: CreateOptions) => void): void; - static afterCreate(fn: (attributes: Model, options: CreateOptions) => void): void; - - /** - * A hook that is run before destroying a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeDestroy(name: string, fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - static beforeDestroy(fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - - /** - * A hook that is run after destroying a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterDestroy(name: string, fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - static afterDestroy(fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - - /** - * A hook that is run before updating a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeUpdate(name: string, fn: (instance: Model, options: UpdateOptions) => void): void; - static beforeUpdate(fn: (instance: Model, options: UpdateOptions) => void): void; - - /** - * A hook that is run after updating a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterUpdate(name: string, fn: (instance: Model, options: UpdateOptions) => void): void; - static afterUpdate(fn: (instance: Model, options: UpdateOptions) => void): void; - - /** - * A hook that is run before creating or updating a single instance, It proxies `beforeCreate` and `beforeUpdate` - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static beforeSave( - name: string, - fn: (instance: Model, options: UpdateOptions | CreateOptions) => void - ): void; - static beforeSave(fn: (instance: Model, options: UpdateOptions | CreateOptions) => void): void; - - /** - * A hook that is run after creating or updating a single instance, It proxies `afterCreate` and `afterUpdate` - * - * @param name - * @param fn A callback function that is called with instance, options - */ - static afterSave( - name: string, - fn: (instance: Model, options: UpdateOptions | CreateOptions) => void - ): void; - static afterSave( - fn: (instance: Model, options: UpdateOptions | CreateOptions) => void - ): void; - - /** - * A hook that is run before creating instances in bulk - * - * @param name - * @param fn A callback function that is called with instances, options - */ - static beforeBulkCreate( - name: string, - fn: (instances: Model[], options: BulkCreateOptions) => void - ): void; - static beforeBulkCreate(fn: (instances: Model[], options: BulkCreateOptions) => void): void; - - /** - * A hook that is run after creating instances in bulk - * - * @param name - * @param fn A callback function that is called with instances, options - */ - static afterBulkCreate( - name: string, fn: (instances: Model[], options: BulkCreateOptions) => void - ): void; - static afterBulkCreate(fn: (instances: Model[], options: BulkCreateOptions) => void): void; - - /** - * A hook that is run before destroying instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeBulkDestroy(name: string, fn: (options: BulkCreateOptions) => void): void; - static beforeBulkDestroy(fn: (options: BulkCreateOptions) => void): void; - - /** - * A hook that is run after destroying instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static afterBulkDestroy(name: string, fn: (options: DestroyOptions) => void): void; - static afterBulkDestroy(fn: (options: DestroyOptions) => void): void; - - /** - * A hook that is run after updating instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeBulkUpdate(name: string, fn: (options: UpdateOptions) => void): void; - static beforeBulkUpdate(fn: (options: UpdateOptions) => void): void; - - /** - * A hook that is run after updating instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - static afterBulkUpdate(name: string, fn: (options: UpdateOptions) => void): void; - static afterBulkUpdate(fn: (options: UpdateOptions) => void): void; - - /** - * A hook that is run before a find (select) query - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeFind(name: string, fn: (options: FindOptions) => void): void; - static beforeFind(fn: (options: FindOptions) => void): void; - - /** - * A hook that is run before a connection is established - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeConnect(name: string, fn: (options: DeepWriteable) => void): void; - static beforeConnect(fn: (options: DeepWriteable) => void): void; - beforeConnect(name: string, fn: (options: DeepWriteable) => void): void; - beforeConnect(fn: (options: DeepWriteable) => void): void; - - /** - * A hook that is run after a connection is established - * - * @param name - * @param fn A callback function that is called with options - */ - static afterConnect(name: string, fn: (connection: unknown, options: Config) => void): void; - static afterConnect(fn: (connection: unknown, options: Config) => void): void; - afterConnect(name: string, fn: (connection: unknown, options: Config) => void): void; - afterConnect(fn: (connection: unknown, options: Config) => void): void; - - /** - * A hook that is run before a connection is released - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeDisconnect(name: string, fn: (connection: unknown) => void): void; - static beforeDisconnect(fn: (connection: unknown) => void): void; - beforeDisconnect(name: string, fn: (connection: unknown) => void): void; - beforeDisconnect(fn: (connection: unknown) => void): void; - - /** - * A hook that is run after a connection is released - * - * @param name - * @param fn A callback function that is called with options - */ - static afterDisconnect(name: string, fn: (connection: unknown) => void): void; - static afterDisconnect(fn: (connection: unknown) => void): void; - afterDisconnect(name: string, fn: (connection: unknown) => void): void; - afterDisconnect(fn: (connection: unknown) => void): void; - - /** - * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeFindAfterExpandIncludeAll(name: string, fn: (options: FindOptions) => void): void; - static beforeFindAfterExpandIncludeAll(fn: (options: FindOptions) => void): void; - - /** - * A hook that is run before a find (select) query, after all option parsing is complete - * - * @param name - * @param fn A callback function that is called with options - */ - static beforeFindAfterOptions(name: string, fn: (options: FindOptions) => void): void; - static beforeFindAfterOptions(fn: (options: FindOptions) => void): void; - - /** - * A hook that is run after a find (select) query - * - * @param name - * @param fn A callback function that is called with instance(s), options - */ - static afterFind( - name: string, - fn: (instancesOrInstance: Model[] | Model | null, options: FindOptions) => void - ): void; - static afterFind( - fn: (instancesOrInstance: Model[] | Model | null, options: FindOptions) => void - ): void; - - /** - * A hook that is run before a define call - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - static beforeDefine( - name: string, - fn: (attributes: ModelAttributes>, options: ModelOptions) => void - ): void; - static beforeDefine( - fn: (attributes: ModelAttributes>, options: ModelOptions) => void - ): void; - - /** - * A hook that is run after a define call - * - * @param name - * @param fn A callback function that is called with factory - */ - static afterDefine(name: string, fn: (model: ModelStatic) => void): void; - static afterDefine(fn: (model: ModelStatic) => void): void; - - /** - * A hook that is run before Sequelize() call - * - * @param name - * @param fn A callback function that is called with config, options - */ - static beforeInit(name: string, fn: (config: Config, options: Options) => void): void; - static beforeInit(fn: (config: Config, options: Options) => void): void; - - /** - * A hook that is run after Sequelize() call - * - * @param name - * @param fn A callback function that is called with sequelize - */ - static afterInit(name: string, fn: (sequelize: Sequelize) => void): void; - static afterInit(fn: (sequelize: Sequelize) => void): void; - - /** - * A hook that is run before sequelize.sync call - * - * @param fn A callback function that is called with options passed to sequelize.sync - */ - static beforeBulkSync(dname: string, fn: (options: SyncOptions) => HookReturn): void; - static beforeBulkSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run after sequelize.sync call - * - * @param fn A callback function that is called with options passed to sequelize.sync - */ - static afterBulkSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - static afterBulkSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run before Model.sync call - * - * @param fn A callback function that is called with options passed to Model.sync - */ - static beforeSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - static beforeSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run after Model.sync call - * - * @param fn A callback function that is called with options passed to Model.sync - */ - static afterSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - static afterSync(fn: (options: SyncOptions) => HookReturn): void; - /** * Use CLS with Sequelize. * CLS namespace provided is stored as `Sequelize._cls` @@ -1049,239 +727,6 @@ export class Sequelize extends Hooks { */ constructor(uri: string, options?: Options); - /** - * A hook that is run before validation - * - * @param name - * @param fn A callback function that is called with instance, options - */ - beforeValidate(name: string, fn: (instance: Model, options: ValidationOptions) => void): void; - beforeValidate(fn: (instance: Model, options: ValidationOptions) => void): void; - - /** - * A hook that is run after validation - * - * @param name - * @param fn A callback function that is called with instance, options - */ - afterValidate(name: string, fn: (instance: Model, options: ValidationOptions) => void): void; - afterValidate(fn: (instance: Model, options: ValidationOptions) => void): void; - - /** - * A hook that is run before creating a single instance - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - beforeCreate(name: string, fn: (attributes: Model, options: CreateOptions) => void): void; - beforeCreate(fn: (attributes: Model, options: CreateOptions) => void): void; - - /** - * A hook that is run after creating a single instance - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - afterCreate(name: string, fn: (attributes: Model, options: CreateOptions) => void): void; - afterCreate(fn: (attributes: Model, options: CreateOptions) => void): void; - - /** - * A hook that is run before destroying a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - beforeDestroy(name: string, fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - beforeDestroy(fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - - /** - * A hook that is run after destroying a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - afterDestroy(name: string, fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - afterDestroy(fn: (instance: Model, options: InstanceDestroyOptions) => void): void; - - /** - * A hook that is run before updating a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - beforeUpdate(name: string, fn: (instance: Model, options: UpdateOptions) => void): void; - beforeUpdate(fn: (instance: Model, options: UpdateOptions) => void): void; - - /** - * A hook that is run after updating a single instance - * - * @param name - * @param fn A callback function that is called with instance, options - */ - afterUpdate(name: string, fn: (instance: Model, options: UpdateOptions) => void): void; - afterUpdate(fn: (instance: Model, options: UpdateOptions) => void): void; - - /** - * A hook that is run before creating instances in bulk - * - * @param name - * @param fn A callback function that is called with instances, options - */ - beforeBulkCreate(name: string, fn: (instances: Model[], options: BulkCreateOptions) => void): void; - beforeBulkCreate(fn: (instances: Model[], options: BulkCreateOptions) => void): void; - - /** - * A hook that is run after creating instances in bulk - * - * @param name - * @param fn A callback function that is called with instances, options - */ - afterBulkCreate(name: string, fn: (instances: Model[], options: BulkCreateOptions) => void): void; - afterBulkCreate(fn: (instances: Model[], options: BulkCreateOptions) => void): void; - - /** - * A hook that is run before destroying instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - beforeBulkDestroy(name: string, fn: (options: BulkCreateOptions) => void): void; - beforeBulkDestroy(fn: (options: BulkCreateOptions) => void): void; - - /** - * A hook that is run after destroying instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - afterBulkDestroy(name: string, fn: (options: DestroyOptions) => void): void; - afterBulkDestroy(fn: (options: DestroyOptions) => void): void; - - /** - * A hook that is run after updating instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - beforeBulkUpdate(name: string, fn: (options: UpdateOptions) => void): void; - beforeBulkUpdate(fn: (options: UpdateOptions) => void): void; - - /** - * A hook that is run after updating instances in bulk - * - * @param name - * @param fn A callback function that is called with options - */ - afterBulkUpdate(name: string, fn: (options: UpdateOptions) => void): void; - afterBulkUpdate(fn: (options: UpdateOptions) => void): void; - - /** - * A hook that is run before a find (select) query - * - * @param name - * @param fn A callback function that is called with options - */ - beforeFind(name: string, fn: (options: FindOptions) => void): void; - beforeFind(fn: (options: FindOptions) => void): void; - - /** - * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded - * - * @param name - * @param fn A callback function that is called with options - */ - beforeFindAfterExpandIncludeAll(name: string, fn: (options: FindOptions) => void): void; - beforeFindAfterExpandIncludeAll(fn: (options: FindOptions) => void): void; - - /** - * A hook that is run before a find (select) query, after all option parsing is complete - * - * @param name - * @param fn A callback function that is called with options - */ - beforeFindAfterOptions(name: string, fn: (options: FindOptions) => void): void; - beforeFindAfterOptions(fn: (options: FindOptions) => void): void; - - /** - * A hook that is run after a find (select) query - * - * @param name - * @param fn A callback function that is called with instance(s), options - */ - afterFind( - name: string, - fn: (instancesOrInstance: Model[] | Model | null, options: FindOptions) => void - ): void; - afterFind(fn: (instancesOrInstance: Model[] | Model | null, options: FindOptions) => void): void; - - /** - * A hook that is run before a define call - * - * @param name - * @param fn A callback function that is called with attributes, options - */ - beforeDefine(name: string, fn: (attributes: ModelAttributes, options: ModelOptions) => void): void; - beforeDefine(fn: (attributes: ModelAttributes, options: ModelOptions) => void): void; - - /** - * A hook that is run after a define call - * - * @param name - * @param fn A callback function that is called with factory - */ - afterDefine(name: string, fn: (model: ModelStatic) => void): void; - afterDefine(fn: (model: ModelStatic) => void): void; - - /** - * A hook that is run before Sequelize() call - * - * @param name - * @param fn A callback function that is called with config, options - */ - beforeInit(name: string, fn: (config: Config, options: Options) => void): void; - beforeInit(fn: (config: Config, options: Options) => void): void; - - /** - * A hook that is run after Sequelize() call - * - * @param name - * @param fn A callback function that is called with sequelize - */ - afterInit(name: string, fn: (sequelize: Sequelize) => void): void; - afterInit(fn: (sequelize: Sequelize) => void): void; - - /** - * A hook that is run before sequelize.sync call - * - * @param fn A callback function that is called with options passed to sequelize.sync - */ - beforeBulkSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - beforeBulkSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run after sequelize.sync call - * - * @param fn A callback function that is called with options passed to sequelize.sync - */ - afterBulkSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - afterBulkSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run before Model.sync call - * - * @param fn A callback function that is called with options passed to Model.sync - */ - beforeSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - beforeSync(fn: (options: SyncOptions) => HookReturn): void; - - /** - * A hook that is run after Model.sync call - * - * @param fn A callback function that is called with options passed to Model.sync - */ - afterSync(name: string, fn: (options: SyncOptions) => HookReturn): void; - afterSync(fn: (options: SyncOptions) => HookReturn): void; - /** * Returns the specified dialect. */ diff --git a/src/sequelize.js b/src/sequelize.js index 5d340f5f4e28..95616aa4415d 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -1,11 +1,11 @@ 'use strict'; import isPlainObject from 'lodash/isPlainObject'; +import { SequelizeTypeScript } from './sequelize-typescript'; import { withSqliteForeignKeysOff } from './dialects/sqlite/sqlite-utils'; -import { AggregateError } from './errors'; import { isString } from './utils'; import { noSequelizeDataType } from './utils/deprecations'; -import { isSameInitialModel, isModelStatic } from './utils/model-utils'; +import { isModelStatic, isSameInitialModel } from './utils/model-utils'; import { injectReplacements, mapBindParameters } from './utils/sql'; import { parseConnectionString } from './utils/url'; @@ -22,7 +22,6 @@ const { QueryTypes } = require('./query-types'); const { TableHints } = require('./table-hints'); const { IndexHints } = require('./index-hints'); const sequelizeErrors = require('./errors'); -const Hooks = require('./hooks'); const { Association } = require('./associations/index'); const Validator = require('./utils/validator-extras').validator; const { Op } = require('./operators'); @@ -37,7 +36,7 @@ require('./utils/dayjs'); /** * This is the main class, the entry point to sequelize. */ -export class Sequelize { +export class Sequelize extends SequelizeTypeScript { /** * Instantiate sequelize with name of database, username and password. * @@ -195,6 +194,8 @@ export class Sequelize { * @param {boolean} [options.logQueryParameters=false] A flag that defines if show bind parameters in log. */ constructor(database, username, password, options) { + super(); + if (arguments.length === 1 && _.isPlainObject(database)) { // new Sequelize({ ... options }) options = database; @@ -214,7 +215,7 @@ export class Sequelize { }); } - Sequelize.runHooks('beforeInit', options, options); + Sequelize.hooks.runSync('beforeInit', options); // @ts-expect-error if (options.pool === false) { @@ -285,7 +286,9 @@ export class Sequelize { this.options.logging = console.debug; } - this._setupHooks(options.hooks); + if (options.hooks) { + this.hooks.addListeners(options.hooks); + } // ========================================== // REPLICATION CONFIG NORMALIZATION @@ -403,7 +406,7 @@ export class Sequelize { this.modelManager = new ModelManager(this); this.connectionManager = this.dialect.connectionManager; - Sequelize.runHooks('afterInit', this); + Sequelize.hooks.runSync('afterInit', this); } /** @@ -699,12 +702,12 @@ Use Sequelize#query if you wish to use replacements.`); const query = new this.dialect.Query(connection, this, options); try { - await this.runHooks('beforeQuery', options, query); + await this.hooks.runAsync('beforeQuery', options, query); checkTransaction(); return await query.run(sql, bindParameters); } finally { - await this.runHooks('afterQuery', options, query); + await this.hooks.runAsync('afterQuery', options, query); if (!options.transaction) { this.connectionManager.releaseConnection(connection); } @@ -854,7 +857,7 @@ Use Sequelize#query if you wish to use replacements.`); } if (options.hooks) { - await this.runHooks('beforeBulkSync', options); + await this.hooks.runAsync('beforeBulkSync', options); } if (options.force) { @@ -881,7 +884,7 @@ Use Sequelize#query if you wish to use replacements.`); } if (options.hooks) { - await this.runHooks('afterBulkSync', options); + await this.hooks.runAsync('afterBulkSync', options); } return this; @@ -1317,8 +1320,6 @@ Object.defineProperty(Sequelize, 'version', { }, }); -Sequelize.options = { hooks: {} }; - /** * @private */ @@ -1415,13 +1416,6 @@ Sequelize.prototype.Association = Sequelize.Association = Association; */ Sequelize.useInflection = Utils.useInflection; -/** - * Allow hooks to be defined on Sequelize + on sequelize instance as universal hooks to run on all models - * and on Sequelize/sequelize methods e.g. Sequelize(), Sequelize#define() - */ -Hooks.applyTo(Sequelize); -Hooks.applyTo(Sequelize.prototype); - /** * Expose various errors available */ diff --git a/src/utils/deprecations.ts b/src/utils/deprecations.ts index 73ea0b681e7e..5ce9a146b46d 100644 --- a/src/utils/deprecations.ts +++ b/src/utils/deprecations.ts @@ -15,3 +15,4 @@ export const noSequelizeDataType = deprecate(noop, `Accessing DataTypes on the S e.g, instead of using Sequelize.STRING, use DataTypes.STRING`, 'SEQUELIZE0010'); export const noModelDropSchema = deprecate(noop, 'Do not use Model.dropSchema. Use Sequelize#dropSchema or QueryInterface#dropSchema instead', 'SEQUELIZE0011'); export const movedSequelizeParam = deprecate(noop, 'The "sequelize" instance has been moved from the second parameter bag to the first parameter bag in "beforeAssociate" and "afterAssociate" hooks', 'SEQUELIZE0012'); +export const hooksReworked = deprecate(noop, 'Sequelize Hooks methods, such as addHook, runHooks, beforeFind, and afterSync… are deprecated in favor of using the methods available through "sequelize.hooks", "Sequelize.hooks" and "YourModel.hooks".', 'SEQUELIZE0013'); diff --git a/src/utils/multimap.ts b/src/utils/multimap.ts new file mode 100644 index 000000000000..a28d0d4abd1c --- /dev/null +++ b/src/utils/multimap.ts @@ -0,0 +1,56 @@ +export class Multimap { + #internalMap = new Map(); + + clear() { + this.#internalMap.clear(); + } + + append(key: K, value: V): this { + const valueSet = this.#internalMap.get(key); + if (valueSet != null) { + valueSet.push(value); + + return this; + } + + this.#internalMap.set(key, [value]); + + return this; + } + + delete(key: K, value: V): boolean { + const valueSet = this.#internalMap.get(key); + if (valueSet == null) { + return false; + } + + const index = valueSet.indexOf(value); + if (index === -1) { + return false; + } + + valueSet.splice(index, 1); + + return true; + } + + keys(): IterableIterator { + return this.#internalMap.keys(); + } + + getAll(key: K): V[] { + const values = this.#internalMap.get(key); + + if (values) { + return [...values]; + } + + return []; + } + + count(key: K): number { + const values = this.#internalMap.get(key); + + return values?.length ?? 0; + } +} diff --git a/test/integration/dialects/postgres/query-interface.test.js b/test/integration/dialects/postgres/query-interface.test.js index 42e32872ed90..eb8660ce0026 100644 --- a/test/integration/dialects/postgres/query-interface.test.js +++ b/test/integration/dialects/postgres/query-interface.test.js @@ -182,6 +182,8 @@ if (dialect.startsWith('postgres')) { }); it('uses declared variables', async function () { + await this.sequelize.query('DROP FUNCTION IF EXISTS add_one;'); + const body = 'RETURN myVar + 1;'; const options = { variables: [{ type: 'integer', name: 'myVar', default: 100 }] }; await this.queryInterface.createFunction('add_one', [], 'integer', 'plpgsql', body, [], options); diff --git a/test/integration/hooks/hooks.test.js b/test/integration/hooks/hooks.test.js index 7c936aeb44da..2c24462a155c 100644 --- a/test/integration/hooks/hooks.test.js +++ b/test/integration/hooks/hooks.test.js @@ -8,6 +8,7 @@ const { DataTypes, Sequelize } = require('@sequelize/core'); const dialect = Support.getTestDialect(); const sinon = require('sinon'); +const { createSequelizeInstance } = require('../../support'); describe(Support.getTestDialectTeaser('Hooks'), () => { beforeEach(async function () { @@ -65,15 +66,15 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { }); after(function () { - this.sequelize.options.hooks = {}; + this.sequelize.hooks.removeAllListeners(); this.sequelize.modelManager.removeModel(this.model); }); }); describe('#init', () => { before(function () { - Sequelize.addHook('beforeInit', (config, options) => { - config.database = 'db2'; + Sequelize.addHook('beforeInit', options => { + options.database = 'db2'; options.host = 'server9'; }); @@ -97,7 +98,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { }); after(() => { - Sequelize.options.hooks = {}; + Sequelize.hooks.removeAllListeners(); }); }); @@ -310,96 +311,48 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { }); afterEach(function () { - this.sequelize.options.hooks = {}; + this.sequelize.hooks.removeAllListeners(); }); }); describe('on error', () => { - - it('should return an error from before', async function () { + it('should return an error from before', async () => { const beforeHook = sinon.spy(); const afterHook = sinon.spy(); - this.sequelize.beforeBulkSync(() => { + const tmpSequelize = createSequelizeInstance(); + + tmpSequelize.beforeBulkSync(() => { beforeHook(); throw new Error('Whoops!'); }); - this.sequelize.afterBulkSync(afterHook); - await expect(this.sequelize.sync()).to.be.rejected; + tmpSequelize.afterBulkSync(afterHook); + + await expect(tmpSequelize.sync()).to.be.rejectedWith('Whoops!'); expect(beforeHook).to.have.been.calledOnce; expect(afterHook).not.to.have.been.called; }); - it('should return an error from after', async function () { + it('should return an error from after', async () => { const beforeHook = sinon.spy(); const afterHook = sinon.spy(); + const tmpSequelize = createSequelizeInstance(); - this.sequelize.beforeBulkSync(beforeHook); - this.sequelize.afterBulkSync(() => { + tmpSequelize.beforeBulkSync(beforeHook); + tmpSequelize.afterBulkSync(() => { afterHook(); throw new Error('Whoops!'); }); - await expect(this.sequelize.sync()).to.be.rejected; + await expect(tmpSequelize.sync()).to.be.rejectedWith('Whoops!'); expect(beforeHook).to.have.been.calledOnce; expect(afterHook).to.have.been.calledOnce; }); afterEach(function () { - this.sequelize.options.hooks = {}; + this.sequelize.hooks.removeAllListeners(); }); - - }); - }); - - describe('#removal', () => { - it('should be able to remove by name', async function () { - const sasukeHook = sinon.spy(); - const narutoHook = sinon.spy(); - - this.User.addHook('beforeCreate', 'sasuke', sasukeHook); - this.User.addHook('beforeCreate', 'naruto', narutoHook); - - await this.User.create({ username: 'makunouchi' }); - expect(sasukeHook).to.have.been.calledOnce; - expect(narutoHook).to.have.been.calledOnce; - this.User.removeHook('beforeCreate', 'sasuke'); - await this.User.create({ username: 'sendo' }); - expect(sasukeHook).to.have.been.calledOnce; - expect(narutoHook).to.have.been.calledTwice; - }); - - it('should be able to remove by reference', async function () { - const sasukeHook = sinon.spy(); - const narutoHook = sinon.spy(); - - this.User.addHook('beforeCreate', sasukeHook); - this.User.addHook('beforeCreate', narutoHook); - - await this.User.create({ username: 'makunouchi' }); - expect(sasukeHook).to.have.been.calledOnce; - expect(narutoHook).to.have.been.calledOnce; - this.User.removeHook('beforeCreate', sasukeHook); - await this.User.create({ username: 'sendo' }); - expect(sasukeHook).to.have.been.calledOnce; - expect(narutoHook).to.have.been.calledTwice; - }); - - it('should be able to remove proxies', async function () { - const sasukeHook = sinon.spy(); - const narutoHook = sinon.spy(); - - this.User.addHook('beforeSave', sasukeHook); - this.User.addHook('beforeSave', narutoHook); - - const user = await this.User.create({ username: 'makunouchi' }); - expect(sasukeHook).to.have.been.calledOnce; - expect(narutoHook).to.have.been.calledOnce; - this.User.removeHook('beforeSave', sasukeHook); - await user.update({ username: 'sendo' }); - expect(sasukeHook).to.have.been.calledOnce; - expect(narutoHook).to.have.been.calledTwice; }); }); }); diff --git a/test/types/hooks.ts b/test/types/hooks.ts index 2fface417324..be7bc721d6d6 100644 --- a/test/types/hooks.ts +++ b/test/types/hooks.ts @@ -1,4 +1,11 @@ -import type { FindOptions, QueryOptions, SaveOptions, UpsertOptions, Config, Utils } from '@sequelize/core'; +import type { + FindOptions, + QueryOptions, + SaveOptions, + UpsertOptions, + Utils, + ConnectionOptions, +} from '@sequelize/core'; import { Model, Sequelize } from '@sequelize/core'; import type { BeforeAssociateEventData, @@ -6,8 +13,8 @@ import type { AssociationOptions, } 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 { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/hooks.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 { expectTypeOf } from 'expect-type'; import type { SemiDeepWritable } from './type-helpers/deep-writable'; @@ -40,14 +47,6 @@ import type { SemiDeepWritable } from './type-helpers/deep-writable'; expectTypeOf(m).toEqualTypeOf<[ TestModel, boolean | null ]>(); expectTypeOf(options).toEqualTypeOf(); }, - beforeQuery(options, query) { - expectTypeOf(options).toEqualTypeOf(); - expectTypeOf(query).toEqualTypeOf(); - }, - afterQuery(options, query) { - expectTypeOf(options).toEqualTypeOf(); - expectTypeOf(query).toEqualTypeOf(); - }, beforeAssociate(data, options) { expectTypeOf(data).toEqualTypeOf(); expectTypeOf(options).toEqualTypeOf>(); @@ -71,10 +70,10 @@ import type { SemiDeepWritable } from './type-helpers/deep-writable'; TestModel.afterSave(hooks.afterSave!); TestModel.afterFind(hooks.afterFind!); - Sequelize.beforeSave(hooks.beforeSave!); - Sequelize.afterSave(hooks.afterSave!); - Sequelize.afterFind(hooks.afterFind!); - Sequelize.afterFind('namedAfterFind', hooks.afterFind!); + sequelize.beforeSave(hooks.beforeSave!); + sequelize.afterSave(hooks.afterSave!); + sequelize.afterFind(hooks.afterFind!); + sequelize.afterFind('namedAfterFind', hooks.afterFind!); } // #12959 @@ -141,21 +140,39 @@ import type { SemiDeepWritable } from './type-helpers/deep-writable'; expectTypeOf(args).toEqualTypeOf>(); }; - hooks.beforeBulkSync = (...args) => { - expectTypeOf(args).toEqualTypeOf>(); - }; - - hooks.beforeQuery = (...args) => { - expectTypeOf(args).toEqualTypeOf>(); - }; - hooks.beforeUpsert = (...args) => { expectTypeOf(args).toEqualTypeOf>(); }; } -Sequelize.beforeConnect('name', config => expectTypeOf(config).toEqualTypeOf>()); -Sequelize.beforeConnect(config => expectTypeOf(config).toEqualTypeOf>()); -Sequelize.addHook('beforeConnect', (...args) => { - expectTypeOf(args).toEqualTypeOf<[Utils.DeepWriteable]>(); +const sequelize = new Sequelize(); + +sequelize.beforeConnect('name', (config: ConnectionOptions) => { + expectTypeOf(config).toMatchTypeOf>(); +}); + +sequelize.beforeConnect((config: ConnectionOptions) => { + expectTypeOf(config).toMatchTypeOf>(); +}); + +sequelize.addHook('beforeConnect', (...args) => { + expectTypeOf(args).toMatchTypeOf<[Utils.DeepWriteable]>(); +}); + +sequelize.beforeQuery((options, query) => { + expectTypeOf(options).toEqualTypeOf(); + expectTypeOf(query).toEqualTypeOf(); +}); + +sequelize.beforeQuery((...args) => { + expectTypeOf(args).toEqualTypeOf>(); +}); + +sequelize.afterQuery((options, query) => { + expectTypeOf(options).toEqualTypeOf(); + expectTypeOf(query).toEqualTypeOf(); +}); + +sequelize.beforeBulkSync((...args) => { + expectTypeOf(args).toEqualTypeOf>(); }); diff --git a/test/types/sequelize.ts b/test/types/sequelize.ts index 8165d6953c38..17ead5eba157 100644 --- a/test/types/sequelize.ts +++ b/test/types/sequelize.ts @@ -1,4 +1,4 @@ -import type { Config, ModelStatic, Utils } from '@sequelize/core'; +import type { Config, ConnectionOptions, ModelStatic, Utils } from '@sequelize/core'; import { Sequelize, Model, QueryTypes, Op } from '@sequelize/core'; Sequelize.useCLS({ @@ -10,7 +10,7 @@ Sequelize.useCLS({ export const sequelize = new Sequelize({ hooks: { - afterConnect: (connection: unknown, config: Config) => { + afterConnect: (connection: unknown, config: ConnectionOptions) => { // noop }, }, @@ -60,22 +60,22 @@ sequelize.beforeCreate('test', () => { }); sequelize - .addHook('beforeConnect', (config: Config) => { + .addHook('beforeConnect', (config: ConnectionOptions) => { // noop }) .addHook('beforeBulkSync', () => { // noop }); -Sequelize.addHook('beforeCreate', () => { +Sequelize.addHook('beforeInit', () => { // noop -}).addHook('beforeBulkCreate', () => { +}).addHook('afterInit', () => { // noop }); -Sequelize.beforeConnect(() => {}); +sequelize.beforeConnect(() => {}); -Sequelize.afterConnect(() => {}); +sequelize.afterConnect(() => {}); const rnd: Utils.Fn = sequelize.random(); diff --git a/test/unit/connection-manager.test.ts b/test/unit/connection-manager.test.ts index 543b7d30e613..01ae7fbbb443 100644 --- a/test/unit/connection-manager.test.ts +++ b/test/unit/connection-manager.test.ts @@ -29,8 +29,6 @@ describe('connection manager', () => { sequelize.beforeConnect(config => { config.username = username; config.password = password; - - return config; }); await sequelize.connectionManager._connect({}); diff --git a/test/unit/esm-named-exports.test.js b/test/unit/esm-named-exports.test.js index b49531f97a04..12f23e977449 100644 --- a/test/unit/esm-named-exports.test.js +++ b/test/unit/esm-named-exports.test.js @@ -42,7 +42,6 @@ describe('ESM module', () => { '_clsRun', 'name', 'version', - 'options', 'postgres', 'mysql', 'mariadb', @@ -51,56 +50,6 @@ describe('ESM module', () => { 'db2', 'mssql', 'ibmi', - '_setupHooks', - 'runHooks', - 'addHook', - 'removeHook', - 'hasHook', - 'hasHooks', - 'beforeValidate', - 'afterValidate', - 'validationFailed', - 'beforeCreate', - 'afterCreate', - 'beforeDestroy', - 'afterDestroy', - 'beforeRestore', - 'afterRestore', - 'beforeUpdate', - 'afterUpdate', - 'beforeSave', - 'afterSave', - 'beforeUpsert', - 'afterUpsert', - 'beforeBulkCreate', - 'afterBulkCreate', - 'beforeBulkDestroy', - 'afterBulkDestroy', - 'beforeBulkRestore', - 'afterBulkRestore', - 'beforeBulkUpdate', - 'afterBulkUpdate', - 'beforeFind', - 'beforeFindAfterExpandIncludeAll', - 'beforeFindAfterOptions', - 'afterFind', - 'beforeCount', - 'beforeDefine', - 'afterDefine', - 'beforeInit', - 'afterInit', - 'beforeAssociate', - 'afterAssociate', - 'beforeConnect', - 'afterConnect', - 'beforeDisconnect', - 'afterDisconnect', - 'beforeSync', - 'afterSync', - 'beforeBulkSync', - 'afterBulkSync', - 'beforeQuery', - 'afterQuery', ]; for (const key of ignoredCjsKeys) { diff --git a/test/unit/hooks.test.js b/test/unit/hooks.test.js index fa9b503f5050..0b3515556645 100644 --- a/test/unit/hooks.test.js +++ b/test/unit/hooks.test.js @@ -8,11 +8,11 @@ const Support = require('./support'); const { DataTypes, Sequelize } = require('@sequelize/core'); const _ = require('lodash'); -const current = Support.sequelize; +const sequelize = Support.sequelize; describe(Support.getTestDialectTeaser('Hooks'), () => { beforeEach(function () { - this.Model = current.define('m'); + this.Model = sequelize.define('m'); }); it('does not expose non-model hooks', function () { @@ -35,14 +35,14 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { describe('proxies', () => { beforeEach(() => { - sinon.stub(current, 'queryRaw').resolves([{ + sinon.stub(sequelize, 'queryRaw').resolves([{ _previousDataValues: {}, dataValues: { id: 1, name: 'abc' }, }]); }); afterEach(() => { - current.queryRaw.restore(); + sequelize.queryRaw.restore(); }); describe('defined by options.hooks', () => { @@ -51,7 +51,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { this.afterSaveHook = sinon.spy(); this.afterCreateHook = sinon.spy(); - this.Model = current.define('m', { + this.Model = sequelize.define('m', { name: DataTypes.STRING, }, { hooks: { @@ -75,7 +75,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { this.beforeSaveHook = sinon.spy(); this.afterSaveHook = sinon.spy(); - this.Model = current.define('m', { + this.Model = sequelize.define('m', { name: DataTypes.STRING, }); @@ -95,7 +95,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { this.beforeSaveHook = sinon.spy(); this.afterSaveHook = sinon.spy(); - this.Model = current.define('m', { + this.Model = sequelize.define('m', { name: DataTypes.STRING, }); @@ -142,7 +142,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { }); it('using define', async function () { - await current.define('M', {}, { + await sequelize.define('M', {}, { hooks: { beforeCreate: [this.hook1, this.hook2, this.hook3], }, @@ -150,7 +150,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { }); it('using a mixture', async function () { - const Model = current.define('M', {}, { + const Model = sequelize.define('M', {}, { hooks: { beforeCreate: this.hook1, }, @@ -195,7 +195,7 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { it('invokes the global hook', async function () { const globalHook = sinon.spy(); - current.addHook('beforeUpdate', globalHook); + sequelize.addHook('beforeUpdate', globalHook); await this.Model.runHooks('beforeUpdate'); expect(globalHook).to.have.been.calledOnce; @@ -206,15 +206,15 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { const globalHookAfter = sinon.spy(); const localHook = sinon.spy(); - current.addHook('beforeUpdate', globalHookBefore); + sequelize.addHook('beforeUpdate', globalHookBefore); - const Model = current.define('m', {}, { + const Model = sequelize.define('m', {}, { hooks: { beforeUpdate: localHook, }, }); - current.addHook('beforeUpdate', globalHookAfter); + sequelize.addHook('beforeUpdate', globalHookAfter); await Model.runHooks('beforeUpdate'); expect(globalHookBefore).to.have.been.calledOnce; @@ -419,4 +419,53 @@ describe(Support.getTestDialectTeaser('Hooks'), () => { expect(this.hook4).to.have.been.calledOnce; }); }); + + describe('#removal', () => { + before(() => { + sinon.stub(sequelize, 'queryRaw').resolves([{ + _previousDataValues: {}, + dataValues: { id: 1, name: 'abc' }, + }]); + }); + + after(() => { + sequelize.queryRaw.restore(); + }); + + it('should be able to remove by name', async () => { + const User = sequelize.define('User'); + + const hook1 = sinon.spy(); + const hook2 = sinon.spy(); + + User.addHook('beforeCreate', 'sasuke', hook1); + User.addHook('beforeCreate', 'naruto', hook2); + + await User.create({ username: 'makunouchi' }); + expect(hook1).to.have.been.calledOnce; + expect(hook2).to.have.been.calledOnce; + User.removeHook('beforeCreate', 'sasuke'); + await User.create({ username: 'sendo' }); + expect(hook1).to.have.been.calledOnce; + expect(hook2).to.have.been.calledTwice; + }); + + it('should be able to remove by reference', async () => { + const User = sequelize.define('User'); + + const hook1 = sinon.spy(); + const hook2 = sinon.spy(); + + User.addHook('beforeCreate', hook1); + User.addHook('beforeCreate', hook2); + + await User.create({ username: 'makunouchi' }); + expect(hook1).to.have.been.calledOnce; + expect(hook2).to.have.been.calledOnce; + User.removeHook('beforeCreate', hook1); + await User.create({ username: 'sendo' }); + expect(hook1).to.have.been.calledOnce; + expect(hook2).to.have.been.calledTwice; + }); + }); });