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

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,
});
console.log(tags);
// console.log(tags);
});
test('update raw attribute', async () => {

View File

@ -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({

View File

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

View File

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

View File

@ -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<string>();
boundEvents = new Set<string>();
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 <keyof SequelizeHooks>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 ? <keyof SequelizeHooks>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);
};
}
}