From 30a867803689dfbbbf4d3436bee60b863a2e7f23 Mon Sep 17 00:00:00 2001 From: chenos Date: Mon, 20 Feb 2023 14:25:46 +0800 Subject: [PATCH] feat: adjacency list --- packages/core/actions/src/actions/list.ts | 4 +- .../Configuration/AddCollectionAction.tsx | 9 +- .../src/collection-manager/interfaces/m2o.tsx | 2 +- .../collection-manager/templates/index.tsx | 1 + .../src/collection-manager/templates/tree.tsx | 72 ++++++++++ .../templates/treeCollection.tsx | 0 .../src/collection-manager/templates/types.ts | 1 + .../core/database/src/__tests__/tree.test.ts | 123 ++++++++++++++++++ packages/core/database/src/collection.ts | 51 +++++++- packages/core/database/src/repository.ts | 3 +- .../hooks/afterCreateForForeignKeyField.ts | 10 +- .../plugins/collection-manager/src/server.ts | 2 +- 12 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 packages/core/client/src/collection-manager/templates/tree.tsx delete mode 100644 packages/core/client/src/collection-manager/templates/treeCollection.tsx create mode 100644 packages/core/database/src/__tests__/tree.test.ts diff --git a/packages/core/actions/src/actions/list.ts b/packages/core/actions/src/actions/list.ts index 4e61dfc107..62fb52ca0c 100644 --- a/packages/core/actions/src/actions/list.ts +++ b/packages/core/actions/src/actions/list.ts @@ -23,8 +23,8 @@ function totalPage(total, pageSize): number { } function findArgs(params: ActionParams) { - const { fields, filter, appends, except, sort } = params; - return { filter, fields, appends, except, sort }; + const { tree, fields, filter, appends, except, sort } = params; + return { tree, filter, fields, appends, except, sort }; } async function listWithPagination(ctx: Context) { diff --git a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx index 9bf610f1c7..844425f97d 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx @@ -84,7 +84,7 @@ const getSchema = (schema, category, compile): ISchema => { 'x-component': 'Action', 'x-component-props': { type: 'primary', - useAction: () => useCreateCollection(), + useAction: () => useCreateCollection(schema), }, }, }, @@ -188,16 +188,19 @@ const useDefaultCollectionFields = (values) => { return defaults; }; -const useCreateCollection = () => { +const useCreateCollection = (schema?: any) => { const form = useForm(); const { refreshCM } = useCollectionManager(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); - const { resource } = useResourceContext(); + const { resource, collection } = useResourceContext(); return { async run() { await form.submit(); const values = cloneDeep(form.values); + if (schema?.events?.beforeSubmit) { + schema.events.beforeSubmit(values); + } const fields = useDefaultCollectionFields(values); if (values.autoCreateReverseField) { } else { diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index 5d2d9c7cdd..65268c3d8e 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -5,7 +5,7 @@ import { recordPickerSelector, recordPickerViewer, relationshipType, - reverseFieldProperties, + reverseFieldProperties } from './properties'; import { IField } from './types'; diff --git a/packages/core/client/src/collection-manager/templates/index.tsx b/packages/core/client/src/collection-manager/templates/index.tsx index 09afc72d45..2bf123f947 100644 --- a/packages/core/client/src/collection-manager/templates/index.tsx +++ b/packages/core/client/src/collection-manager/templates/index.tsx @@ -1,3 +1,4 @@ export * from './calendar'; export * from './general'; +export * from './tree'; diff --git a/packages/core/client/src/collection-manager/templates/tree.tsx b/packages/core/client/src/collection-manager/templates/tree.tsx new file mode 100644 index 0000000000..438605c644 --- /dev/null +++ b/packages/core/client/src/collection-manager/templates/tree.tsx @@ -0,0 +1,72 @@ +import { getConfigurableProperties } from './properties'; +import { ICollectionTemplate } from './types'; + +export const tree: ICollectionTemplate = { + name: 'tree', + title: '{{t("Tree collection")}}', + order: 3, + color: 'blue', + default: { + tree: 'adjacencyList', + fields: [ + { + interface: 'integer', + name: 'parentId', + type: 'bigInt', + isForeignKey: true, + uiSchema: { + type: 'number', + title: '{{t("Parent ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + interface: 'm2o', + type: 'belongsTo', + name: 'parent', + foreignKey: 'parentId', + uiSchema: { + title: '{{t("Parent")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: false, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + { + interface: 'o2m', + type: 'hasMany', + name: 'children', + foreignKey: 'parentId', + uiSchema: { + title: '{{t("Children")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: true, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + ], + }, + events: { + beforeSubmit(values) { + if (Array.isArray(values?.fields)) { + values?.fields.map((f) => { + f.target = values.name; + }); + } + }, + }, + configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'category', 'moreOptions'), +}; diff --git a/packages/core/client/src/collection-manager/templates/treeCollection.tsx b/packages/core/client/src/collection-manager/templates/treeCollection.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/core/client/src/collection-manager/templates/types.ts b/packages/core/client/src/collection-manager/templates/types.ts index 3047795e7a..183e30d43e 100644 --- a/packages/core/client/src/collection-manager/templates/types.ts +++ b/packages/core/client/src/collection-manager/templates/types.ts @@ -9,6 +9,7 @@ export interface ICollectionTemplate { order?: number; /** 默认配置 */ default?: CollectionOptions; + events?: any; /** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */ configurableProperties?: Record; /** 当前模板可用的字段类型 */ diff --git a/packages/core/database/src/__tests__/tree.test.ts b/packages/core/database/src/__tests__/tree.test.ts new file mode 100644 index 0000000000..3fd02b9fe0 --- /dev/null +++ b/packages/core/database/src/__tests__/tree.test.ts @@ -0,0 +1,123 @@ +import { Database } from '../database'; +import { mockDatabase } from './'; + +describe('sort', function () { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should be auto completed', () => { + const collection = db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'belongsTo', + name: 'parent', + }, + { + type: 'hasMany', + name: 'children', + }, + ], + }); + expect(collection.getField('parent').options.target).toBe('categories'); + expect(collection.getField('parent').options.foreignKey).toBe('parentId'); + expect(collection.getField('children').options.target).toBe('categories'); + expect(collection.getField('children').options.foreignKey).toBe('parentId'); + }); + + it('should be tree', async () => { + db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'string', + name: 'description', + }, + { + type: 'belongsTo', + name: 'parent', + }, + { + type: 'hasMany', + name: 'children', + }, + ], + }); + await db.sync(); + const values = [ + { + name: '1', + children: [ + { + name: '1-1', + children: [ + { + name: '1-1-1', + children: [ + { + name: '1-1-1-1', + }, + ], + }, + ], + }, + ], + }, + { + name: '2', + children: [ + { + name: '2-1', + children: [ + { + name: '2-1-1', + children: [ + { + name: '2-1-1-1', + }, + ], + }, + ], + }, + ], + }, + ]; + + await db.getRepository('categories').create({ + values, + }); + + const instances = await db.getRepository('categories').find({ + filter: { + parentId: null, + }, + tree: true, + fields: ['id', 'name'], + appends: ['parent', 'children'], + sort: 'id', + }); + + expect(instances.map((i) => i.toJSON())).toMatchObject(values); + + const instance = await db.getRepository('categories').findOne({ + filterByTk: 1, + tree: true, + fields: ['id', 'name'], + }); + + expect(instance.toJSON()).toMatchObject(values[0]); + }); +}); diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index b1a3cc3f63..ff482bdc85 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -7,7 +7,7 @@ import { QueryInterfaceDropTableOptions, SyncOptions, Transactionable, - Utils, + Utils } from 'sequelize'; import { Database } from './database'; import { Field, FieldOptions } from './fields'; @@ -37,6 +37,8 @@ export interface CollectionOptions extends Omit */ magicAttribute?: string; + tree?: string; + [key: string]: any; } @@ -83,6 +85,7 @@ export class Collection< this.db.modelCollection.set(this.model, this); this.db.tableNameCollectionMap.set(this.model.tableName, this); + this.treeHook(); if (!options.inherits) { this.setFields(options.fields); @@ -92,6 +95,51 @@ export class Collection< this.setSortable(options.sortable); } + treeHook() { + if (!this.options.tree) { + return; + } + this.on('field.beforeAdd', (name, opts, { collection }) => { + if (!collection.options.tree) { + return; + } + if (name === 'parent' || name === 'children') { + opts.target = collection.name; + opts.foreignKey = 'parentId'; + } + }); + this.model.afterFind(async (instances, options: any) => { + if (!options.tree) { + return; + } + const arr: Model[] = Array.isArray(instances) ? instances : [instances]; + let index = 0; + for (const instance of arr) { + const opts = { + ...lodash.pick(options, ['tree', 'fields', 'appends', 'except', 'sort']), + }; + let __index = `${index++}`; + if (options.parentIndex) { + __index = `${options.parentIndex}.${__index}`; + } + instance.setDataValue('__index', __index); + const children = await this.repository.find({ + filter: { + parentId: instance.id, + }, + transaction: options.transaction, + ...opts, + // @ts-ignore + parentIndex: `${__index}.children`, + context: options.context, + }); + if (children?.length > 0) { + instance.setDataValue('children', children); + } + } + }); + } + private checkOptions(options: CollectionOptions) { checkIdentifier(options.name); this.checkTableName(); @@ -231,6 +279,7 @@ export class Collection< this.checkFieldType(name, options); const { database } = this.context; + this.emit('field.beforeAdd', name, options, { collection: this }); const field = database.buildField( { name, ...options }, diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index fc26bbffb2..d58d0f37d8 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -11,7 +11,7 @@ import { Op, Transactionable, UpdateOptions as SequelizeUpdateOptions, - WhereOperators, + WhereOperators } from 'sequelize'; import { Collection } from './collection'; import { Database } from './database'; @@ -106,6 +106,7 @@ export interface CommonFindOptions extends Transactionable { except?: Except; sort?: Sort; context?: any; + tree?: boolean; } export type FindOneOptions = Omit; diff --git a/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts b/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts index f1688d872d..4b5f491e90 100644 --- a/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts +++ b/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts @@ -99,7 +99,7 @@ export function afterCreateForForeignKeyField(db: Database) { }); } - return async (model, { transaction, context }) => { + const hook = async (model, { transaction, context }) => { // skip if no app context if (!context) { return; @@ -172,4 +172,12 @@ export function afterCreateForForeignKeyField(db: Database) { }); } }; + + return async (model, options) => { + try { + await hook(model, options); + } catch (error) { + + } + }; } diff --git a/packages/plugins/collection-manager/src/server.ts b/packages/plugins/collection-manager/src/server.ts index 54c9fb0684..0c064edd55 100644 --- a/packages/plugins/collection-manager/src/server.ts +++ b/packages/plugins/collection-manager/src/server.ts @@ -14,7 +14,7 @@ import { beforeCreateForChildrenCollection, beforeCreateForReverseField, beforeDestroyForeignKey, - beforeInitOptions, + beforeInitOptions } from './hooks'; import { InheritedCollection } from '@nocobase/database';