feat: update action logs plugin
This commit is contained in:
parent
c182c29161
commit
e8113f3809
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ export default {
|
||||
type: 'belongsTo',
|
||||
name: 'log',
|
||||
target: 'action_logs',
|
||||
foreignKey: 'log_id',
|
||||
},
|
||||
{
|
||||
type: 'jsonb',
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user