From 380b5e8c7a39b2e2fe457b2c819e41028f7dc0a8 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 19 Jan 2022 10:02:52 +0800 Subject: [PATCH] Feat/plugin collection manager (#147) * refactor: collection manager plugin * feat(database): magic attribute model * MagicAttributeModel * load collections & fields options * collections filterTargetKey --- packages/database/src/collection.ts | 5 +- packages/database/src/fields/field.ts | 13 +- packages/plugin-collection-manager/.npmignore | 7 + .../plugin-collection-manager/package.json | 11 ++ .../__tests__/collections.repository.test.ts | 162 ++++++++++++++++++ .../src/__tests__/fields.repository.test.ts | 86 ++++++++++ .../src/__tests__/fields/belongsTo.test.ts | 0 .../__tests__/fields/belongsToMany.test.ts | 0 .../src/__tests__/fields/children.test.ts | 79 +++++++++ .../src/__tests__/fields/hasMany.test.ts | 75 ++++++++ .../src/__tests__/fields/hasOne.test.ts | 73 ++++++++ .../src/__tests__/fields/reverseField.test.ts | 59 +++++++ .../src/__tests__/index.ts | 11 ++ .../src/collections/collections.ts | 42 +++++ .../src/collections/fields.ts | 65 +++++++ .../src/hooks/afterCreateForReverseField.ts | 14 ++ .../beforeCreateForChildrenCollection.ts | 25 +++ .../src/hooks/beforeCreateForReverseField.ts | 38 ++++ .../src/hooks/beforeInitOptions.ts | 58 +++++++ .../plugin-collection-manager/src/index.ts | 45 +++++ .../src/models/collection.ts | 54 ++++++ .../src/models/field.ts | 32 ++++ .../src/repositories/collection-repository.ts | 17 ++ 23 files changed, 967 insertions(+), 4 deletions(-) create mode 100644 packages/plugin-collection-manager/.npmignore create mode 100644 packages/plugin-collection-manager/package.json create mode 100644 packages/plugin-collection-manager/src/__tests__/collections.repository.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/fields.repository.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/fields/belongsTo.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/fields/belongsToMany.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/fields/children.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/fields/hasMany.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/fields/hasOne.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/fields/reverseField.test.ts create mode 100644 packages/plugin-collection-manager/src/__tests__/index.ts create mode 100644 packages/plugin-collection-manager/src/collections/collections.ts create mode 100644 packages/plugin-collection-manager/src/collections/fields.ts create mode 100644 packages/plugin-collection-manager/src/hooks/afterCreateForReverseField.ts create mode 100644 packages/plugin-collection-manager/src/hooks/beforeCreateForChildrenCollection.ts create mode 100644 packages/plugin-collection-manager/src/hooks/beforeCreateForReverseField.ts create mode 100644 packages/plugin-collection-manager/src/hooks/beforeInitOptions.ts create mode 100644 packages/plugin-collection-manager/src/index.ts create mode 100644 packages/plugin-collection-manager/src/models/collection.ts create mode 100644 packages/plugin-collection-manager/src/models/field.ts create mode 100644 packages/plugin-collection-manager/src/repositories/collection-repository.ts diff --git a/packages/database/src/collection.ts b/packages/database/src/collection.ts index 0db522396..3f8fbb64e 100644 --- a/packages/database/src/collection.ts +++ b/packages/database/src/collection.ts @@ -188,9 +188,6 @@ export class Collection< /** * TODO - * - * @param name - * @param options */ updateOptions(options: CollectionOptions, mergeOptions?: any) { let newOptions = lodash.cloneDeep(options); @@ -206,6 +203,8 @@ export class Collection< } this.context.database.emit('afterUpdateCollection', this); + + return this; } setUpHooks(bindHooks) { diff --git a/packages/database/src/fields/field.ts b/packages/database/src/fields/field.ts index ebbc2fc28..cbc504b53 100644 --- a/packages/database/src/fields/field.ts +++ b/packages/database/src/fields/field.ts @@ -1,7 +1,7 @@ import { Collection } from '../collection'; import { Database } from '../database'; import _ from 'lodash'; -import { DataType, ModelAttributeColumnOptions, ModelIndexesOptions } from 'sequelize'; +import { DataType, ModelAttributeColumnOptions, ModelIndexesOptions, SyncOptions } from 'sequelize'; export interface FieldContext { database: Database; @@ -45,6 +45,17 @@ export abstract class Field { this.init(); } + // TODO + async sync(syncOptions: SyncOptions) { + await this.collection.sync({ + ...syncOptions, + force: false, + alter: { + drop: false, + }, + }); + } + init() { // code } diff --git a/packages/plugin-collection-manager/.npmignore b/packages/plugin-collection-manager/.npmignore new file mode 100644 index 000000000..461574b2f --- /dev/null +++ b/packages/plugin-collection-manager/.npmignore @@ -0,0 +1,7 @@ +node_modules +*.log +docs +__tests__ +tsconfig.json +src +.fatherrc.ts \ No newline at end of file diff --git a/packages/plugin-collection-manager/package.json b/packages/plugin-collection-manager/package.json new file mode 100644 index 000000000..dbdc84799 --- /dev/null +++ b/packages/plugin-collection-manager/package.json @@ -0,0 +1,11 @@ +{ + "name": "@nocobase/plugin-plugin-manager", + "version": "0.6.0-alpha.0", + "main": "lib/index.js", + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "@nocobase/test": "^0.6.0-alpha.0" + }, + "gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4" +} diff --git a/packages/plugin-collection-manager/src/__tests__/collections.repository.test.ts b/packages/plugin-collection-manager/src/__tests__/collections.repository.test.ts new file mode 100644 index 000000000..2d2f873b7 --- /dev/null +++ b/packages/plugin-collection-manager/src/__tests__/collections.repository.test.ts @@ -0,0 +1,162 @@ +import Database, { Collection as DBCollection } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '.'; + +describe('collections repository', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + await app.db.sync(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('case 1', async () => { + // 什么都没提供,随机 name 和 key + const data = await Collection.repository.create({ + values: {}, + }); + expect(data.get('key')).toBeDefined(); + expect(data.get('name')).toBeDefined(); + }); + + it('case 2', async () => { + // 提供了 name + const data = await Collection.repository.create({ + values: { + name: 'tests', + }, + }); + expect(data.toJSON()).toMatchObject({ + name: 'tests', + }); + }); + + it('case 3', async () => { + // 动态参数,存 options 字段里 + const data = await Collection.repository.create({ + values: { + name: 'tests', + createdBy: true, + updatedBy: true, + timestamps: true, + }, + }); + expect(data.toJSON()).toMatchObject({ + name: 'tests', + createdBy: true, + updatedBy: true, + timestamps: true, + }); + const [updated] = await Collection.repository.update({ + filterByTk: data.get('key') as any, + values: { + createdBy: false, + updatedBy: false, + timestamps: false, + }, + }); + expect(updated.toJSON()).toMatchObject({ + name: 'tests', + createdBy: false, + updatedBy: false, + timestamps: false, + }); + }); + + it('case 4', async () => { + await Collection.repository.create({ + values: { + name: 'tests', + fields: [ + { + type: 'uid', + name: 'name', + prefix: 'f_', + }, + { + type: 'string', + unique: true, + }, + { + type: 'string', + name: 'title', + unique: true, + }, + { + type: 'belongsToMany', + target: 'tests', + }, + { + type: 'belongsTo', + target: 'foos', + }, + { + type: 'hasMany', + target: 'foos', + }, + { + type: 'hasOne', + target: 'foos', + }, + ], + }, + }); + + const data = await Collection.repository.findOne({ + filter: { + name: 'tests', + }, + appends: ['fields'], + }); + + const json = data.toJSON(); + + expect(json.fields.length).toBe(7); + + expect(json).toMatchObject({ + name: 'tests', + fields: [ + { + type: 'uid', + name: 'name', + prefix: 'f_', + }, + { + type: 'string', + unique: true, + }, + { + type: 'string', + name: 'title', + unique: true, + }, + { + type: 'belongsToMany', + target: 'tests', + }, + { + type: 'belongsTo', + target: 'foos', + }, + { + type: 'hasMany', + target: 'foos', + }, + { + type: 'hasOne', + target: 'foos', + }, + ], + }); + }); +}); diff --git a/packages/plugin-collection-manager/src/__tests__/fields.repository.test.ts b/packages/plugin-collection-manager/src/__tests__/fields.repository.test.ts new file mode 100644 index 000000000..43a5b885e --- /dev/null +++ b/packages/plugin-collection-manager/src/__tests__/fields.repository.test.ts @@ -0,0 +1,86 @@ +import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '.'; + +describe('collections repository', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + await app.db.sync(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + await Collection.repository.create({ + values: { + name: 'tests', + }, + }); + await Collection.repository.create({ + values: { + name: 'foos', + }, + }); + await Collection.repository.create({ + values: { + name: 'bars', + }, + }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should generate the name and key randomly', async () => { + const field = await Field.repository.create({ + values: { + type: 'string', + collectionName: 'tests', + }, + }); + expect(field.toJSON()).toMatchObject({ + type: 'string', + collectionName: 'tests', + }); + expect(field.get('name')).toBeDefined(); + expect(field.get('key')).toBeDefined(); + }); + + it('should not generate the name randomly', async () => { + const field = await Field.repository.create({ + values: { + type: 'string', + name: 'name', + collectionName: 'tests', + }, + }); + expect(field.toJSON()).toMatchObject({ + type: 'string', + name: 'name', + collectionName: 'tests', + }); + }); + + it('dynamic parameters', async () => { + const field = await Field.repository.create({ + values: { + type: 'string', + name: 'name', + collectionName: 'tests', + unique: true, + defaultValue: 'abc', + } as StringFieldOptions, + }); + expect(field.toJSON()).toMatchObject({ + type: 'string', + name: 'name', + collectionName: 'tests', + unique: true, + defaultValue: 'abc', + }); + }); +}); diff --git a/packages/plugin-collection-manager/src/__tests__/fields/belongsTo.test.ts b/packages/plugin-collection-manager/src/__tests__/fields/belongsTo.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-collection-manager/src/__tests__/fields/belongsToMany.test.ts b/packages/plugin-collection-manager/src/__tests__/fields/belongsToMany.test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-collection-manager/src/__tests__/fields/children.test.ts b/packages/plugin-collection-manager/src/__tests__/fields/children.test.ts new file mode 100644 index 000000000..ddc60a18d --- /dev/null +++ b/packages/plugin-collection-manager/src/__tests__/fields/children.test.ts @@ -0,0 +1,79 @@ +import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '..'; + +describe('children options', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + await app.db.sync(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + await Collection.repository.create({ + values: { + name: 'tests', + }, + }); + await Collection.repository.create({ + values: { + name: 'foos', + }, + }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('when there are no children, the target collection is not created', async () => { + const field = await Field.repository.create({ + values: { + type: 'hasMany', + collectionName: 'tests', + }, + }); + const json = field.toJSON(); + expect(json).toMatchObject({ + type: 'hasMany', + collectionName: 'tests', + sourceKey: 'id', + targetKey: 'id', + }); + expect(json.name).toBeDefined(); + expect(json.target).toBeDefined(); + expect(json.foreignKey).toBeDefined(); + // 无 children 时,target collection 不创建 + const target = await Collection.model.findOne({ + where: { + name: json.target, + }, + }); + expect(target).toBeNull(); + }); + + it('the collectionName of the child field is the target of the parent field', async () => { + const field = await Field.repository.create({ + values: { + type: 'hasMany', + collectionName: 'tests', + children: [{ type: 'string' }, { type: 'string' }], + }, + }); + const json = field.toJSON(); + const target = await Collection.model.findOne({ + where: { + name: json.target, + }, + }); + expect(target).toBeDefined(); + // 子字段的 collectionName 是父字段的 target + for (const child of json.children) { + expect(child.collectionName).toBe(json.target); + } + }); +}); diff --git a/packages/plugin-collection-manager/src/__tests__/fields/hasMany.test.ts b/packages/plugin-collection-manager/src/__tests__/fields/hasMany.test.ts new file mode 100644 index 000000000..734357e7f --- /dev/null +++ b/packages/plugin-collection-manager/src/__tests__/fields/hasMany.test.ts @@ -0,0 +1,75 @@ +import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '..'; + +describe('hasMany field options', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + await app.db.sync(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + await Collection.repository.create({ + values: { + name: 'tests', + }, + }); + await Collection.repository.create({ + values: { + name: 'foos', + }, + }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should generate the foreignKey randomly', async () => { + const field = await Field.repository.create({ + values: { + type: 'hasMany', + collectionName: 'tests', + target: 'foos', + }, + }); + const json = field.toJSON(); + expect(json).toMatchObject({ + type: 'hasMany', + collectionName: 'tests', + target: 'foos', + sourceKey: 'id', + targetKey: 'id', + }); + expect(json.name).toBeDefined(); + expect(json.foreignKey).toBeDefined(); + }); + + it('the parameters are not generated randomly', async () => { + const field = await Field.repository.create({ + values: { + name: 'foos', + type: 'hasMany', + collectionName: 'tests', + target: 'foos', + sourceKey: 'abc', + foreignKey: 'def', + targetKey: 'ghi', + }, + }); + expect(field.toJSON()).toMatchObject({ + name: 'foos', + type: 'hasMany', + collectionName: 'tests', + target: 'foos', + sourceKey: 'abc', + foreignKey: 'def', + targetKey: 'ghi', + }); + }); +}); diff --git a/packages/plugin-collection-manager/src/__tests__/fields/hasOne.test.ts b/packages/plugin-collection-manager/src/__tests__/fields/hasOne.test.ts new file mode 100644 index 000000000..2df4b5a3d --- /dev/null +++ b/packages/plugin-collection-manager/src/__tests__/fields/hasOne.test.ts @@ -0,0 +1,73 @@ +import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '..'; + +describe('hasOne field options', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + await app.db.sync(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + await Collection.repository.create({ + values: { + name: 'tests', + }, + }); + await Collection.repository.create({ + values: { + name: 'foos', + }, + }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should generate the foreignKey randomly', async () => { + const field = await Field.repository.create({ + values: { + type: 'hasOne', + collectionName: 'tests', + target: 'foos', + }, + }); + const json = field.toJSON(); + // hasOne 的 sourceKey 默认为 id,foreignKey 随机生成 + expect(json).toMatchObject({ + type: 'hasOne', + collectionName: 'tests', + target: 'foos', + sourceKey: 'id', + }); + expect(json.name).toBeDefined(); + expect(json.foreignKey).toBeDefined(); + }); + + it('the parameters are not generated randomly', async () => { + const field = await Field.repository.create({ + values: { + name: 'foo', + type: 'hasOne', + collectionName: 'tests', + target: 'foos', + sourceKey: 'abc', + foreignKey: 'def', + }, + }); + expect(field.toJSON()).toMatchObject({ + name: 'foo', + type: 'hasOne', + collectionName: 'tests', + target: 'foos', + sourceKey: 'abc', + foreignKey: 'def', + }); + }); +}); diff --git a/packages/plugin-collection-manager/src/__tests__/fields/reverseField.test.ts b/packages/plugin-collection-manager/src/__tests__/fields/reverseField.test.ts new file mode 100644 index 000000000..e046736fa --- /dev/null +++ b/packages/plugin-collection-manager/src/__tests__/fields/reverseField.test.ts @@ -0,0 +1,59 @@ +import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '..'; + +describe('reverseField options', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + await app.db.sync(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + await Collection.repository.create({ + values: { + name: 'tests', + }, + }); + await Collection.repository.create({ + values: { + name: 'targets', + }, + }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('reverseField', async () => { + const field = await Field.repository.create({ + values: { + type: 'hasMany', + collectionName: 'tests', + target: 'targets', + reverseField: {}, + }, + }); + const json = JSON.parse(JSON.stringify(field.toJSON())); + expect(json).toMatchObject({ + type: 'hasMany', + collectionName: 'tests', + target: 'targets', + targetKey: 'id', + sourceKey: 'id', + reverseField: { + type: 'belongsTo', + collectionName: 'targets', + target: 'tests', + targetKey: 'id', + sourceKey: 'id', + }, + }); + expect(json.foreignKey).toBe(json.reverseField.foreignKey); + }); +}); diff --git a/packages/plugin-collection-manager/src/__tests__/index.ts b/packages/plugin-collection-manager/src/__tests__/index.ts new file mode 100644 index 000000000..1eaf5dc56 --- /dev/null +++ b/packages/plugin-collection-manager/src/__tests__/index.ts @@ -0,0 +1,11 @@ +import { mockServer } from '@nocobase/test'; +import CollectionManagerPlugin from '..'; + +export async function createApp() { + const app = mockServer(); + const queryInterface = app.db.sequelize.getQueryInterface(); + await queryInterface.dropAllTables(); + app.plugin(CollectionManagerPlugin); + await app.load(); + return app; +} diff --git a/packages/plugin-collection-manager/src/collections/collections.ts b/packages/plugin-collection-manager/src/collections/collections.ts new file mode 100644 index 000000000..0dd3eaee6 --- /dev/null +++ b/packages/plugin-collection-manager/src/collections/collections.ts @@ -0,0 +1,42 @@ +import { CollectionOptions } from '@nocobase/database'; + +export default { + name: 'collections', + title: '数据表配置', + sortable: 'sort', + autoGenId: false, + model: 'CollectionModel', + timestamps: false, + filterTargetKey: 'name', + fields: [ + { + type: 'uid', + name: 'key', + primaryKey: true, + }, + { + type: 'uid', + name: 'name', + unique: true, + prefix: 't_', + }, + { + type: 'string', + name: 'title', + required: true, + }, + { + type: 'json', + name: 'options', + defaultValue: {}, + }, + { + type: 'hasMany', + name: 'fields', + target: 'fields', + sourceKey: 'name', + targetKey: 'name', + foreignKey: 'collectionName', + }, + ], +} as CollectionOptions; diff --git a/packages/plugin-collection-manager/src/collections/fields.ts b/packages/plugin-collection-manager/src/collections/fields.ts new file mode 100644 index 000000000..fa64c74e6 --- /dev/null +++ b/packages/plugin-collection-manager/src/collections/fields.ts @@ -0,0 +1,65 @@ +import { CollectionOptions } from '@nocobase/database'; + +export default { + name: 'fields', + autoGenId: false, + model: 'FieldModel', + timestamps: false, + sortable: { + type: 'sort', + name: 'sort', + scope: ['parentKey'], + }, + fields: [ + { + type: 'uid', + name: 'key', + primaryKey: true, + }, + { + type: 'uid', + name: 'name', + prefix: 'f_', + }, + { + type: 'string', + name: 'type', + }, + { + type: 'string', + name: 'interface', + allowNull: true, + }, + { + type: 'belongsTo', + name: 'collection', + target: 'collections', + foreignKey: 'collectionName', + targetKey: 'name', + }, + { + type: 'hasMany', + name: 'children', + target: 'fields', + sourceKey: 'key', + foreignKey: 'parentKey', + }, + { + type: 'hasOne', + name: 'reverseField', + target: 'fields', + sourceKey: 'key', + foreignKey: 'reverseKey', + }, + { + type: 'belongsTo', + name: 'uiSchema', + target: 'ui_schemas', + }, + { + type: 'json', + name: 'options', + defaultValue: {}, + }, + ], +} as CollectionOptions; diff --git a/packages/plugin-collection-manager/src/hooks/afterCreateForReverseField.ts b/packages/plugin-collection-manager/src/hooks/afterCreateForReverseField.ts new file mode 100644 index 000000000..966bfd1f0 --- /dev/null +++ b/packages/plugin-collection-manager/src/hooks/afterCreateForReverseField.ts @@ -0,0 +1,14 @@ +import Database from '@nocobase/database'; + +export function afterCreateForReverseField(db: Database) { + const Field = db.getCollection('fields'); + + return async (model, { transaction }) => { + const reverseKey = model.get('reverseKey'); + if (!reverseKey) { + return; + } + const reverse = await Field.model.findByPk(reverseKey, { transaction }); + await reverse.update({ reverseKey: model.get('key') }, { hooks: false, transaction }); + }; +} diff --git a/packages/plugin-collection-manager/src/hooks/beforeCreateForChildrenCollection.ts b/packages/plugin-collection-manager/src/hooks/beforeCreateForChildrenCollection.ts new file mode 100644 index 000000000..8dd5b4337 --- /dev/null +++ b/packages/plugin-collection-manager/src/hooks/beforeCreateForChildrenCollection.ts @@ -0,0 +1,25 @@ +import Database from '@nocobase/database'; + +export function beforeCreateForChildrenCollection(db: Database) { + const Collection = db.getCollection('collections'); + const Field = db.getCollection('fields'); + + return async (model, { transaction }) => { + const parentKey = model.get('parentKey'); + if (!parentKey) { + return; + } + const parent = await Field.model.findByPk(parentKey, { transaction }); + const parentTarget = parent.get('target'); + model.set('collectionName', parentTarget); + const collection = await Collection.model.findOne({ + transaction, + where: { + name: parentTarget, + }, + }); + if (!collection) { + await Collection.model.create({ name: parentTarget }, { transaction }); + } + }; +} diff --git a/packages/plugin-collection-manager/src/hooks/beforeCreateForReverseField.ts b/packages/plugin-collection-manager/src/hooks/beforeCreateForReverseField.ts new file mode 100644 index 000000000..dea9f007c --- /dev/null +++ b/packages/plugin-collection-manager/src/hooks/beforeCreateForReverseField.ts @@ -0,0 +1,38 @@ +import Database from '@nocobase/database'; + +export function beforeCreateForReverseField(db: Database) { + const Field = db.getCollection('fields'); + + return async (model, { transaction }) => { + const reverseKey = model.get('reverseKey'); + if (!reverseKey) { + return; + } + const reverse = await Field.model.findByPk(reverseKey, { transaction }); + model.set('collectionName', reverse.get('target')); + model.set('target', reverse.get('collectionName')); + const reverseType = reverse.get('type') as any; + if (['hasMany', 'hasOne'].includes(reverseType)) { + model.set('type', 'belongsTo'); + model.set('targetKey', reverse.get('sourceKey')); + model.set('foreignKey', reverse.get('foreignKey')); + model.set('sourceKey', reverse.get('targetKey')); + } + if (['belongsTo'].includes(reverseType)) { + if (!model.get('type')) { + model.set('type', 'hasMany'); + } + model.set('sourceKey', reverse.get('targetKey')); + model.set('foreignKey', reverse.get('foreignKey')); + model.set('targetKey', reverse.get('sourceKey')); + } + if (['belongsToMany'].includes(reverseType)) { + model.set('type', 'belongsToMany'); + model.set('through', reverse.get('through')); + model.set('sourceKey', reverse.get('targetKey')); + model.set('foreignKey', reverse.get('otherKey')); + model.set('targetKey', reverse.get('sourceKey')); + model.set('otherKey', reverse.get('foreignKey')); + } + }; +} diff --git a/packages/plugin-collection-manager/src/hooks/beforeInitOptions.ts b/packages/plugin-collection-manager/src/hooks/beforeInitOptions.ts new file mode 100644 index 000000000..d45869762 --- /dev/null +++ b/packages/plugin-collection-manager/src/hooks/beforeInitOptions.ts @@ -0,0 +1,58 @@ +import { uid } from '@nocobase/utils'; +import { Model } from 'sequelize'; + +export default { + belongsTo(model: Model) { + const defaults = { + targetKey: 'id', + foreignKey: `f_${uid()}`, + }; + for (const key in defaults) { + if (model.get(key)) { + continue; + } + model.set(key, defaults[key]); + } + }, + belongsToMany(model: Model) { + const defaults = { + targetKey: 'id', + sourceKey: 'id', + through: `t_${uid()}`, + foreignKey: `f_${uid()}`, + otherKey: `f_${uid()}`, + }; + for (const key in defaults) { + if (model.get(key)) { + continue; + } + model.set(key, defaults[key]); + } + }, + hasMany(model: Model) { + const defaults = { + targetKey: 'id', + sourceKey: 'id', + foreignKey: `f_${uid()}`, + target: `t_${uid()}`, + }; + for (const key in defaults) { + if (model.get(key)) { + continue; + } + model.set(key, defaults[key]); + } + }, + hasOne(model: Model) { + const defaults = { + sourceKey: 'id', + foreignKey: `f_${uid()}`, + }; + for (const key in defaults) { + if (model.get(key)) { + continue; + } + model.set(key, defaults[key]); + } + }, +}; diff --git a/packages/plugin-collection-manager/src/index.ts b/packages/plugin-collection-manager/src/index.ts new file mode 100644 index 000000000..3b1f54c89 --- /dev/null +++ b/packages/plugin-collection-manager/src/index.ts @@ -0,0 +1,45 @@ +import path from 'path'; +import { Plugin } from '@nocobase/server'; +import { CollectionModel } from './models/collection'; +import { FieldModel } from './models/field'; +import { uid } from '@nocobase/utils'; +import beforeInitOptions from './hooks/beforeInitOptions'; +import { beforeCreateForChildrenCollection } from './hooks/beforeCreateForChildrenCollection'; +import { beforeCreateForReverseField } from './hooks/beforeCreateForReverseField'; +import { afterCreateForReverseField } from './hooks/afterCreateForReverseField'; + +export default class CollectionManagerPlugin extends Plugin { + async load() { + this.app.db.registerModels({ + CollectionModel, + FieldModel, + }); + await this.app.db.import({ + directory: path.resolve(__dirname, './collections'), + }); + // 要在 beforeInitOptions 之前处理 + this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db)); + this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db)); + this.app.db.on('fields.beforeCreate', async (model, options) => { + const type = model.get('type'); + await this.app.db.emitAsync(`fields.${type}.beforeInitOptions`, model, options); + }); + for (const key in beforeInitOptions) { + if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) { + const fn = beforeInitOptions[key]; + this.app.db.on(`fields.${key}.beforeInitOptions`, fn); + } + } + this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db)); + this.app.db.on('collections.afterCreate', async (model, options) => { + if (options.context) { + await model.migrate(); + } + }); + this.app.db.on('fields.afterCreate', async (model, options) => { + if (options.context) { + await model.migrate(); + } + }); + } +} diff --git a/packages/plugin-collection-manager/src/models/collection.ts b/packages/plugin-collection-manager/src/models/collection.ts new file mode 100644 index 000000000..2c8de92ff --- /dev/null +++ b/packages/plugin-collection-manager/src/models/collection.ts @@ -0,0 +1,54 @@ +import { SyncOptions } from 'sequelize'; +import Database, { Collection, MagicAttributeModel } from '@nocobase/database'; +import { FieldModel } from './field'; + +interface LoadOptions { + // TODO + skipField?: boolean; + skipExist?: boolean; +} + +export class CollectionModel extends MagicAttributeModel { + + get db(): Database { + return (this.constructor).database; + } + + async load(loadOptions?: LoadOptions) { + const { skipExist, skipField } = loadOptions; + const name = this.get('name'); + let collection: Collection; + if (this.db.hasCollection(name)) { + collection = this.db.getCollection(name); + if (skipExist) { + return collection; + } + collection.updateOptions(this.get()); + } else { + collection = this.db.collection(this.get()); + } + if (!skipField) { + await this.loadFields(); + } + return collection; + } + + async loadFields() { + // @ts-ignore + const instances: FieldModel[] = await this.getFields(); + for (const instance of instances) { + await instance.load(); + } + } + + async migrate(options?: SyncOptions) { + const collection = await this.load(); + await collection.sync({ + force: false, + alter: { + drop: false, + }, + ...options, + }); + } +} diff --git a/packages/plugin-collection-manager/src/models/field.ts b/packages/plugin-collection-manager/src/models/field.ts new file mode 100644 index 000000000..8c446a08a --- /dev/null +++ b/packages/plugin-collection-manager/src/models/field.ts @@ -0,0 +1,32 @@ +import { SyncOptions } from 'sequelize'; +import Database, { MagicAttributeModel } from '@nocobase/database'; + +interface LoadOptions { + // TODO + skipExist?: boolean; +} + +export class FieldModel extends MagicAttributeModel { + get db(): Database { + return (this.constructor).database; + } + + async load(loadOptions?: LoadOptions) { + const { skipExist } = loadOptions; + const collectionName = this.get('collectionName'); + if (!this.db.hasCollection(collectionName)) { + throw new Error(`${collectionName} collection does not exist.`); + } + const collection = this.db.getCollection(collectionName); + const name = this.get('name'); + if (skipExist && collection.hasField(name)) { + return collection.getField(name); + } + return collection.setField(name, this.get()); + } + + async migrate(options?: SyncOptions) { + const field = await this.load(); + await field.sync(options); + } +} diff --git a/packages/plugin-collection-manager/src/repositories/collection-repository.ts b/packages/plugin-collection-manager/src/repositories/collection-repository.ts new file mode 100644 index 000000000..1aecf2c04 --- /dev/null +++ b/packages/plugin-collection-manager/src/repositories/collection-repository.ts @@ -0,0 +1,17 @@ +import { Repository } from '@nocobase/database'; +import { CollectionModel } from '../models/collection'; + +interface LoadOptions { + filter?: any; + skipExist?: boolean; +} + +export class CollectionRepository extends Repository { + async load(options?: LoadOptions) { + const { filter, skipExist } = options; + const instances = (await this.find({ filter })) as CollectionModel[]; + for (const instance of instances) { + await instance.load({ skipExist }); + } + } +}