From c56cd8674d13cff47b671a4aecdd451ec9e924e3 Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 7 Dec 2021 15:21:16 +0800 Subject: [PATCH] feat: upgrade plugin-action-logs --- .env.example | 26 +++++ packages/database/src/collection.ts | 2 + packages/database/src/index.ts | 1 + packages/database/src/repository.ts | 19 +++- packages/plugin-action-logs/.npmignore | 7 ++ packages/plugin-action-logs/package.json | 15 +++ .../src/__tests__/hook.test.ts | 105 ++++++++++++++++++ .../src/collections/action_changes.ts | 30 +++++ .../src/collections/action_logs.ts | 46 ++++++++ packages/plugin-action-logs/src/constants.ts | 3 + .../src/hooks/after-create.ts | 51 +++++++++ .../src/hooks/after-destroy.ts | 47 ++++++++ .../src/hooks/after-update.ts | 56 ++++++++++ .../plugin-action-logs/src/hooks/index.ts | 3 + packages/plugin-action-logs/src/index.ts | 1 + packages/plugin-action-logs/src/server.ts | 16 +++ .../plugin-action-logs/tsconfig.build.json | 9 ++ packages/plugin-action-logs/tsconfig.json | 5 + yarn.lock | 7 -- 19 files changed, 437 insertions(+), 12 deletions(-) create mode 100644 .env.example create mode 100644 packages/plugin-action-logs/.npmignore create mode 100644 packages/plugin-action-logs/package.json create mode 100644 packages/plugin-action-logs/src/__tests__/hook.test.ts create mode 100644 packages/plugin-action-logs/src/collections/action_changes.ts create mode 100644 packages/plugin-action-logs/src/collections/action_logs.ts create mode 100644 packages/plugin-action-logs/src/constants.ts create mode 100644 packages/plugin-action-logs/src/hooks/after-create.ts create mode 100644 packages/plugin-action-logs/src/hooks/after-destroy.ts create mode 100644 packages/plugin-action-logs/src/hooks/after-update.ts create mode 100644 packages/plugin-action-logs/src/hooks/index.ts create mode 100644 packages/plugin-action-logs/src/index.ts create mode 100644 packages/plugin-action-logs/src/server.ts create mode 100644 packages/plugin-action-logs/tsconfig.build.json create mode 100644 packages/plugin-action-logs/tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..a2db61f6b --- /dev/null +++ b/.env.example @@ -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= diff --git a/packages/database/src/collection.ts b/packages/database/src/collection.ts index a0f0d8ab5..a91604eff 100644 --- a/packages/database/src/collection.ts +++ b/packages/database/src/collection.ts @@ -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) { diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 27f466d7b..956d1bc44 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -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'; \ No newline at end of file diff --git a/packages/database/src/repository.ts b/packages/database/src/repository.ts index a8a5ac058..e7fb3ae45 100644 --- a/packages/database/src/repository.ts +++ b/packages/database/src/repository.ts @@ -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(values, { transaction }); + const instance = await this.model.create(values, { + ...options, + transaction, + }); if (!instance) { return; } await updateAssociations(instance, values, { + ...options, transaction, }); @@ -316,7 +324,7 @@ export class Repository { + 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); + }); +}); diff --git a/packages/plugin-action-logs/src/collections/action_changes.ts b/packages/plugin-action-logs/src/collections/action_changes.ts new file mode 100644 index 000000000..24f230033 --- /dev/null +++ b/packages/plugin-action-logs/src/collections/action_changes.ts @@ -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; diff --git a/packages/plugin-action-logs/src/collections/action_logs.ts b/packages/plugin-action-logs/src/collections/action_logs.ts new file mode 100644 index 000000000..f67bfd699 --- /dev/null +++ b/packages/plugin-action-logs/src/collections/action_logs.ts @@ -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; diff --git a/packages/plugin-action-logs/src/constants.ts b/packages/plugin-action-logs/src/constants.ts new file mode 100644 index 000000000..0cf75c724 --- /dev/null +++ b/packages/plugin-action-logs/src/constants.ts @@ -0,0 +1,3 @@ +export const LOG_TYPE_CREATE = 'create'; +export const LOG_TYPE_UPDATE = 'update'; +export const LOG_TYPE_DESTROY = 'destroy'; diff --git a/packages/plugin-action-logs/src/hooks/after-create.ts b/packages/plugin-action-logs/src/hooks/after-create.ts new file mode 100644 index 000000000..75644e904 --- /dev/null +++ b/packages/plugin-action-logs/src/hooks/after-create.ts @@ -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(); + // } + } +} diff --git a/packages/plugin-action-logs/src/hooks/after-destroy.ts b/packages/plugin-action-logs/src/hooks/after-destroy.ts new file mode 100644 index 000000000..2a3fc0384 --- /dev/null +++ b/packages/plugin-action-logs/src/hooks/after-destroy.ts @@ -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(); + // } + } +} diff --git a/packages/plugin-action-logs/src/hooks/after-update.ts b/packages/plugin-action-logs/src/hooks/after-update.ts new file mode 100644 index 000000000..879e94649 --- /dev/null +++ b/packages/plugin-action-logs/src/hooks/after-update.ts @@ -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(); + // } + } +} diff --git a/packages/plugin-action-logs/src/hooks/index.ts b/packages/plugin-action-logs/src/hooks/index.ts new file mode 100644 index 000000000..3c84cf55e --- /dev/null +++ b/packages/plugin-action-logs/src/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './after-create'; +export * from './after-update'; +export * from './after-destroy'; diff --git a/packages/plugin-action-logs/src/index.ts b/packages/plugin-action-logs/src/index.ts new file mode 100644 index 000000000..7ddad5814 --- /dev/null +++ b/packages/plugin-action-logs/src/index.ts @@ -0,0 +1 @@ +export { default } from './server'; diff --git a/packages/plugin-action-logs/src/server.ts b/packages/plugin-action-logs/src/server.ts new file mode 100644 index 000000000..df18b46e0 --- /dev/null +++ b/packages/plugin-action-logs/src/server.ts @@ -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; diff --git a/packages/plugin-action-logs/tsconfig.build.json b/packages/plugin-action-logs/tsconfig.build.json new file mode 100644 index 000000000..8d3db734b --- /dev/null +++ b/packages/plugin-action-logs/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./lib", + "declaration": true + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] +} \ No newline at end of file diff --git a/packages/plugin-action-logs/tsconfig.json b/packages/plugin-action-logs/tsconfig.json new file mode 100644 index 000000000..e10a5305a --- /dev/null +++ b/packages/plugin-action-logs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./esm/*", "./lib/*"] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9064d3ae6..6d0a78fa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"