diff --git a/packages/database/src/__tests__/hooks.test.ts b/packages/database/src/__tests__/hooks.test.ts index d3ba6822e..f68ef3f15 100644 --- a/packages/database/src/__tests__/hooks.test.ts +++ b/packages/database/src/__tests__/hooks.test.ts @@ -122,7 +122,7 @@ describe('hooks', () => { expect(test.get('arr')).toEqual([1, 2]); }); - it.only('add hook in custom field', async () => { + it('add hook in custom field', async () => { const table = db.table({ name: 'test3', fields: [ @@ -211,4 +211,61 @@ describe('hooks', () => { const test = await Test3.create({}); expect(test.get('arr')).toEqual([3, 5, 1, 4, 6, 2]); }); + + it('event emitter', async () => { + const table = db.table({ + name: 'test', + fields: [ + { + type: 'json', + name: 'arr', + defaultValue: [], + }, + ], + }); + db.table({ + name: 'test2', + fields: [ + { + type: 'json', + name: 'arr', + defaultValue: [], + }, + ], + }); + await db.sync(); + const arr = []; + db.on('afterCreate', async function abc1(...args) { + return new Promise((resolve) => { + setTimeout(() => { + console.log('db.on.afterCreate') + arr.push(1); + resolve(); + }, 200); + }); + }); + db.on('test.afterCreate', async function abc2(model, options) { + return new Promise((resolve) => { + setTimeout(() => { + console.log('db.on.test.afterCreate') + arr.push(2); + resolve(); + }, 100); + }); + }); + db.on('test2.afterCreate', async (model, options) => { + return new Promise((resolve) => { + setTimeout(() => { + console.log('db.on.test2.afterCreate') + arr.push(3); + resolve(); + }, 100); + }); + }); + const Test2 = db.getModel('test2'); + await Test2.create(); + const Test1 = db.getModel('test'); + await Test1.create(); + expect(arr).toEqual([3,1,2,1]); + }); }); diff --git a/packages/database/src/database.ts b/packages/database/src/database.ts index dcf166189..3f92a5b0e 100644 --- a/packages/database/src/database.ts +++ b/packages/database/src/database.ts @@ -8,6 +8,7 @@ import Table, { MergeOptions, TableOptions } from './table'; import { Model, ModelCtor } from './model'; import { requireModule } from './utils'; import _ from 'lodash'; +import { EventEmitter } from 'events'; export interface SyncOptions extends SequelizeSyncOptions { @@ -48,7 +49,7 @@ export function extend(tableOptions: TableOptions, mergeOptions: MergeOptions = return new Extend(tableOptions, mergeOptions); } -export default class Database { +export default class Database extends EventEmitter { public readonly sequelize: Sequelize; @@ -70,11 +71,140 @@ export default class Database { protected extTableOptions = new Map(); - constructor(options: DatabaseOptions) { + protected hookTypes = new Set([ + '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' + ]); + + constructor(options?: DatabaseOptions) { + super(); this.options = options; this.sequelize = new Sequelize(options); } + private _getHookType(event: any) { + if (typeof event === 'string') { + event = event.split('.'); + } + if (!Array.isArray(event)) { + return; + } + const hookType = [...event].pop(); + if (!this.hookTypes.has(hookType)) { + return; + } + return hookType; + } + + on(event: string | symbol, listener: (...args: any[]) => void) { + const hookType = this._getHookType(event); + if (hookType) { + console.log('sequelize.addHook', hookType) + this.sequelize.addHook(hookType, async (model) => { + await this.emitAsync(`${model.constructor.name}.${hookType}`); + await this.emitAsync(hookType); + }); + this.hookTypes.delete(hookType); + } + return super.on(event, listener); + } + + async emitAsync(event: string | symbol, ...args: any[]): Promise { + // @ts-ignore + const events = this._events; + let callbacks = events?.[event]; + if (!callbacks) { + return false; + } + // helper function to reuse as much code as possible + const run = (cb) => { + switch (args.length) { + // fast cases + case 0: + cb = cb.call(this); + break; + case 1: + cb = cb.call(this, args[0]); + break; + case 2: + cb = cb.call(this, args[0], args[1]); + break; + case 3: + cb = cb.call(this, args[0], args[1], args[2]); + break; + // slower + default: + cb = cb.apply(this, args); + } + + if ( + cb && ( + cb instanceof Promise || + typeof cb.then === 'function' + ) + ) { + return cb; + } + + return Promise.resolve(true); + }; + + if (typeof callbacks === 'function') { + await run(callbacks); + } else if (typeof callbacks === 'object') { + callbacks = callbacks.slice().filter(Boolean); + await callbacks.reduce((prev, next) => { + return prev.then((res) => { + return run(next).then((result) => Promise.resolve(res.concat(result))); + }); + }, Promise.resolve([])); + } + + return true; + } + /** * 载入指定目录下 tables 配置(配置的文件驱动) *