feat: upgrade plugin-action-logs
This commit is contained in:
parent
864223d26a
commit
c56cd8674d
26
.env.example
Normal file
26
.env.example
Normal 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=
|
@ -73,6 +73,7 @@ export class Collection<
|
||||
const m = this.context.database.sequelize.model(name);
|
||||
if ((m as any).isThrough) {
|
||||
this.model = m;
|
||||
Object.defineProperty(this.model, 'database', { value: this.context.database });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -83,6 +84,7 @@ export class Collection<
|
||||
}
|
||||
this.model = class extends M {};
|
||||
this.model.init(null, this.sequelizeModelOptions());
|
||||
Object.defineProperty(this.model, 'database', { value: this.context.database });
|
||||
}
|
||||
|
||||
setRepository(repository?: RepositoryType | string) {
|
||||
|
@ -6,3 +6,4 @@ export * from './relation-repository/belongs-to-many-repository';
|
||||
export * from './relation-repository/belongs-to-repository';
|
||||
export { Model } from 'sequelize';
|
||||
export * from './fields';
|
||||
export * from './update-associations';
|
@ -2,7 +2,9 @@ import {
|
||||
Association,
|
||||
BulkCreateOptions,
|
||||
CreateOptions as SequelizeCreateOptions,
|
||||
UpdateOptions as SequelizeUpdateOptions,
|
||||
FindAndCountOptions as SequelizeAndCountOptions,
|
||||
DestroyOptions as SequelizeDestroyOptions,
|
||||
FindOptions as SequelizeFindOptions,
|
||||
Model,
|
||||
ModelCtor,
|
||||
@ -79,7 +81,7 @@ export interface CommonFindOptions {
|
||||
|
||||
interface FindOneOptions extends FindOptions, CommonFindOptions {}
|
||||
|
||||
export interface DestroyOptions extends TransactionAble {
|
||||
export interface DestroyOptions extends SequelizeDestroyOptions {
|
||||
filter?: Filter;
|
||||
filterByPk?: PrimaryKey | PrimaryKey[];
|
||||
truncate?: boolean;
|
||||
@ -98,20 +100,22 @@ interface FindAndCountOptions extends Omit<SequelizeAndCountOptions, 'where' | '
|
||||
sort?: Sort;
|
||||
}
|
||||
|
||||
export interface CreateOptions extends TransactionAble {
|
||||
export interface CreateOptions extends SequelizeCreateOptions {
|
||||
values?: Values;
|
||||
whitelist?: WhiteList;
|
||||
blacklist?: BlackList;
|
||||
updateAssociationValues?: AssociationKeysToBeUpdate;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
export interface UpdateOptions extends TransactionAble {
|
||||
export interface UpdateOptions extends SequelizeUpdateOptions {
|
||||
values: Values;
|
||||
filter?: Filter;
|
||||
filterByPk?: PrimaryKey;
|
||||
whitelist?: WhiteList;
|
||||
blacklist?: BlackList;
|
||||
updateAssociationValues?: AssociationKeysToBeUpdate;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
interface RelatedQueryOptions {
|
||||
@ -287,13 +291,17 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
const guard = UpdateGuard.fromOptions(this.model, options);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAssociations(instance, values, {
|
||||
...options,
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -316,7 +324,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
});
|
||||
|
||||
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;
|
||||
@ -344,6 +352,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
|
||||
for (const instance of instances) {
|
||||
await updateModelByValues(instance, values, {
|
||||
...options,
|
||||
sanitized: true,
|
||||
transaction,
|
||||
});
|
||||
|
7
packages/plugin-action-logs/.npmignore
Normal file
7
packages/plugin-action-logs/.npmignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
15
packages/plugin-action-logs/package.json
Normal file
15
packages/plugin-action-logs/package.json
Normal 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"
|
||||
}
|
105
packages/plugin-action-logs/src/__tests__/hook.test.ts
Normal file
105
packages/plugin-action-logs/src/__tests__/hook.test.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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;
|
46
packages/plugin-action-logs/src/collections/action_logs.ts
Normal file
46
packages/plugin-action-logs/src/collections/action_logs.ts
Normal 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;
|
3
packages/plugin-action-logs/src/constants.ts
Normal file
3
packages/plugin-action-logs/src/constants.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const LOG_TYPE_CREATE = 'create';
|
||||
export const LOG_TYPE_UPDATE = 'update';
|
||||
export const LOG_TYPE_DESTROY = 'destroy';
|
51
packages/plugin-action-logs/src/hooks/after-create.ts
Normal file
51
packages/plugin-action-logs/src/hooks/after-create.ts
Normal 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();
|
||||
// }
|
||||
}
|
||||
}
|
47
packages/plugin-action-logs/src/hooks/after-destroy.ts
Normal file
47
packages/plugin-action-logs/src/hooks/after-destroy.ts
Normal 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();
|
||||
// }
|
||||
}
|
||||
}
|
56
packages/plugin-action-logs/src/hooks/after-update.ts
Normal file
56
packages/plugin-action-logs/src/hooks/after-update.ts
Normal 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();
|
||||
// }
|
||||
}
|
||||
}
|
3
packages/plugin-action-logs/src/hooks/index.ts
Normal file
3
packages/plugin-action-logs/src/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './after-create';
|
||||
export * from './after-update';
|
||||
export * from './after-destroy';
|
1
packages/plugin-action-logs/src/index.ts
Normal file
1
packages/plugin-action-logs/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './server';
|
16
packages/plugin-action-logs/src/server.ts
Normal file
16
packages/plugin-action-logs/src/server.ts
Normal 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;
|
9
packages/plugin-action-logs/tsconfig.build.json
Normal file
9
packages/plugin-action-logs/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
|
||||
}
|
5
packages/plugin-action-logs/tsconfig.json
Normal file
5
packages/plugin-action-logs/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./esm/*", "./lib/*"]
|
||||
}
|
@ -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"
|
||||
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:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
|
||||
|
Loading…
Reference in New Issue
Block a user