From e8113f3809e702ed14d82ec1a02bbe9cdea87646 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 8 Sep 2021 14:36:18 +0800 Subject: [PATCH] feat: update action logs plugin --- packages/database/src/database.ts | 4 +- .../src/__tests__/hook.test.ts | 99 +++++++++---------- .../plugin-action-logs/src/__tests__/index.ts | 27 +---- .../src/collections/action_changes.ts | 1 + .../src/collections/action_logs.ts | 2 + .../src/hooks/after-create.ts | 87 ++++++++-------- .../src/hooks/after-destroy.ts | 77 +++++++-------- .../src/hooks/after-update.ts | 82 ++++++++------- .../plugin-action-logs/src/hooks/index.ts | 13 +-- packages/plugin-action-logs/src/server.ts | 56 ++--------- 10 files changed, 189 insertions(+), 259 deletions(-) diff --git a/packages/database/src/database.ts b/packages/database/src/database.ts index a5a05af7b..9dc01f391 100644 --- a/packages/database/src/database.ts +++ b/packages/database/src/database.ts @@ -168,7 +168,7 @@ type HookType = const hookType = this._getHookType(event); if (hookType) { const state = this.hookTypes.get(hookType); - console.log('sequelize.addHook', hookType) + console.log('sequelize.addHook', event, hookType) this.sequelize.addHook(hookType, async (...args: any[]) => { let modelName: string; switch (state) { @@ -182,7 +182,7 @@ type HookType = modelName = args?.[0]?.model?.name; break; } - console.log({ modelName, args }); + // console.log({ modelName, args }); if (modelName) { await this.emitAsync(`${modelName}.${hookType}`, ...args); } diff --git a/packages/plugin-action-logs/src/__tests__/hook.test.ts b/packages/plugin-action-logs/src/__tests__/hook.test.ts index d233087a0..06a8121f7 100644 --- a/packages/plugin-action-logs/src/__tests__/hook.test.ts +++ b/packages/plugin-action-logs/src/__tests__/hook.test.ts @@ -1,69 +1,66 @@ +import Database from '@nocobase/database'; +import Application from '@nocobase/server'; import { getApp, getAPI, getAgent } from '.'; describe('hook', () => { - let app; - let anonymousAPI; - let userAPI; - let db; - let user; + let app: Application; + let db: Database; + let api; beforeEach(async () => { app = await getApp(); db = app.database; - - anonymousAPI = getAPI(getAgent(app)); - + db.table({ + name: 'posts', + logging: true, + fields: [ + { + type: 'string', + name: 'title', + }, + { + type: 'string', + name: 'status', + defaultValue: 'draft', + }, + ] + }); + await db.sync(); const User = db.getModel('users'); - user = await User.create({ nickname: 'a', token: 'token1' }); - + const user = await User.create({ nickname: 'a', token: 'token1' }); + console.log('beforeEach', user); const userAgent = getAgent(app); userAgent.set('Authorization', `Bearer ${user.token}`); - userAPI = getAPI(userAgent); + api = getAPI(userAgent); }); - afterEach(() => db.close()); + afterEach(async () => { + await db.close(); + }); - describe('common', () => { - it('create log', async () => { - await userAPI.resource('posts').create({ - values: { title: 't1' } - }); + it('database', async () => { + const Post = db.getModel('posts'); + const post = await Post.create({ title: 't1' }); + await post.update({title: 't2'}); + await post.destroy(); + const ActionLog = db.getModel('action_logs'); + const count = await ActionLog.count(); + expect(count).toBe(3); + }); - const Post = db.getModel('posts'); - const p1 = await Post.findByPk(1); - - const logs = await p1.getAction_logs(); - - expect(logs.length).toBe(1); - expect(logs[0].get()).toMatchObject({ - type: 'create', - index: p1.id, - user_id: user.id, - collection_name: 'posts' - }); + it('resource', async () => { + const response = await api.resource('posts').create({ + values: { title: 't1' }, }); - - it('logs should be scoped (no other model logs)', async () => { - await userAPI.resource('posts').create({ - values: { title: 't1' } - }); - await userAPI.resource('posts').update({ - resourceKey: '1', - values: { title: 't11' } - }); - await userAPI.resource('posts').create({ - values: { title: 't2' } - }); - await userAPI.resource('comments').create({ - values: { content: 'c1' } - }); - - const Post = db.getModel('posts'); - const p1 = await Post.findByPk(1); - - const logs = await p1.getAction_logs(); - expect(logs.length).toBe(2); - expect(logs.map(item => item.collection_name)).toEqual(['posts', 'posts']); + await api.resource('posts').update({ + resourceKey: response.body.data.id, + values: { title: 't2' }, }); + await api.resource('posts').destroy({ + resourceKey: response.body.data.id, + }); + const ActionLog = db.getModel('action_logs'); + const count = await ActionLog.count(); + expect(count).toBe(3); }); }); diff --git a/packages/plugin-action-logs/src/__tests__/index.ts b/packages/plugin-action-logs/src/__tests__/index.ts index 45ff12800..52699b0e3 100644 --- a/packages/plugin-action-logs/src/__tests__/index.ts +++ b/packages/plugin-action-logs/src/__tests__/index.ts @@ -45,43 +45,20 @@ export function getDatabase() { return new Database(config); }; -export async function getApp() { +export async function getApp(): Promise { const app = new Application({ database: config, resourcer: { prefix: '/api', }, }); - app.resourcer.use(middlewares.associated); - app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common }); app.registerPlugin({ collections: path.resolve(__dirname, '../../../plugin-collections'), users: path.resolve(__dirname, '../../../plugin-users'), logs: plugin }); await app.loadPlugins(); - const testTables = app.database.import({ - directory: path.resolve(__dirname, './tables') - }); - try { - await app.database.sync(); - } catch (err) { - console.error(err); - } - - for (const table of testTables.values()) { - // TODO(bug): 由于每个用例结束后不会清理用于测试的数据表,导致再次创建和更新 - // 创建和更新里面仍会再次创建 fields,导致创建相关的数据重复,数据库报错。 - await app.database.getModel('collections').import(table.getOptions(), { update: true, migrate: false }); - } - - app.context.db = app.database; - app.use(bodyParser()); - app.use(middleware({ - prefix: '/api', - resourcer: app.resourcer, - database: app.database, - })); + await app.database.sync(); return app; } diff --git a/packages/plugin-action-logs/src/collections/action_changes.ts b/packages/plugin-action-logs/src/collections/action_changes.ts index 7adb20683..c8967000d 100644 --- a/packages/plugin-action-logs/src/collections/action_changes.ts +++ b/packages/plugin-action-logs/src/collections/action_changes.ts @@ -12,6 +12,7 @@ export default { type: 'belongsTo', name: 'log', target: 'action_logs', + foreignKey: 'log_id', }, { type: 'jsonb', diff --git a/packages/plugin-action-logs/src/collections/action_logs.ts b/packages/plugin-action-logs/src/collections/action_logs.ts index 926ee44b6..aecdfe480 100644 --- a/packages/plugin-action-logs/src/collections/action_logs.ts +++ b/packages/plugin-action-logs/src/collections/action_logs.ts @@ -22,6 +22,7 @@ export default { name: 'collection', target: 'collections', targetKey: 'name', + constraints: false, }, { type: 'string', @@ -35,6 +36,7 @@ export default { type: 'hasMany', name: 'changes', target: 'action_changes', + foreignKey: 'log_id', } ], } as TableOptions; diff --git a/packages/plugin-action-logs/src/hooks/after-create.ts b/packages/plugin-action-logs/src/hooks/after-create.ts index 36aea421c..0279ae659 100644 --- a/packages/plugin-action-logs/src/hooks/after-create.ts +++ b/packages/plugin-action-logs/src/hooks/after-create.ts @@ -1,53 +1,52 @@ -import { Field } from '@nocobase/database'; +import Database, { Field, Model } from '@nocobase/database'; import { LOG_TYPE_CREATE } from '../constants'; -export default async function (model, options) { - if (!options.context) { +export async function afterCreate(model, options) { + const db = model.database as Database; + const table = db.getTable(model.constructor.name); + if (!table.getOptions('logging')) { return; } - const { database: db } = model; - const { context: { state }, transaction = await db.sequelize.transaction() } = options; + const { transaction = await db.sequelize.transaction() } = options; const ActionLog = db.getModel('action_logs'); - // 创建操作记录 - const log = await ActionLog.create({ - type: LOG_TYPE_CREATE, - collection_name: model.constructor.name, - index: model.get(model.constructor.primaryKeyAttribute), - created_at: model.get('created_at') - }, { - transaction - }); - if (state.currentUser) { - // TODO(bug): state.currentUser 不是 belongsTo field 的 target 实例 - // Sequelize 会另外创建一个 Model 的继承类,直传 instance 因为无法匹配类会当做 id 造成类型错误 - // await log.setUser(state.currentUser, { transaction }); - await log.updateAssociations({ user: state.currentUser.id }, { - transaction - }); - } - - const fields = db.getTable(model.constructor.name).getFields(); - const fieldsList = Array.from(fields.values()); - const changes = []; - const changed = model.changed(); - if (changed) { - changed.forEach((key: string) => { - const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key); - if (field && !field.options.hidden && field.options.type !== 'formula') { - changes.push({ - field: field.options, - after: model.get(key) - }); - } - }); - await log.updateAssociations({ - changes + const currentUserId = options?.context?.state?.currentUser?.id; + try { + const log = await ActionLog.create({ + type: LOG_TYPE_CREATE, + collection_name: model.constructor.name, + index: model.get(model.constructor.primaryKeyAttribute), + created_at: model.get('created_at'), + user_id: currentUserId, }, { - transaction + transaction, + hooks: false, }); - } - - if (!options.transaction) { - await transaction.commit(); + const fields = db.getTable(model.constructor.name).getFields(); + const fieldsList = Array.from(fields.values()); + const changes = []; + const changed = model.changed(); + if (changed) { + changed.forEach((key: string) => { + const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key); + if (field && !field.options.hidden && field.options.type !== 'formula') { + changes.push({ + field: field.options, + after: model.get(key) + }); + } + }); + await log.updateAssociations({ + changes + }, { + transaction + }); + } + if (!options.transaction) { + await transaction.commit(); + } + } catch (error) { + if (!options.transaction) { + await transaction.rollback(); + } } } diff --git a/packages/plugin-action-logs/src/hooks/after-destroy.ts b/packages/plugin-action-logs/src/hooks/after-destroy.ts index 28acac0d0..0ca3755ad 100644 --- a/packages/plugin-action-logs/src/hooks/after-destroy.ts +++ b/packages/plugin-action-logs/src/hooks/after-destroy.ts @@ -1,49 +1,48 @@ -import { Field } from '@nocobase/database'; +import Database, { Field } from '@nocobase/database'; import { LOG_TYPE_DESTROY } from '../constants'; -export default async function (model, options) { - if (!options.context) { +export async function afterDestroy(model, options) { + const db = model.database as Database; + const table = db.getTable(model.constructor.name); + if (!table.getOptions('logging')) { return; } - const { database: db } = model; - const { context: { state }, transaction = await db.sequelize.transaction() } = options; + const { transaction = await db.sequelize.transaction() } = options; const ActionLog = db.getModel('action_logs'); - // 创建操作记录 - const log = await ActionLog.create({ - // user_id: state.currentUser ? state.currentUser.id : null, - type: LOG_TYPE_DESTROY, - collection_name: model.constructor.name, - index: model.get(model.constructor.primaryKeyAttribute), - // created_at: model.get('created_at') - }, { - transaction - }); - if (state.currentUser) { - await log.updateAssociations({ user: state.currentUser.id }, { + const currentUserId = options?.context?.state?.currentUser?.id; + try { + const log = await ActionLog.create({ + type: LOG_TYPE_DESTROY, + collection_name: model.constructor.name, + index: model.get(model.constructor.primaryKeyAttribute), + user_id: currentUserId, + }, { + transaction, + hooks: false, + }); + const fields = db.getTable(model.constructor.name).getFields(); + const fieldsList = Array.from(fields.values()); + const changes = []; + Object.keys(model.get()).forEach((key: string) => { + const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key); + if (field) { + changes.push({ + field: field.options, + before: model.get(key) + }); + } + }); + await log.updateAssociations({ + changes + }, { transaction }); - } - - const fields = db.getTable(model.constructor.name).getFields(); - const fieldsList = Array.from(fields.values()); - const changes = []; - Object.keys(model.get()).forEach((key: string) => { - const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key); - if (field) { - changes.push({ - field: field.options, - before: model.get(key) - }); + if (!options.transaction) { + await transaction.commit(); + } + } catch (error) { + if (!options.transaction) { + await transaction.rollback(); } - }); - - await log.updateAssociations({ - changes - }, { - transaction - }); - - if (!options.transaction) { - await transaction.commit(); } } diff --git a/packages/plugin-action-logs/src/hooks/after-update.ts b/packages/plugin-action-logs/src/hooks/after-update.ts index d1e7e1613..999d5c49b 100644 --- a/packages/plugin-action-logs/src/hooks/after-update.ts +++ b/packages/plugin-action-logs/src/hooks/after-update.ts @@ -1,51 +1,57 @@ -import { Field } from '@nocobase/database'; +import Database, { Field } from '@nocobase/database'; import { LOG_TYPE_UPDATE } from '../constants'; -export default async function (model, options) { - if (!options.context) { +export async function afterUpdate(model, options) { + const db = model.database as Database; + const table = db.getTable(model.constructor.name); + if (!table.getOptions('logging')) { return; } - const { database: db } = model; - const { context: { state }, transaction = await db.sequelize.transaction() } = options; + const changed = model.changed(); + if (!changed) { + return; + } + const { transaction = await db.sequelize.transaction() } = options; const ActionLog = db.getModel('action_logs'); - + const currentUserId = options?.context?.state?.currentUser?.id; const fields = db.getTable(model.constructor.name).getFields(); const fieldsList = Array.from(fields.values()); const changes = []; - const changed = model.changed(); - if (changed) { - changed.forEach((key: string) => { - const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key); - if (field && !field.options.hidden && field.options.type !== 'formula') { - changes.push({ - field: field.options, - after: model.get(key), - before: model.previous(key) - }); - } - }); - - if (changes.length) { - // 创建操作记录 - const log = await ActionLog.create({ - type: LOG_TYPE_UPDATE, - collection_name: model.constructor.name, - index: model.get(model.constructor.primaryKeyAttribute), - created_at: model.get('updated_at') - }, { - transaction - }); - - await log.updateAssociations({ - ...(state.currentUser ? { user: state.currentUser.id } : {}), - changes - }, { - transaction + changed.forEach((key: string) => { + const field = fields.get(key) || fieldsList.find((item: Field) => item.options.field === key); + if (field && !field.options.hidden && field.options.type !== 'formula') { + changes.push({ + field: field.options, + after: model.get(key), + before: model.previous(key) }); } + }); + if (!changes.length) { + return; } - - if (!options.transaction) { - await transaction.commit(); + try { + const log = await ActionLog.create({ + type: LOG_TYPE_UPDATE, + collection_name: model.constructor.name, + index: model.get(model.constructor.primaryKeyAttribute), + created_at: model.get('updated_at'), + user_id: currentUserId, + }, { + transaction, + hooks: false, + }); + await log.updateAssociations({ + changes + }, { + transaction + }); + if (!options.transaction) { + await transaction.commit(); + } + } catch (error) { + if (!options.transaction) { + await transaction.rollback(); + } } } diff --git a/packages/plugin-action-logs/src/hooks/index.ts b/packages/plugin-action-logs/src/hooks/index.ts index 799f79683..3c84cf55e 100644 --- a/packages/plugin-action-logs/src/hooks/index.ts +++ b/packages/plugin-action-logs/src/hooks/index.ts @@ -1,10 +1,3 @@ -import afterCreate from './after-create'; -import afterUpdate from './after-update'; -import afterDestroy from './after-destroy'; - -export function addAll(Model) { - Model.addHook('afterCreate', afterCreate); - // Model.addHook('afterBulkCreate', hooks.afterBulkCreate); - Model.addHook('afterUpdate', afterUpdate); - Model.addHook('afterDestroy', afterDestroy); -} +export * from './after-create'; +export * from './after-update'; +export * from './after-destroy'; diff --git a/packages/plugin-action-logs/src/server.ts b/packages/plugin-action-logs/src/server.ts index 1dc8346d4..34b09c052 100644 --- a/packages/plugin-action-logs/src/server.ts +++ b/packages/plugin-action-logs/src/server.ts @@ -1,57 +1,13 @@ import path from 'path'; -import { Op } from 'sequelize'; +import Application from '@nocobase/server'; +import { afterCreate, afterUpdate, afterDestroy } from './hooks'; -import { addAll } from './hooks'; - -export default async function() { +export default async function (this: Application) { const { database } = this; - database.import({ directory: path.resolve(__dirname, 'collections'), }); - - // 为所有的表都加上日志的 hooks - database.addHook('afterTableInit', (table) => { - if (!table.options.logging) { - return; - } - addAll(database.getModel(table.options.name)); - }); - - // const Collection = database.getModel('collections'); - // Collection.addHook('afterCreate', async (model, options) => { - // if (!model.get('logging')) { - // return; - // } - - // const { transaction = await model.sequelize.transaction() } = options; - - // const exists = await model.countFields({ - // where: { - // dataType: { [Op.iLike]: 'hasMany' }, - // name: 'action_logs' - // }, - // transaction - // }); - - // if (!exists) { - // await model.createSystemField({ - // interface: 'linkTo', - // dataType: 'hasMany', - // name: 'action_logs', - // target: 'action_logs', - // title: '数据动态', - // foreignKey: 'index', - // state: 0, - // scope: { - // collection_name: model.get('name') - // }, - // constraints: false - // }, { transaction }); - // } - - // if (!options.transaction) { - // await transaction.commit(); - // } - // }); + database.on('afterCreate', afterCreate); + database.on('afterUpdate', afterUpdate); + database.on('afterDestroy', afterDestroy); }