Skip to content

Commit

Permalink
feat: add model hook decorators (#15333)
Browse files Browse the repository at this point in the history
  • Loading branch information
ephys committed Nov 26, 2022
1 parent a4f8e62 commit bd037c8
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 2 deletions.
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -25,6 +25,11 @@
"require": "./lib/index.js",
"types": "./types/index.d.ts"
},
"./decorators-legacy": {
"types": "./types/decorators/legacy/index.d.ts",
"import": "./lib/decorators/legacy/index.mjs",
"require": "./lib/decorators/legacy/index.js"
},
"./_non-semver-use-at-your-own-risk_/*": {
"types": "./types/*",
"default": "./lib/*"
Expand Down
3 changes: 3 additions & 0 deletions src/decorators/legacy/README.md
@@ -0,0 +1,3 @@
# decorators-legacy

This directory regroups the decorators that are built using the legacy decorator proposal.
75 changes: 75 additions & 0 deletions src/decorators/legacy/hook-decorators.ts
@@ -0,0 +1,75 @@
import upperFirst from 'lodash/upperFirst';
import type { ModelHooks } from '../../model-typescript.js';
import { Model } from '../../model.js';
import { isModelStatic } from '../../utils/model-utils.js';

export interface HookOptions {
name?: string;
}

export type HookDecoratorArgs = [targetOrOptions: Object | HookOptions, propertyName?: string | symbol];

/**
* Implementation for hook decorator functions. These are polymorphic. When
* called with a single argument (IHookOptions) they return a decorator
* factory function. When called with multiple arguments, they add the hook
* to the model’s metadata.
*
* @param hookType
* @param args
*/
export function implementHookDecorator(
hookType: keyof ModelHooks,
args: HookDecoratorArgs,
): MethodDecorator | undefined {
if (args.length === 1) {
const options: HookOptions = args[0];

return (target: Object, propertyName: string | symbol) => {
addHook(target, propertyName, hookType, options);
};
}

const target = args[0];
const propertyName = args[1]!;

addHook(target, propertyName, hookType);

// eslint-disable-next-line consistent-return
return undefined;
}

function addHook(
targetModel: Object,
methodName: string | symbol,
hookType: keyof ModelHooks,
options?: HookOptions,
): void {
if (typeof targetModel !== 'function') {
throw new TypeError(
`Decorator @${upperFirst(hookType)} has been used on method "${targetModel.constructor.name}.${String(methodName)}" which is not static. Only static methods can be used for hooks.`,
);
}

if (!isModelStatic(targetModel)) {
throw new TypeError(
`Decorator @${upperFirst(hookType)} has been used on "${targetModel.name}.${String(methodName)}", but class "${targetModel.name}" does not extend Model. Hook decorators can only be used on models.`,
);
}

// @ts-expect-error
const targetMethod: unknown = targetModel[methodName];
if (typeof targetMethod !== 'function') {
throw new TypeError(
`Decorator @${upperFirst(hookType)} has been used on "${targetModel.name}.${String(methodName)}", which is not a method.`,
);
}

if (methodName in Model) {
throw new Error(
`Decorator @${upperFirst(hookType)} has been used on "${targetModel.name}.${String(methodName)}", but method ${JSON.stringify(methodName)} already exists on the base Model class and replacing it can lead to issues.`,
);
}

targetModel.hooks.addListener(hookType, targetMethod.bind(targetModel), options?.name);
}
34 changes: 34 additions & 0 deletions src/decorators/legacy/index.mjs
@@ -0,0 +1,34 @@
import Pkg from './index.js';

export const AfterAssociate = Pkg.AfterAssociate;
export const AfterBulkCreate = Pkg.AfterBulkCreate;
export const AfterBulkDestroy = Pkg.AfterBulkDestroy;
export const AfterBulkRestore = Pkg.AfterBulkRestore;
export const AfterBulkUpdate = Pkg.AfterBulkUpdate;
export const AfterCreate = Pkg.AfterCreate;
export const AfterDestroy = Pkg.AfterDestroy;
export const AfterFind = Pkg.AfterFind;
export const AfterRestore = Pkg.AfterRestore;
export const AfterSave = Pkg.AfterSave;
export const AfterSync = Pkg.AfterSync;
export const AfterUpdate = Pkg.AfterUpdate;
export const AfterUpsert = Pkg.AfterUpsert;
export const AfterValidate = Pkg.AfterValidate;
export const BeforeAssociate = Pkg.BeforeAssociate;
export const BeforeBulkCreate = Pkg.BeforeBulkCreate;
export const BeforeBulkDestroy = Pkg.BeforeBulkDestroy;
export const BeforeBulkRestore = Pkg.BeforeBulkRestore;
export const BeforeBulkUpdate = Pkg.BeforeBulkUpdate;
export const BeforeCount = Pkg.BeforeCount;
export const BeforeCreate = Pkg.BeforeCreate;
export const BeforeDestroy = Pkg.BeforeDestroy;
export const BeforeFind = Pkg.BeforeFind;
export const BeforeFindAfterExpandIncludeAll = Pkg.BeforeFindAfterExpandIncludeAll;
export const BeforeFindAfterOptions = Pkg.BeforeFindAfterOptions;
export const BeforeRestore = Pkg.BeforeRestore;
export const BeforeSave = Pkg.BeforeSave;
export const BeforeSync = Pkg.BeforeSync;
export const BeforeUpdate = Pkg.BeforeUpdate;
export const BeforeUpsert = Pkg.BeforeUpsert;
export const BeforeValidate = Pkg.BeforeValidate;
export const ValidationFailed = Pkg.ValidationFailed;
2 changes: 2 additions & 0 deletions src/decorators/legacy/index.ts
@@ -0,0 +1,2 @@
export * from './model-hooks-bulk.js';
export * from './model-hooks-single.js';
50 changes: 50 additions & 0 deletions src/decorators/legacy/model-hooks-bulk.ts
@@ -0,0 +1,50 @@
import type { HookDecoratorArgs, HookOptions } from './hook-decorators.js';
import { implementHookDecorator } from './hook-decorators.js';

export function BeforeBulkCreate(target: Object, propertyName: string): void;
export function BeforeBulkCreate(options: HookOptions): MethodDecorator;
export function BeforeBulkCreate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeBulkCreate', args);
}

export function AfterBulkCreate(target: Object, propertyName: string): void;
export function AfterBulkCreate(options: HookOptions): MethodDecorator;
export function AfterBulkCreate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterBulkCreate', args);
}

export function BeforeBulkDestroy(target: Object, propertyName: string): void;
export function BeforeBulkDestroy(options: HookOptions): MethodDecorator;
export function BeforeBulkDestroy(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeBulkDestroy', args);
}

export function AfterBulkDestroy(target: Object, propertyName: string): void;
export function AfterBulkDestroy(options: HookOptions): MethodDecorator;
export function AfterBulkDestroy(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterBulkDestroy', args);
}

export function BeforeBulkRestore(target: Object, propertyName: string): void;
export function BeforeBulkRestore(options: HookOptions): MethodDecorator;
export function BeforeBulkRestore(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeBulkRestore' as any, args);
}

export function AfterBulkRestore(target: Object, propertyName: string): void;
export function AfterBulkRestore(options: HookOptions): MethodDecorator;
export function AfterBulkRestore(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterBulkRestore' as any, args);
}

export function BeforeBulkUpdate(target: Object, propertyName: string): void;
export function BeforeBulkUpdate(options: HookOptions): MethodDecorator;
export function BeforeBulkUpdate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeBulkUpdate', args);
}

export function AfterBulkUpdate(target: Object, propertyName: string): void;
export function AfterBulkUpdate(options: HookOptions): MethodDecorator;
export function AfterBulkUpdate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterBulkUpdate', args);
}
146 changes: 146 additions & 0 deletions src/decorators/legacy/model-hooks-single.ts
@@ -0,0 +1,146 @@
import type { HookOptions, HookDecoratorArgs } from './hook-decorators.js';
import { implementHookDecorator } from './hook-decorators.js';

export function BeforeAssociate(target: Object, propertyName: string): void;
export function BeforeAssociate(options: HookOptions): MethodDecorator;
export function BeforeAssociate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeAssociate', args);
}

export function AfterAssociate(target: Object, propertyName: string): void;
export function AfterAssociate(options: HookOptions): MethodDecorator;
export function AfterAssociate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterAssociate', args);
}

export function BeforeCount(target: Object, propertyName: string): void;
export function BeforeCount(options: HookOptions): MethodDecorator;
export function BeforeCount(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeCount', args);
}

export function BeforeCreate(target: Object, propertyName: string): void;
export function BeforeCreate(options: HookOptions): MethodDecorator;
export function BeforeCreate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeCreate', args);
}

export function AfterCreate(target: Object, propertyName: string): void;
export function AfterCreate(options: HookOptions): MethodDecorator;
export function AfterCreate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterCreate', args);
}

export function BeforeDestroy(target: Object, propertyName: string): void;
export function BeforeDestroy(options: HookOptions): MethodDecorator;
export function BeforeDestroy(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeDestroy', args);
}

export function AfterDestroy(target: Object, propertyName: string): void;
export function AfterDestroy(options: HookOptions): MethodDecorator;
export function AfterDestroy(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterDestroy', args);
}

export function BeforeFind(target: Object, propertyName: string): void;
export function BeforeFind(options: HookOptions): MethodDecorator;
export function BeforeFind(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeFind', args);
}

export function BeforeFindAfterExpandIncludeAll(target: Object, propertyName: string): void;
export function BeforeFindAfterExpandIncludeAll(options: HookOptions): MethodDecorator;
export function BeforeFindAfterExpandIncludeAll(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeFindAfterExpandIncludeAll', args);
}

export function BeforeFindAfterOptions(target: Object, propertyName: string): void;
export function BeforeFindAfterOptions(options: HookOptions): MethodDecorator;
export function BeforeFindAfterOptions(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeFindAfterOptions', args);
}

export function AfterFind(target: Object, propertyName: string): void;
export function AfterFind(options: HookOptions): MethodDecorator;
export function AfterFind(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterFind', args);
}

export function BeforeRestore(target: Object, propertyName: string): void;
export function BeforeRestore(options: HookOptions): MethodDecorator;
export function BeforeRestore(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeRestore' as any, args);
}

export function AfterRestore(target: Object, propertyName: string): void;
export function AfterRestore(options: HookOptions): MethodDecorator;
export function AfterRestore(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterRestore' as any, args);
}

export function BeforeSave(target: Object, propertyName: string): void;
export function BeforeSave(options: HookOptions): MethodDecorator;
export function BeforeSave(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeSave' as any, args);
}

export function AfterSave(target: Object, propertyName: string): void;
export function AfterSave(options: HookOptions): MethodDecorator;
export function AfterSave(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterSave' as any, args);
}

export function BeforeSync(target: Object, propertyName: string): void;
export function BeforeSync(options: HookOptions): MethodDecorator;
export function BeforeSync(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeSync', args);
}

export function AfterSync(target: Object, propertyName: string): void;
export function AfterSync(options: HookOptions): MethodDecorator;
export function AfterSync(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterSync', args);
}

export function BeforeUpdate(target: Object, propertyName: string): void;
export function BeforeUpdate(options: HookOptions): MethodDecorator;
export function BeforeUpdate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeUpdate', args);
}

export function AfterUpdate(target: Object, propertyName: string): void;
export function AfterUpdate(options: HookOptions): MethodDecorator;
export function AfterUpdate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterUpdate', args);
}

export function BeforeUpsert(target: Object, propertyName: string): void;
export function BeforeUpsert(options: HookOptions): MethodDecorator;
export function BeforeUpsert(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeUpsert' as any, args);
}

export function AfterUpsert(target: Object, propertyName: string): void;
export function AfterUpsert(options: HookOptions): MethodDecorator;
export function AfterUpsert(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterUpsert' as any, args);
}

export function BeforeValidate(target: Object, propertyName: string): void;
export function BeforeValidate(options: HookOptions): MethodDecorator;
export function BeforeValidate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('beforeValidate', args);
}

export function ValidationFailed(target: Object, propertyName: string): void;
export function ValidationFailed(options: HookOptions): MethodDecorator;
export function ValidationFailed(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('validationFailed' as any, args);
}

export function AfterValidate(target: Object, propertyName: string): void;
export function AfterValidate(options: HookOptions): MethodDecorator;
export function AfterValidate(...args: HookDecoratorArgs): undefined | MethodDecorator {
return implementHookDecorator('afterValidate', args);
}
6 changes: 4 additions & 2 deletions test/tsconfig.json
Expand Up @@ -6,12 +6,14 @@
"paths": {
"@sequelize/core": ["../types/index.d.ts"],
"@sequelize/core/_non-semver-use-at-your-own-risk_/*": ["../types/*"],
"@sequelize/core/package.json": ["../package.json"]
"@sequelize/core/package.json": ["../package.json"],
"@sequelize/core/decorators-legacy": ["../types/decorators/legacy/index.d.ts"]
},
"types": ["node", "mocha", "sinon", "chai", "sinon-chai", "chai-as-promised", "chai-datetime"],
"noEmit": true,
"emitDeclarationOnly": false,
"exactOptionalPropertyTypes": false
"exactOptionalPropertyTypes": false,
"experimentalDecorators": true
},
"include": ["./types/**/*", "./**/**/*.ts"]
}

0 comments on commit bd037c8

Please sign in to comment.