diff --git a/packages/core/database/src/__tests__/database.test.ts b/packages/core/database/src/__tests__/database.test.ts index 71adbf24b..6dfe798ab 100644 --- a/packages/core/database/src/__tests__/database.test.ts +++ b/packages/core/database/src/__tests__/database.test.ts @@ -204,6 +204,53 @@ describe('database', () => { expect(listener).toHaveBeenCalled(); }); + test('off collection event', async () => { + const Post = db.collection({ + name: 'posts', + fields: [{ type: 'string', name: 'title' }], + }); + + const postAfterCreateListener = jest.fn(); + + db.on('posts.afterCreate', postAfterCreateListener); + + await db.sync(); + + db.off('posts.afterCreate', postAfterCreateListener); + + await Post.repository.create({ + values: { + title: 'test', + }, + }); + + expect(postAfterCreateListener).toHaveBeenCalledTimes(0); + }); + + test('off global event', async () => { + const Post = db.collection({ + name: 'posts', + fields: [{ type: 'string', name: 'title' }], + }); + + const postAfterCreateListener = jest.fn(); + + db.on('posts.afterCreate', postAfterCreateListener); + db.on('afterCreate', postAfterCreateListener); + + await db.sync(); + + db.off('afterCreate', postAfterCreateListener); + + await Post.repository.create({ + values: { + title: 'test', + }, + }); + + expect(postAfterCreateListener).toHaveBeenCalledTimes(1); + }); + test('custom model', async () => { class CustomModel extends Model { customMethod() { diff --git a/packages/core/database/src/__tests__/model-hook.test.ts b/packages/core/database/src/__tests__/model-hook.test.ts new file mode 100644 index 000000000..8d398ee01 --- /dev/null +++ b/packages/core/database/src/__tests__/model-hook.test.ts @@ -0,0 +1,54 @@ +import { Database } from '..'; +import { mockDatabase } from '.'; + +describe('model hook', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + describe('match', () => { + test('sequelize db hooks', async () => { + const matcher = db.modelHook.match('beforeDefine'); + expect(matcher).toEqual('beforeDefine'); + }); + + test('sequelize global model hooks', async () => { + const matcher = db.modelHook.match('beforeCreate'); + expect(matcher).toEqual('beforeCreate'); + }); + + test('sequelize model hooks without existing collection', async () => { + const matcher = db.modelHook.match('posts.beforeCreate'); + expect(matcher).toEqual('beforeCreate'); + }); + + test('sequelize model hooks with existing collection', async () => { + db.collection({ + name: 'posts', + fields: [] + }); + const matcher = db.modelHook.match('posts.beforeCreate'); + expect(matcher).toEqual('beforeCreate'); + }); + + test('customized global hooks', async () => { + const matcher = db.modelHook.match('beforeDefineCollection'); + expect(matcher).toBeNull(); + }); + + test('customized model hooks', async () => { + db.collection({ + name: 'posts', + fields: [] + }); + const matcher = db.modelHook.match('posts.beforeCreateWithAssociations'); + expect(matcher).toBeNull(); + }); + }); +}); diff --git a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts index 164ddee80..98799a82f 100644 --- a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts +++ b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts @@ -308,7 +308,7 @@ describe('belongs to many', () => { offset: 0, }); - console.log(tags); + // console.log(tags); }); test('update raw attribute', async () => { diff --git a/packages/core/database/src/__tests__/sequelize-hooks.test.ts b/packages/core/database/src/__tests__/sequelize-hooks.test.ts index ca07f9d8a..08db44736 100644 --- a/packages/core/database/src/__tests__/sequelize-hooks.test.ts +++ b/packages/core/database/src/__tests__/sequelize-hooks.test.ts @@ -14,6 +14,33 @@ describe('sequelize-hooks', () => { await db.close(); }); + test('exec order', async () => { + const collection = db.collection({ + name: 't_test', + }); + const orders = []; + db.on('beforeCreate', () => { + orders.push('beforeCreate'); + }); + db.on('t_test.beforeCreate', () => { + orders.push('model.beforeCreate'); + }); + db.on('afterCreate', () => { + orders.push('afterCreate'); + }); + db.on('t_test.afterCreate', () => { + orders.push('model.afterCreate'); + }); + await collection.sync(); + await collection.model.create(); + expect(orders).toEqual([ + 'model.beforeCreate', + 'beforeCreate', + 'model.afterCreate', + 'afterCreate' + ]); + }); + describe('afterSync', () => { test('singular name', async () => { const collection = db.collection({ diff --git a/packages/core/database/src/__tests__/update-associations.test.ts b/packages/core/database/src/__tests__/update-associations.test.ts index 420621533..c4c6c3491 100644 --- a/packages/core/database/src/__tests__/update-associations.test.ts +++ b/packages/core/database/src/__tests__/update-associations.test.ts @@ -173,7 +173,7 @@ describe('update associations', () => { await updateAssociations(user1, { posts: post1, }); - console.log(JSON.stringify(user1, null, 2)); + expect(user1.toJSON()).toMatchObject({ name: 'user1', }); @@ -191,7 +191,7 @@ describe('update associations', () => { name: 'post111', }, }); - console.log(JSON.stringify(user1, null, 2)); + expect(user1.toJSON()).toMatchObject({ name: 'user1', }); @@ -216,7 +216,7 @@ describe('update associations', () => { post3, ], }); - console.log(JSON.stringify(user1, null, 2)); + expect(user1.toJSON()).toMatchObject({ name: 'user1', }); diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index dec73f699..656d621df 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -356,13 +356,13 @@ export class Database extends EventEmitter implements AsyncEmitter { return this.sequelize.close(); } - on(event: string | symbol, listener: (...args: any[]) => void): this { - const modelEventName = this.modelHook.isModelHook(event); + on(event: string | symbol, listener): this { + // NOTE: to match if event is a sequelize or model type + const type = this.modelHook.match(event); - if (modelEventName && !this.modelHook.hasBindEvent(modelEventName)) { - this.sequelize.addHook(modelEventName, this.modelHook.sequelizeHookBuilder(modelEventName)); - - this.modelHook.bindEvent(modelEventName); + if (type && !this.modelHook.hasBoundEvent(type)) { + this.sequelize.addHook(type, this.modelHook.buildSequelizeHook(type)); + this.modelHook.bindEvent(type); } return super.on(event, listener); diff --git a/packages/core/database/src/model-hook.ts b/packages/core/database/src/model-hook.ts index d2f652941..a7c1bb749 100644 --- a/packages/core/database/src/model-hook.ts +++ b/packages/core/database/src/model-hook.ts @@ -1,28 +1,30 @@ import lodash from 'lodash'; -import type { SequelizeHooks } from 'sequelize/types/lib/hooks'; import Database from './database'; import { Model } from './model'; +import type { SequelizeHooks } from 'sequelize/types/lib/hooks'; const { hooks } = require('sequelize/lib/hooks'); + + export class ModelHook { database: Database; - boundEvent = new Set(); + + boundEvents = new Set(); constructor(database: Database) { this.database = database; } - isModelHook(eventName: string | symbol): keyof SequelizeHooks | false { - if (lodash.isString(eventName)) { - const hookType = eventName.split('.').pop(); - - if (hooks[hookType]) { - return hookType; - } + match(event: string | Symbol): keyof SequelizeHooks | null { + // NOTE: skip Symbol event + if (!lodash.isString(event)) { + return null; } - return false; + const type = event.split('.').pop(); + + return type in hooks ? type : null; } findModelName(hookArgs) { @@ -47,25 +49,24 @@ export class ModelHook { return null; } - bindEvent(eventName) { - this.boundEvent.add(eventName); + bindEvent(type) { + this.boundEvents.add(type); } - hasBindEvent(eventName) { - return this.boundEvent.has(eventName); + hasBoundEvent(type): boolean { + return this.boundEvents.has(type); } - sequelizeHookBuilder(eventName) { + buildSequelizeHook(type) { return async (...args: any[]) => { const modelName = this.findModelName(args); - if (modelName) { // emit model event - await this.database.emitAsync(`${modelName}.${eventName}`, ...args); + await this.database.emitAsync(`${modelName}.${type}`, ...args); } // emit sequelize global event - await this.database.emitAsync(eventName, ...args); + await this.database.emitAsync(type, ...args); }; } }