refactor: server hooks

This commit is contained in:
Chareice 2022-02-09 20:24:25 +08:00 committed by chenos
parent fca6977ad1
commit 159775ff54
9 changed files with 195 additions and 125 deletions

View File

@ -17,9 +17,13 @@ describe('server hooks', () => {
'x-uid': 'table', 'x-uid': 'table',
'x-component': 'Table', 'x-component': 'Table',
'x-collection': 'posts', 'x-collection': 'posts',
'x-server-hooks': { 'x-server-hooks': [
afterDestroyCollection: ['onCollectionDestroy'], {
}, type: 'onCollectionDestroy',
collection: 'posts',
method: 'onCollectionDestroy',
},
],
properties: { properties: {
col1: { col1: {
'x-uid': 'col1', 'x-uid': 'col1',
@ -29,9 +33,14 @@ describe('server hooks', () => {
'x-uid': 'field1', 'x-uid': 'field1',
'x-component': 'Input', 'x-component': 'Input',
'x-collection-field': 'posts.title', 'x-collection-field': 'posts.title',
'x-server-hooks': { 'x-server-hooks': [
afterDestroyField: ['onFieldDestroy'], {
}, type: 'onCollectionFieldDestroy',
collection: 'posts',
fields: ['title'],
method: 'onFieldDestroy',
},
],
}, },
}, },
}, },
@ -63,29 +72,7 @@ describe('server hooks', () => {
uiSchemaPlugin = app.getPlugin<PluginUiSchema>('PluginUiSchema'); uiSchemaPlugin = app.getPlugin<PluginUiSchema>('PluginUiSchema');
}); });
it('should save uiSchemaAttrs', async () => { it('should call server hooks onFieldDestroy', async () => {
const node = await uiSchemaRepository.findOne({
filter: {
uid: 'table',
},
});
// @ts-ignore
const nodeAttr = await node.getAttrs();
expect(nodeAttr.get('collectionPath')).toEqual('posts');
const nodeField = await uiSchemaRepository.findOne({
filter: {
uid: 'field1',
},
});
// @ts-ignore
const nodeFieldAttr = await nodeField.getAttrs();
expect(nodeFieldAttr.get('collectionPath')).toEqual('posts.title');
});
it('should call server hooks afterFieldDestroy', async () => {
const PostModel = await db.getRepository('collections').create({ const PostModel = await db.getRepository('collections').create({
values: { values: {
name: 'posts', name: 'posts',
@ -106,7 +93,7 @@ describe('server hooks', () => {
const serverHooks = uiSchemaPlugin.serverHooks; const serverHooks = uiSchemaPlugin.serverHooks;
const hookFn = jest.fn(); const hookFn = jest.fn();
serverHooks.register('afterDestroyField', 'onFieldDestroy', hookFn); serverHooks.register('onCollectionFieldDestroy', 'onFieldDestroy', hookFn);
// destroy a field // destroy a field
await db.getRepository('fields').destroy({ await db.getRepository('fields').destroy({
@ -119,7 +106,7 @@ describe('server hooks', () => {
expect(hookFn).toHaveBeenCalled(); expect(hookFn).toHaveBeenCalled();
}); });
it('should call server hooks afterCollectionDestroy', async () => { it('should call server hooks onCollectionDestroy', async () => {
const PostModel = await db.getRepository('collections').create({ const PostModel = await db.getRepository('collections').create({
values: { values: {
name: 'posts', name: 'posts',
@ -141,7 +128,7 @@ describe('server hooks', () => {
const hookFn = jest.fn(); const hookFn = jest.fn();
serverHooks.register('afterDestroyCollection', 'onCollectionDestroy', hookFn); serverHooks.register('onCollectionDestroy', 'onCollectionDestroy', hookFn);
// destroy a field // destroy a field
await db.getRepository('collections').destroy({ await db.getRepository('collections').destroy({
@ -154,18 +141,21 @@ describe('server hooks', () => {
expect(hookFn).toHaveBeenCalled(); expect(hookFn).toHaveBeenCalled();
}); });
it('should call server hooks afterUiSchemaCreated', async () => { it('should call server hooks onUiSchemaCreate', async () => {
const menuSchema = { const menuSchema = {
'x-uid': 'menu', 'x-uid': 'menu',
'x-server-hooks': { 'x-server-hooks': [
afterCreateSelf: ['afterCreateMenu'], {
}, type: 'onSelfCreate',
method: 'afterCreateMenu',
},
],
}; };
const serverHooks = uiSchemaPlugin.serverHooks; const serverHooks = uiSchemaPlugin.serverHooks;
const hookFn = jest.fn(); const hookFn = jest.fn();
serverHooks.register('afterCreateSelf', 'afterCreateMenu', hookFn); serverHooks.register('onSelfCreate', 'afterCreateMenu', hookFn);
await uiSchemaRepository.create({ await uiSchemaRepository.create({
values: { values: {
@ -175,4 +165,71 @@ describe('server hooks', () => {
expect(hookFn).toHaveBeenCalled(); expect(hookFn).toHaveBeenCalled();
}); });
it('should rollback after throw error', async () => {
const testSchema = {
'x-uid': 'test',
'x-collection-field': 'posts.title',
'x-server-hooks': [
{
type: 'onCollectionFieldDestroy',
collection: 'posts',
fields: ['title'],
method: 'preventDestroy',
},
],
};
await uiSchemaRepository.create({
values: {
schema: testSchema,
},
});
const PostModel = await db.getRepository('collections').create({
values: {
name: 'posts',
},
});
const fieldModel = await db.getRepository('fields').create({
values: {
name: 'title',
type: 'string',
collectionName: 'posts',
},
});
// @ts-ignore
await PostModel.migrate();
const serverHooks = uiSchemaPlugin.serverHooks;
const jestFn = jest.fn();
serverHooks.register('onCollectionFieldDestroy', 'preventDestroy', async ({ options }) => {
await options.transaction.rollback();
jestFn();
throw new Error('cant delete field');
});
try {
// destroy a field
await db.getRepository('fields').destroy({
filter: {
name: 'title',
},
individualHooks: true,
});
} catch (e) {}
expect(jestFn).toHaveBeenCalled();
expect(
await db.getRepository('fields').findOne({
filter: {
name: 'title',
},
}),
).toBeDefined();
});
}); });

View File

@ -1,13 +0,0 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'uiSchemaAttrs',
autoGenId: false,
timestamps: false,
fields: [
{
type: 'string',
name: 'collectionPath',
},
],
} as CollectionOptions;

View File

@ -0,0 +1,24 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'uiSchemaServerHooks',
model: 'ServerHookModel',
// autoGenId: false,
timestamps: false,
fields: [
{ type: 'belongsTo', name: 'uiSchema', target: 'ui_schemas', foreignKey: 'uid' },
{ type: 'string', name: 'type' },
{
type: 'string',
name: 'collection',
},
{
type: 'array',
name: 'fields',
},
{
type: 'string',
name: 'method',
},
],
} as CollectionOptions;

View File

@ -26,9 +26,9 @@ export default {
name: 'name', name: 'name',
}, },
{ {
type: 'hasOne', type: 'hasMany',
name: 'attrs', name: 'serverHooks',
target: 'uiSchemaAttrs', target: 'uiSchemaServerHooks',
foreignKey: 'uid', foreignKey: 'uid',
}, },
{ {

View File

@ -3,8 +3,10 @@ import { HookType } from './server-hooks';
class UiSchemaModel extends MagicAttributeModel { class UiSchemaModel extends MagicAttributeModel {
getListenServerHooks(type: HookType) { getListenServerHooks(type: HookType) {
const hooks = this.get('x-server-hooks') || {}; const hooks = this.get('x-server-hooks') || [];
return hooks[type] || []; return hooks.filter((hook) => {
hook.type = '';
});
} }
} }

View File

@ -386,33 +386,22 @@ export default class UiSchemaRepository extends Repository {
} }
private async insertSchemaRecord(name, uid, schema, transaction) { private async insertSchemaRecord(name, uid, schema, transaction) {
const serverHooks = schema['x-server-hooks'] || [];
const node = await this.create({ const node = await this.create({
values: { values: {
name, name,
uid, uid,
schema, schema,
serverHooks,
}, },
transaction, transaction,
hooks: false, hooks: false,
}); });
await this.insertSchemaAttrs(node, transaction);
return node; return node;
} }
private async insertSchemaAttrs(schema, transaction) {
const collectionPath = schema.get('x-collection') || schema.get('x-collection-field');
if (collectionPath) {
await this.database.getRepository('uiSchemaAttrs').create({
values: {
uid: schema.get('uid'),
collectionPath: collectionPath,
},
});
}
}
async insertSingleNode(schema: SchemaNode, transaction: Transaction) { async insertSingleNode(schema: SchemaNode, transaction: Transaction) {
const db = this.database; const db = this.database;
const treeCollection = db.getCollection('ui_schema_tree_path'); const treeCollection = db.getCollection('ui_schema_tree_path');

View File

@ -1,7 +1,12 @@
import { Database } from '@nocobase/database'; import { Database } from '@nocobase/database';
import { UiSchemaModel } from '../model'; import { ServerHookModel } from './model';
export type HookType = 'afterDestroyField' | 'afterDestroyCollection' | 'afterCreateSelf'; export type HookType =
| 'onSchemaDestroy'
| 'onCollectionDestroy'
| 'onCollectionFieldDestroy'
| 'onAnyCollectionFieldDestroy'
| 'onSelfCreate';
export class ServerHooks { export class ServerHooks {
hooks = new Map<HookType, Map<string, any>>(); hooks = new Map<HookType, Map<string, any>>();
@ -12,80 +17,82 @@ export class ServerHooks {
listen() { listen() {
this.db.on('fields.afterDestroy', async (model, options) => { this.db.on('fields.afterDestroy', async (model, options) => {
await this.afterFieldDestroy(model, options); await this.onCollectionFieldDestroy(model, options);
}); });
this.db.on('collections.afterDestroy', async (model, options) => { this.db.on('collections.afterDestroy', async (model, options) => {
await this.afterCollectionDestroy(model, options); await this.onCollectionDestroy(model, options);
}); });
this.db.on('ui_schemas.afterCreate', async (model, options) => { this.db.on('ui_schemas.afterCreateWithAssociations', async (model, options) => {
await this.afterUiSchemaCreated(model, options); await this.onUiSchemaCreate(model, options);
}); });
} }
protected async afterUiSchemaCreated(uiSchemaModel, options) { protected async onCollectionDestroy(collectionModel, options) {
const { transaction } = options; const { transaction } = options;
const listenHooksName = uiSchemaModel.getListenServerHooks('afterCreateSelf');
for (const listenHookName of listenHooksName) { await this.findHooksAndCall(
const hookFunc = this.hooks.get('afterCreateSelf')?.get(listenHookName); {
type: 'onCollectionDestroy',
await hookFunc({ collection: collectionModel.get('name'),
schemaInstance: uiSchemaModel, },
{
collectionModel,
options, options,
});
}
}
protected async afterCollectionDestroy(collectionModel, options) {
const { transaction } = options;
const collectionPath = collectionModel.get('name');
const listenSchemas = (await this.db.getRepository('ui_schemas').find({
filter: {
'attrs.collectionPath': collectionPath,
}, },
transaction, transaction,
})) as UiSchemaModel[]; );
for (const listenSchema of listenSchemas) {
const listenHooksName = listenSchema.getListenServerHooks('afterDestroyCollection');
for (const listenHookName of listenHooksName) {
const hookFunc = this.hooks.get('afterDestroyCollection')?.get(listenHookName);
await hookFunc({
collectionInstance: collectionModel,
schemaInstance: listenSchema,
options,
});
}
}
} }
protected async afterFieldDestroy(fieldModel, options) { protected async onCollectionFieldDestroy(fieldModel, options) {
const { transaction } = options; const { transaction } = options;
const collectionName = fieldModel.get('collectionName');
const fieldName = fieldModel.get('name');
const collectionPath = `${fieldModel.get('collectionName')}.${fieldModel.get('name')}`; await this.findHooksAndCall(
{
const listenSchemas = (await this.db.getRepository('ui_schemas').find({ type: 'onCollectionFieldDestroy',
filter: { collection: collectionName,
'attrs.collectionPath': collectionPath, 'fields.$anyOf': [fieldName],
},
{
fieldInstance: fieldModel,
options,
}, },
transaction, transaction,
})) as UiSchemaModel[]; );
}
for (const listenSchema of listenSchemas) { protected async onUiSchemaCreate(uiSchemaModel, options) {
const listenHooksName = listenSchema.getListenServerHooks('afterDestroyField'); const { transaction } = options;
for (const listenHookName of listenHooksName) {
const hookFunc = this.hooks.get('afterDestroyField')?.get(listenHookName);
await hookFunc({ await this.findHooksAndCall(
fieldInstance: fieldModel, {
schemaInstance: listenSchema, type: 'onSelfCreate',
options, uid: uiSchemaModel.schema['x-uid'],
}); },
{
uiSchemaModel,
options,
},
transaction,
);
}
protected async findHooksAndCall(hooksFilter, hooksArgs, transaction) {
const hooks = (await this.db.getRepository('uiSchemaServerHooks').find({
filter: hooksFilter,
appends: ['uiSchema'],
transaction,
})) as ServerHookModel[];
for (const hookRecord of hooks) {
const hoodMethodName = hookRecord.get('method') as string;
const hookFunc = this.hooks.get(hookRecord.get('type') as HookType)?.get(hoodMethodName);
if (hookFunc) {
await hookFunc({ ...hooksArgs, schemaInstance: (<any>hookRecord).uiSchema });
} }
} }
} }

View File

@ -0,0 +1,3 @@
import { Model } from '@nocobase/database';
export class ServerHookModel extends Model {}

View File

@ -5,6 +5,7 @@ import { uiSchemaActions } from './actions/ui-schema-action';
import UiSchemaRepository from './repository'; import UiSchemaRepository from './repository';
import { ServerHooks } from './server-hooks'; import { ServerHooks } from './server-hooks';
import { UiSchemaModel } from './model'; import { UiSchemaModel } from './model';
import { ServerHookModel } from './server-hooks/model';
export default class PluginUiSchema extends Plugin { export default class PluginUiSchema extends Plugin {
serverHooks: ServerHooks; serverHooks: ServerHooks;
@ -20,7 +21,7 @@ export default class PluginUiSchema extends Plugin {
this.serverHooks = new ServerHooks(db); this.serverHooks = new ServerHooks(db);
this.app.db.registerModels({ MagicAttributeModel, UiSchemaModel }); this.app.db.registerModels({ MagicAttributeModel, UiSchemaModel, ServerHookModel });
this.registerRepository(); this.registerRepository();