refactor(database): hook proxy (#402)

* refactor(database): make db hooks clearly managed by proxy and support named events

* refactor(database): roll back most event logic

* refactor(database): rollback module name

* fix(database): fix test file path

* test: exec order

* fix(database): rollback buildSequelizeHook method

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Junyi 2022-06-08 11:22:08 +08:00 committed by GitHub
parent 75f417a006
commit 78071b4cd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 157 additions and 28 deletions

View File

@ -204,6 +204,53 @@ describe('database', () => {
expect(listener).toHaveBeenCalled(); 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 () => { test('custom model', async () => {
class CustomModel extends Model { class CustomModel extends Model {
customMethod() { customMethod() {

View File

@ -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();
});
});
});

View File

@ -308,7 +308,7 @@ describe('belongs to many', () => {
offset: 0, offset: 0,
}); });
console.log(tags); // console.log(tags);
}); });
test('update raw attribute', async () => { test('update raw attribute', async () => {

View File

@ -14,6 +14,33 @@ describe('sequelize-hooks', () => {
await db.close(); 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', () => { describe('afterSync', () => {
test('singular name', async () => { test('singular name', async () => {
const collection = db.collection({ const collection = db.collection({

View File

@ -173,7 +173,7 @@ describe('update associations', () => {
await updateAssociations(user1, { await updateAssociations(user1, {
posts: post1, posts: post1,
}); });
console.log(JSON.stringify(user1, null, 2));
expect(user1.toJSON()).toMatchObject({ expect(user1.toJSON()).toMatchObject({
name: 'user1', name: 'user1',
}); });
@ -191,7 +191,7 @@ describe('update associations', () => {
name: 'post111', name: 'post111',
}, },
}); });
console.log(JSON.stringify(user1, null, 2));
expect(user1.toJSON()).toMatchObject({ expect(user1.toJSON()).toMatchObject({
name: 'user1', name: 'user1',
}); });
@ -216,7 +216,7 @@ describe('update associations', () => {
post3, post3,
], ],
}); });
console.log(JSON.stringify(user1, null, 2));
expect(user1.toJSON()).toMatchObject({ expect(user1.toJSON()).toMatchObject({
name: 'user1', name: 'user1',
}); });

View File

@ -356,13 +356,13 @@ export class Database extends EventEmitter implements AsyncEmitter {
return this.sequelize.close(); return this.sequelize.close();
} }
on(event: string | symbol, listener: (...args: any[]) => void): this { on(event: string | symbol, listener): this {
const modelEventName = this.modelHook.isModelHook(event); // NOTE: to match if event is a sequelize or model type
const type = this.modelHook.match(event);
if (modelEventName && !this.modelHook.hasBindEvent(modelEventName)) { if (type && !this.modelHook.hasBoundEvent(type)) {
this.sequelize.addHook(modelEventName, this.modelHook.sequelizeHookBuilder(modelEventName)); this.sequelize.addHook(type, this.modelHook.buildSequelizeHook(type));
this.modelHook.bindEvent(type);
this.modelHook.bindEvent(modelEventName);
} }
return super.on(event, listener); return super.on(event, listener);

View File

@ -1,28 +1,30 @@
import lodash from 'lodash'; import lodash from 'lodash';
import type { SequelizeHooks } from 'sequelize/types/lib/hooks';
import Database from './database'; import Database from './database';
import { Model } from './model'; import { Model } from './model';
import type { SequelizeHooks } from 'sequelize/types/lib/hooks';
const { hooks } = require('sequelize/lib/hooks'); const { hooks } = require('sequelize/lib/hooks');
export class ModelHook { export class ModelHook {
database: Database; database: Database;
boundEvent = new Set<string>();
boundEvents = new Set<string>();
constructor(database: Database) { constructor(database: Database) {
this.database = database; this.database = database;
} }
isModelHook(eventName: string | symbol): keyof SequelizeHooks | false { match(event: string | Symbol): keyof SequelizeHooks | null {
if (lodash.isString(eventName)) { // NOTE: skip Symbol event
const hookType = eventName.split('.').pop(); if (!lodash.isString(event)) {
return null;
if (hooks[hookType]) {
return <keyof SequelizeHooks>hookType;
}
} }
return false; const type = event.split('.').pop();
return type in hooks ? <keyof SequelizeHooks>type : null;
} }
findModelName(hookArgs) { findModelName(hookArgs) {
@ -47,25 +49,24 @@ export class ModelHook {
return null; return null;
} }
bindEvent(eventName) { bindEvent(type) {
this.boundEvent.add(eventName); this.boundEvents.add(type);
} }
hasBindEvent(eventName) { hasBoundEvent(type): boolean {
return this.boundEvent.has(eventName); return this.boundEvents.has(type);
} }
sequelizeHookBuilder(eventName) { buildSequelizeHook(type) {
return async (...args: any[]) => { return async (...args: any[]) => {
const modelName = this.findModelName(args); const modelName = this.findModelName(args);
if (modelName) { if (modelName) {
// emit model event // emit model event
await this.database.emitAsync(`${modelName}.${eventName}`, ...args); await this.database.emitAsync(`${modelName}.${type}`, ...args);
} }
// emit sequelize global event // emit sequelize global event
await this.database.emitAsync(eventName, ...args); await this.database.emitAsync(type, ...args);
}; };
} }
} }