feat: upgrade plugin-action-logs

This commit is contained in:
chenos 2021-12-07 15:21:16 +08:00
parent 864223d26a
commit c56cd8674d
19 changed files with 437 additions and 12 deletions

26
.env.example Normal file
View File

@ -0,0 +1,26 @@
DB_DIALECT=sqlite
DB_STORAGE=db.sqlite
# STORAGE (Initialization only)
# local or ali-oss
DEFAULT_STORAGE_TYPE=local
STORAGE_TYPE=local
# LOCAL STORAGE
LOCAL_STORAGE_USE_STATIC_SERVER=true
LOCAL_STORAGE_BASE_URL=
# ALI OSS STORAGE
ALI_OSS_STORAGE_BASE_URL=
ALI_OSS_REGION=oss-cn-beijing
ALI_OSS_ACCESS_KEY_ID=
ALI_OSS_ACCESS_KEY_SECRET=
ALI_OSS_BUCKET=
# AWS
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_REGION=
AWS_S3_BUCKET=
AWS_S3_STORAGE_BASE_URL=

View File

@ -73,6 +73,7 @@ export class Collection<
const m = this.context.database.sequelize.model(name); const m = this.context.database.sequelize.model(name);
if ((m as any).isThrough) { if ((m as any).isThrough) {
this.model = m; this.model = m;
Object.defineProperty(this.model, 'database', { value: this.context.database });
return; return;
} }
} }
@ -83,6 +84,7 @@ export class Collection<
} }
this.model = class extends M {}; this.model = class extends M {};
this.model.init(null, this.sequelizeModelOptions()); this.model.init(null, this.sequelizeModelOptions());
Object.defineProperty(this.model, 'database', { value: this.context.database });
} }
setRepository(repository?: RepositoryType | string) { setRepository(repository?: RepositoryType | string) {

View File

@ -6,3 +6,4 @@ export * from './relation-repository/belongs-to-many-repository';
export * from './relation-repository/belongs-to-repository'; export * from './relation-repository/belongs-to-repository';
export { Model } from 'sequelize'; export { Model } from 'sequelize';
export * from './fields'; export * from './fields';
export * from './update-associations';

View File

@ -2,7 +2,9 @@ import {
Association, Association,
BulkCreateOptions, BulkCreateOptions,
CreateOptions as SequelizeCreateOptions, CreateOptions as SequelizeCreateOptions,
UpdateOptions as SequelizeUpdateOptions,
FindAndCountOptions as SequelizeAndCountOptions, FindAndCountOptions as SequelizeAndCountOptions,
DestroyOptions as SequelizeDestroyOptions,
FindOptions as SequelizeFindOptions, FindOptions as SequelizeFindOptions,
Model, Model,
ModelCtor, ModelCtor,
@ -79,7 +81,7 @@ export interface CommonFindOptions {
interface FindOneOptions extends FindOptions, CommonFindOptions {} interface FindOneOptions extends FindOptions, CommonFindOptions {}
export interface DestroyOptions extends TransactionAble { export interface DestroyOptions extends SequelizeDestroyOptions {
filter?: Filter; filter?: Filter;
filterByPk?: PrimaryKey | PrimaryKey[]; filterByPk?: PrimaryKey | PrimaryKey[];
truncate?: boolean; truncate?: boolean;
@ -98,20 +100,22 @@ interface FindAndCountOptions extends Omit<SequelizeAndCountOptions, 'where' | '
sort?: Sort; sort?: Sort;
} }
export interface CreateOptions extends TransactionAble { export interface CreateOptions extends SequelizeCreateOptions {
values?: Values; values?: Values;
whitelist?: WhiteList; whitelist?: WhiteList;
blacklist?: BlackList; blacklist?: BlackList;
updateAssociationValues?: AssociationKeysToBeUpdate; updateAssociationValues?: AssociationKeysToBeUpdate;
context?: any;
} }
export interface UpdateOptions extends TransactionAble { export interface UpdateOptions extends SequelizeUpdateOptions {
values: Values; values: Values;
filter?: Filter; filter?: Filter;
filterByPk?: PrimaryKey; filterByPk?: PrimaryKey;
whitelist?: WhiteList; whitelist?: WhiteList;
blacklist?: BlackList; blacklist?: BlackList;
updateAssociationValues?: AssociationKeysToBeUpdate; updateAssociationValues?: AssociationKeysToBeUpdate;
context?: any;
} }
interface RelatedQueryOptions { interface RelatedQueryOptions {
@ -287,13 +291,17 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
const guard = UpdateGuard.fromOptions(this.model, options); const guard = UpdateGuard.fromOptions(this.model, options);
const values = guard.sanitize(options.values || {}); const values = guard.sanitize(options.values || {});
const instance = await this.model.create<any>(values, { transaction }); const instance = await this.model.create<any>(values, {
...options,
transaction,
});
if (!instance) { if (!instance) {
return; return;
} }
await updateAssociations(instance, values, { await updateAssociations(instance, values, {
...options,
transaction, transaction,
}); });
@ -316,7 +324,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
}); });
for (let i = 0; i < instances.length; i++) { for (let i = 0; i < instances.length; i++) {
await updateAssociations(instances[i], records[i], { transaction }); await updateAssociations(instances[i], records[i], { ...options, transaction });
} }
return instances; return instances;
@ -344,6 +352,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
for (const instance of instances) { for (const instance of instances) {
await updateModelByValues(instance, values, { await updateModelByValues(instance, values, {
...options,
sanitized: true, sanitized: true,
transaction, transaction,
}); });

View File

@ -0,0 +1,7 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View File

@ -0,0 +1,15 @@
{
"name": "@nocobase/plugin-action-logs",
"version": "0.6.0-alpha.0",
"main": "lib/index.js",
"license": "MIT",
"scripts": {
"build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
"build:cjs": "tsc --project tsconfig.build.json",
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
},
"devDependencies": {
"@nocobase/test": "^0.6.0-alpha.0"
},
"gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4"
}

View File

@ -0,0 +1,105 @@
import Database from '@nocobase/database';
import { mockServer, MockServer } from '@nocobase/test';
import logPlugin from '../server';
describe('hook', () => {
let api: MockServer;
let db: Database;
beforeEach(async () => {
api = mockServer();
// api.plugin(require('@nocobase/plugin-users').default);
api.plugin(logPlugin);
await api.load();
db = api.db;
db.collection({
name: 'posts',
logging: true,
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'string',
name: 'status',
defaultValue: 'draft',
},
],
});
db.collection({
name: 'users',
logging: false,
fields: [
{ type: 'string', name: 'nickname' },
{ type: 'string', name: 'token' },
],
});
await db.sync();
});
afterEach(async () => {
await api.destroy();
});
it('model', async () => {
const Post = db.getCollection('posts').model;
const post = await Post.create({ title: 't1' });
await post.update({ title: 't2' });
await post.destroy();
const ActionLog = db.getCollection('action_logs').model;
const count = await ActionLog.count();
expect(count).toBe(3);
});
it('repository', async () => {
const Post = db.getCollection('posts');
const User = db.getCollection('users').model;
const user = await User.create({ nickname: 'a', token: 'token1' });
await Post.repository.create({
values: { title: 't1' },
context: {
state: {
currentUser: user,
},
},
});
const ActionLog = db.getCollection('action_logs');
const log = await ActionLog.repository.findOne({
appends: ['changes'],
});
expect(log.toJSON()).toMatchObject({
collectionName: 'posts',
type: 'create',
userId: 1,
changes: [
{
field: {
name: 'title',
type: 'string',
},
before: null,
after: 't1',
},
],
});
});
it.skip('resource', async () => {
const agent = api.agent();
agent.set('Authorization', `Bearer token1`);
const response = await agent.resource('posts').create({
values: { title: 't1' },
});
await agent.resource('posts').update({
resourceIndex: response.body.data.id,
values: { title: 't2' },
});
await agent.resource('posts').destroy({
resourceIndex: response.body.data.id,
});
const ActionLog = db.getCollection('action_logs').model;
const count = await ActionLog.count();
expect(count).toBe(3);
});
});

View File

@ -0,0 +1,30 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'action_changes',
title: '变动值',
createdBy: false,
updatedBy: false,
createdAt: false,
updatedAt: false,
fields: [
{
type: 'belongsTo',
name: 'log',
target: 'action_logs',
foreignKey: 'actionLogId',
},
{
type: 'json',
name: 'field',
},
{
type: 'json',
name: 'before',
},
{
type: 'json',
name: 'after',
},
],
} as CollectionOptions;

View File

@ -0,0 +1,46 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'action_logs',
title: '操作记录',
createdBy: false,
updatedBy: false,
updatedAt: false,
fields: [
{
type: 'date',
name: 'createdAt',
},
{
type: 'belongsTo',
name: 'user',
target: 'users',
},
{
type: 'string',
name: 'collectionName',
},
{
type: 'belongsTo',
name: 'collection',
target: 'collections',
targetKey: 'name',
sourceKey: 'collectionName',
constraints: false,
},
{
type: 'string',
name: 'type',
},
{
type: 'integer',
name: 'index',
},
{
type: 'hasMany',
name: 'changes',
target: 'action_changes',
foreignKey: 'actionLogId',
},
],
} as CollectionOptions;

View File

@ -0,0 +1,3 @@
export const LOG_TYPE_CREATE = 'create';
export const LOG_TYPE_UPDATE = 'update';
export const LOG_TYPE_DESTROY = 'destroy';

View File

@ -0,0 +1,51 @@
import Database, { updateAssociations } from '@nocobase/database';
import { LOG_TYPE_CREATE } from '../constants';
export async function afterCreate(model, options) {
const db = model.constructor.database as Database;
const collection = db.getCollection(model.constructor.name);
if (!collection.options.logging) {
return;
}
const transaction = options.transaction;
const ActionLog = db.getCollection('action_logs');
const currentUserId = options?.context?.state?.currentUser?.id;
try {
const log = await ActionLog.model.create(
{
type: LOG_TYPE_CREATE,
collectionName: model.constructor.name,
index: model.get(model.constructor.primaryKeyAttribute),
createdAt: model.get('createdAt'),
userId: currentUserId,
},
{
transaction,
hooks: false,
},
);
const changes = [];
const changed = model.changed();
if (changed) {
changed.forEach((key: string) => {
const field = collection.findField((field) => {
return field.name === key || field.options.field === key;
});
if (field && !field.options.hidden && field.options.type !== 'formula') {
changes.push({
field: field.options,
after: model.get(key),
});
}
});
await updateAssociations(log, { changes }, { transaction });
}
// if (!options.transaction) {
// await transaction.commit();
// }
} catch (error) {
// if (!options.transaction) {
// await transaction.rollback();
// }
}
}

View File

@ -0,0 +1,47 @@
import Database, { updateAssociations } from '@nocobase/database';
import { LOG_TYPE_DESTROY } from '../constants';
export async function afterDestroy(model, options) {
const db = model.constructor.database as Database;
const collection = db.getCollection(model.constructor.name);
if (!collection.options.logging) {
return;
}
const transaction = options.transaction;
const ActionLog = db.getCollection('action_logs');
const currentUserId = options?.context?.state?.currentUser?.id;
try {
const log = await ActionLog.model.create(
{
type: LOG_TYPE_DESTROY,
collectionName: model.constructor.name,
index: model.get(model.constructor.primaryKeyAttribute),
userId: currentUserId,
},
{
transaction,
hooks: false,
},
);
const changes = [];
Object.keys(model.get()).forEach((key: string) => {
const field = collection.findField((field) => {
return field.name === key || field.options.field === key;
});
if (field) {
changes.push({
field: field.options,
before: model.get(key),
});
}
});
await updateAssociations(log, { changes }, { transaction });
// if (!options.transaction) {
// await transaction.commit();
// }
} catch (error) {
// if (!options.transaction) {
// await transaction.rollback();
// }
}
}

View File

@ -0,0 +1,56 @@
import Database, { updateAssociations } from '@nocobase/database';
import { LOG_TYPE_UPDATE } from '../constants';
export async function afterUpdate(model, options) {
const db = model.constructor.database as Database;
const collection = db.getCollection(model.constructor.name);
if (!collection.options.logging) {
return;
}
const changed = model.changed();
if (!changed) {
return;
}
const transaction = options.transaction;
const ActionLog = db.getCollection('action_logs');
const currentUserId = options?.context?.state?.currentUser?.id;
const changes = [];
changed.forEach((key: string) => {
const field = collection.findField((field) => {
return field.name === key || field.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;
}
try {
const log = await ActionLog.model.create(
{
type: LOG_TYPE_UPDATE,
collectionName: model.constructor.name,
index: model.get(model.constructor.primaryKeyAttribute),
createdAt: model.get('updatedAt'),
userId: currentUserId,
},
{
transaction,
hooks: false,
},
);
await updateAssociations(log, { changes }, { transaction });
// if (!options.transaction) {
// await transaction.commit();
// }
} catch (error) {
// if (!options.transaction) {
// await transaction.rollback();
// }
}
}

View File

@ -0,0 +1,3 @@
export * from './after-create';
export * from './after-update';
export * from './after-destroy';

View File

@ -0,0 +1 @@
export { default } from './server';

View File

@ -0,0 +1,16 @@
import path from 'path';
import { IPlugin } from '@nocobase/server';
import { afterCreate, afterUpdate, afterDestroy } from './hooks';
export default {
name: 'action-logs',
async load() {
const database = this.app.db;
await database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.on('afterCreate', afterCreate);
database.on('afterUpdate', afterUpdate);
database.on('afterDestroy', afterDestroy);
},
} as IPlugin;

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./lib",
"declaration": true
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
}

View File

@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./esm/*", "./lib/*"]
}

View File

@ -4640,13 +4640,6 @@ crypto-random-string@^1.0.0:
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
crypto-random-string@^3.3.0:
version "3.3.1"
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.1.tgz#13cee94cac8001e4842501608ef779e0ed08f82d"
integrity sha512-5j88ECEn6h17UePrLi6pn1JcLtAiANa3KExyr9y9Z5vo2mv56Gh3I4Aja/B9P9uyMwyxNHAHWv+nE72f30T5Dg==
dependencies:
type-fest "^0.8.1"
css-blank-pseudo@^0.1.4: css-blank-pseudo@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"