diff --git a/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts b/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts index 9479d61ecf..97e9f15d5b 100644 --- a/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts +++ b/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts @@ -1063,4 +1063,49 @@ describe('ui_schema repository', () => { 'x-async': false, }); }); + + it('should remove schema ancestor', async () => { + const schema = { + 'x-uid': 'A', + name: 'A', + properties: { + B: { + 'x-uid': 'B', + properties: { + C: { + 'x-uid': 'C', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + F: { + 'x-uid': 'F', + }, + }, + }, + E: { + 'x-uid': 'E', + }, + }, + }; + + await repository.insert(schema); + expect((await repository.getJsonSchema('B')).properties.C).toBeDefined(); + + await repository.clearAncestor('C'); + + expect((await repository.getJsonSchema('B')).properties.C).not.toBeDefined(); + + const c = await repository.getJsonSchema('C'); + expect(c).toMatchObject({ + 'x-uid': 'C', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }); + }); }); diff --git a/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts b/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts index 2c5f64a6b9..1677cf330d 100644 --- a/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts +++ b/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts @@ -1,56 +1,33 @@ import { Context } from '@nocobase/actions'; import UiSchemaRepository from '../repository'; +import lodash from 'lodash'; const getRepositoryFromCtx = (ctx: Context) => { return ctx.db.getCollection('uiSchemas').repository as UiSchemaRepository; }; +const callRepositoryMethod = (method, paramsKey: 'resourceIndex' | 'values') => { + return async (ctx, next) => { + const params = lodash.get(ctx.action.params, paramsKey); + + const repository = getRepositoryFromCtx(ctx); + const returnValue = await repository[method](params); + + ctx.body = returnValue || { + result: 'ok', + }; + + await next(); + }; +}; + export const uiSchemaActions = { - async getJsonSchema(ctx: Context, next) { - const { resourceIndex } = ctx.action.params; - const repository = getRepositoryFromCtx(ctx); - ctx.body = await repository.getJsonSchema(resourceIndex); - await next(); - }, - - async getProperties(ctx: Context, next) { - const { resourceIndex } = ctx.action.params; - const repository = getRepositoryFromCtx(ctx); - ctx.body = await repository.getProperties(resourceIndex); - await next(); - }, - - async insert(ctx: Context, next) { - const { values } = ctx.action.params; - const repository = getRepositoryFromCtx(ctx); - - ctx.body = await repository.insert(values); - await next(); - }, - - async remove(ctx: Context, next) { - const { resourceIndex } = ctx.action.params; - const repository = getRepositoryFromCtx(ctx); - await repository.remove(resourceIndex); - - ctx.body = { - result: 'ok', - }; - - await next(); - }, - - async patch(ctx: Context, next) { - const { values } = ctx.action.params; - const repository = getRepositoryFromCtx(ctx); - await repository.patch(values); - - ctx.body = { - result: 'ok', - }; - - await next(); - }, + getJsonSchema: callRepositoryMethod('getJsonSchema', 'resourceIndex'), + getProperties: callRepositoryMethod('getProperties', 'resourceIndex'), + insert: callRepositoryMethod('insert', 'values'), + remove: callRepositoryMethod('remove', 'resourceIndex'), + patch: callRepositoryMethod('patch', 'values'), + clearAncestor: callRepositoryMethod('clearAncestor', 'resourceIndex'), async insertAdjacent(ctx: Context, next) { const { resourceIndex, position, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params; @@ -63,6 +40,7 @@ export const uiSchemaActions = { await next(); }, + insertBeforeBegin: insertPositionActionBuilder('beforeBegin'), insertAfterBegin: insertPositionActionBuilder('afterBegin'), insertBeforeEnd: insertPositionActionBuilder('beforeEnd'), diff --git a/packages/plugin-ui-schema-storage/src/repository.ts b/packages/plugin-ui-schema-storage/src/repository.ts index 0865fd7541..8dc4b49311 100644 --- a/packages/plugin-ui-schema-storage/src/repository.ts +++ b/packages/plugin-ui-schema-storage/src/repository.ts @@ -13,7 +13,7 @@ type BreakRemoveOnType = { [key: string]: any; }; -export interface removeParentOptions { +export interface removeParentOptions extends TransactionAble { removeParentsIfNoChildren?: boolean; breakRemoveOn?: BreakRemoveOnType; } @@ -22,6 +22,45 @@ interface InsertAdjacentOptions extends removeParentOptions {} const nodeKeys = ['properties', 'definitions', 'patternProperties', 'additionalProperties', 'items']; +function transaction(transactionAbleArgPosition?: number) { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + const originalMethod = descriptor.value; + + descriptor.value = async function (...args) { + if (!lodash.isNumber(transactionAbleArgPosition)) { + transactionAbleArgPosition = originalMethod.length - 1; + } + + let transaction = lodash.get(args, [transactionAbleArgPosition, 'transaction']); + let handleTransaction = false; + if (!transaction) { + transaction = await this.database.sequelize.transaction(); + handleTransaction = true; + + lodash.set(args, transactionAbleArgPosition, { + ...lodash.get(args, transactionAbleArgPosition, {}), + transaction, + }); + } + + if (handleTransaction) { + try { + const results = await originalMethod.apply(this, args); + await transaction.commit(); + return results; + } catch (e) { + await transaction.rollback(); + throw e; + } + } else { + return await originalMethod.apply(this, args); + } + }; + + return descriptor; + }; +} + export class UiSchemaRepository extends Repository { tableNameAdapter(tableName) { if (this.database.sequelize.getDialect() === 'postgres') { @@ -212,19 +251,32 @@ export class UiSchemaRepository extends Repository { return buildTree(nodes.find((node) => node['x-uid'] == rootUid)); } - treeCollection() { - return this.database.getCollection('uiSchemaTreePath'); + @transaction() + async clearAncestor(uid: string, options?: TransactionAble) { + const db = this.database; + const treeTable = this.uiSchemaTreePathTableName; + + await db.sequelize.query( + `DELETE + FROM ${treeTable} + WHERE descendant IN + (SELECT descendant FROM (SELECT descendant FROM ${treeTable} WHERE ancestor = :uid) as descendantTable) + AND ancestor IN (SELECT ancestor + FROM (SELECT ancestor FROM ${treeTable} WHERE descendant = :uid AND ancestor != descendant) as ancestorTable) + `, + { + type: 'DELETE', + replacements: { + uid, + }, + transaction: options.transaction, + }, + ); } + @transaction() async patch(newSchema: any, options?) { - let handleTransaction = true; - let transaction; - if (options?.transaction) { - handleTransaction = false; - transaction = options.transaction; - } else { - transaction = await this.database.sequelize.transaction(); - } + const { transaction } = options; const rootUid = newSchema['x-uid']; const oldTree = await this.getJsonSchema(rootUid); @@ -244,14 +296,7 @@ export class UiSchemaRepository extends Repository { } }; - try { - await traverSchemaTree(newSchema); - - handleTransaction && (await transaction.commit()); - } catch (err) { - handleTransaction && (await transaction.rollback()); - throw err; - } + await traverSchemaTree(newSchema); } async updateNode(uid: string, schema: any, transaction?: Transaction) { @@ -406,65 +451,44 @@ export class UiSchemaRepository extends Repository { await removeLeafNode(uid); } + @transaction() async remove(uid: string, options?: TransactionAble & removeParentOptions) { - let handleTransaction: boolean = true; - let transaction; + let { transaction } = options; - if (options?.transaction) { - transaction = options.transaction; - handleTransaction = false; - } else { - transaction = await this.database.sequelize.transaction(); + if (options?.removeParentsIfNoChildren) { + await this.removeEmptyParents({ transaction, uid, breakRemoveOn: options.breakRemoveOn }); + return; } - try { - if (options?.removeParentsIfNoChildren) { - await this.removeEmptyParents({ transaction, uid, breakRemoveOn: options.breakRemoveOn }); - if (handleTransaction) { - await transaction.commit(); - } - return; - } - - await this.database.sequelize.query( - this.sqlAdapter(`DELETE FROM ${this.uiSchemasTableName} WHERE "x-uid" IN ( + await this.database.sequelize.query( + this.sqlAdapter(`DELETE FROM ${this.uiSchemasTableName} WHERE "x-uid" IN ( SELECT descendant FROM ${this.uiSchemaTreePathTableName} WHERE ancestor = :uid )`), - { - replacements: { - uid, - }, - transaction, + { + replacements: { + uid, }, - ); + transaction, + }, + ); - await this.database.sequelize.query( - ` - DELETE FROM ${this.uiSchemaTreePathTableName} + await this.database.sequelize.query( + ` DELETE FROM ${this.uiSchemaTreePathTableName} WHERE descendant IN ( select descendant FROM (SELECT descendant FROM ${this.uiSchemaTreePathTableName} WHERE ancestor = :uid)as descendantTable) `, - { - replacements: { - uid, - }, - transaction, + { + replacements: { + uid, }, - ); - - if (handleTransaction) { - await transaction.commit(); - } - } catch (err) { - if (handleTransaction) { - await transaction.rollback(); - } - throw err; - } + transaction, + }, + ); } + @transaction() async insertBeside(targetUid: string, schema: any, side: 'before' | 'after', options?: InsertAdjacentOptions) { const targetParent = await this.findParentUid(targetUid); @@ -496,6 +520,7 @@ export class UiSchemaRepository extends Repository { return await this.getJsonSchema(insertedNodes[0].get('x-uid')); } + @transaction() async insertInner(targetUid: string, schema: any, position: 'first' | 'last', options?: InsertAdjacentOptions) { const nodes = UiSchemaRepository.schemaToSingleNodes(schema); const rootNode = nodes[0]; @@ -510,6 +535,7 @@ export class UiSchemaRepository extends Repository { return await this.getJsonSchema(insertedNodes[0].get('x-uid')); } + @transaction() async insertAdjacent( position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', target: string, @@ -519,58 +545,45 @@ export class UiSchemaRepository extends Repository { return await this[`insert${lodash.upperFirst(position)}`](target, schema, options); } + @transaction() async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { return await this.insertInner(targetUid, schema, 'first', options); } + @transaction() async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { return await this.insertInner(targetUid, schema, 'last', options); } + @transaction() async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { return await this.insertBeside(targetUid, schema, 'before', options); } + @transaction() async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { return await this.insertBeside(targetUid, schema, 'after', options); } - async insertNodes(nodes: SchemaNode[], options?) { - let handleTransaction: boolean = true; - let transaction; - - if (options?.transaction) { - transaction = options.transaction; - handleTransaction = false; - } else { - transaction = await this.database.sequelize.transaction(); - } + @transaction() + async insertNodes(nodes: SchemaNode[], options?: TransactionAble) { + const { transaction } = options; const insertedNodes = []; - try { - for (const node of nodes) { - insertedNodes.push( - await this.insertSingleNode(node, { - ...options, - transaction, - }), - ); - } - - if (handleTransaction) { - await transaction.commit(); - } - return insertedNodes; - } catch (err) { - console.log({ err }); - if (handleTransaction) { - await transaction.rollback(); - } - throw err; + for (const node of nodes) { + insertedNodes.push( + await this.insertSingleNode(node, { + ...options, + transaction, + }), + ); } + + return insertedNodes; } + @transaction() async insert(schema: any, options?: TransactionAble) { const nodes = UiSchemaRepository.schemaToSingleNodes(schema); const insertedNodes = await this.insertNodes(nodes, options); @@ -650,20 +663,7 @@ export class UiSchemaRepository extends Repository { // if node is a tree root move tree to new path if (isTree) { - // delete old tree path - await db.sequelize.query( - `DELETE FROM ${treeTable} - WHERE descendant IN (SELECT descendant FROM (SELECT descendant FROM ${treeTable} WHERE ancestor = :uid) as descendantTable ) - AND ancestor IN (SELECT ancestor FROM (SELECT ancestor FROM ${treeTable} WHERE descendant = :uid AND ancestor != descendant) as ancestorTable) - `, - { - type: 'DELETE', - replacements: { - uid, - }, - transaction, - }, - ); + await this.clearAncestor(uid, { transaction }); // insert new tree path await db.sequelize.query(