feat: update action logs plugin

This commit is contained in:
chenos 2021-09-08 14:36:18 +08:00
parent c182c29161
commit e8113f3809
10 changed files with 189 additions and 259 deletions

View File

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

View File

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

View File

@ -45,43 +45,20 @@ export function getDatabase() {
return new Database(config);
};
export async function getApp() {
export async function getApp(): Promise<Application> {
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;
}

View File

@ -12,6 +12,7 @@ export default {
type: 'belongsTo',
name: 'log',
target: 'action_logs',
foreignKey: 'log_id',
},
{
type: 'jsonb',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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