From 688387413d17855c6c0804d71c6c40c9edf16b39 Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 15 Nov 2022 20:37:26 +0800 Subject: [PATCH] feat: no recursive update associations (#1091) * feat: update associations * fix(collection-manager): should update uiSchema --- .../update-association-values.test.ts | 232 ++++++++++++++++++ .../src/__tests__/update-associations.test.ts | 7 +- .../src/fields/belongs-to-many-field.ts | 1 + .../core/database/src/update-associations.ts | 16 +- .../src/__tests__/fields/reverseField.test.ts | 111 ++++++++- .../plugins/collection-manager/src/server.ts | 11 +- 6 files changed, 372 insertions(+), 6 deletions(-) create mode 100644 packages/core/database/src/__tests__/update-association-values.test.ts diff --git a/packages/core/database/src/__tests__/update-association-values.test.ts b/packages/core/database/src/__tests__/update-association-values.test.ts new file mode 100644 index 000000000..adfb208ff --- /dev/null +++ b/packages/core/database/src/__tests__/update-association-values.test.ts @@ -0,0 +1,232 @@ +import { Database } from '../database'; +import { mockDatabase } from './'; + +describe('update associations', () => { + let db: Database; + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('hasOne', async () => { + db.collection({ + name: 'a', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'hasOne', + name: 'b', + target: 'b', + }, + ], + }); + db.collection({ + name: 'b', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'hasOne', + name: 'c', + target: 'c', + }, + ], + }); + db.collection({ + name: 'c', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'hasOne', + name: 'd', + target: 'd', + }, + ], + }); + db.collection({ + name: 'd', + fields: [ + { + type: 'string', + name: 'name', + }, + ], + }); + await db.sync(); + const b = await db.getRepository('b').create({ + values: {}, + }); + const c = await db.getRepository('c').create({ + values: {}, + }); + const d = await db.getRepository('d').create({ + values: {}, + }); + await db.getRepository('a').create({ + updateAssociationValues: ['b'], + values: { + name: 'a1', + b: { + id: b.id, + c: { + id: c.id, + d: { + id: d.id, + }, + }, + }, + }, + }); + const d1 = await d.reload(); + expect(d1.cId).toBe(c.id); + + const b2 = await db.getRepository('b').create({ + values: {}, + }); + const c2 = await db.getRepository('c').create({ + values: {}, + }); + const d2 = await db.getRepository('d').create({ + values: {}, + }); + await db.getRepository('a').create({ + values: { + name: 'a1', + b: { + id: b2.id, + c: { + id: c2.id, + d: { + id: d2.id, + }, + }, + }, + }, + }); + const c22 = await c2.reload(); + expect(c22.bId).toBeNull(); + const d22 = await d2.reload(); + expect(d22.cId).toBeNull(); + }); + + it('hasMany', async () => { + db.collection({ + name: 'a', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'hasMany', + name: 'b', + target: 'b', + }, + ], + }); + db.collection({ + name: 'b', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'hasMany', + name: 'c', + target: 'c', + }, + ], + }); + db.collection({ + name: 'c', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'hasMany', + name: 'd', + target: 'd', + }, + ], + }); + db.collection({ + name: 'd', + fields: [ + { + type: 'string', + name: 'name', + }, + ], + }); + + await db.sync(); + const b = await db.getRepository('b').create({ + values: {}, + }); + const c = await db.getRepository('c').create({ + values: {}, + }); + const d = await db.getRepository('d').create({ + values: {}, + }); + await db.getRepository('a').create({ + updateAssociationValues: ['b'], + values: { + name: 'a1', + b: { + id: b.id, + c: { + id: c.id, + d: { + id: d.id, + }, + }, + }, + }, + }); + const d1 = await d.reload(); + expect(d1.cId).toBe(c.id); + const b2 = await db.getRepository('b').create({ + values: {}, + }); + const c2 = await db.getRepository('c').create({ + values: {}, + }); + const d2 = await db.getRepository('d').create({ + values: {}, + }); + await db.getRepository('a').create({ + values: { + name: 'a1', + b: { + id: b2.id, + c: { + id: c2.id, + d: { + id: d2.id, + }, + }, + }, + }, + }); + const c22 = await c2.reload(); + expect(c22.bId).toBeNull(); + const d22 = await d2.reload(); + expect(d22.cId).toBeNull(); + }); +}); diff --git a/packages/core/database/src/__tests__/update-associations.test.ts b/packages/core/database/src/__tests__/update-associations.test.ts index 40d380539..dcba399af 100644 --- a/packages/core/database/src/__tests__/update-associations.test.ts +++ b/packages/core/database/src/__tests__/update-associations.test.ts @@ -83,7 +83,12 @@ describe('update associations', () => { }, }); - expect(post4.toJSON()).toMatchObject({ + const p4 = await db.getRepository('posts').findOne({ + filterByTk: post4.id, + appends: ['user'], + }); + + expect(p4?.toJSON()).toMatchObject({ id: 4, name: 'post4', userId: 1, diff --git a/packages/core/database/src/fields/belongs-to-many-field.ts b/packages/core/database/src/fields/belongs-to-many-field.ts index e493e1f45..a7dac5189 100644 --- a/packages/core/database/src/fields/belongs-to-many-field.ts +++ b/packages/core/database/src/fields/belongs-to-many-field.ts @@ -102,5 +102,6 @@ export interface BelongsToManyFieldOptions extends MultipleRelationFieldOptions, Omit { type: 'belongsToMany'; + target?: string; through?: string; } diff --git a/packages/core/database/src/update-associations.ts b/packages/core/database/src/update-associations.ts index 844b2dde0..910ff89b4 100644 --- a/packages/core/database/src/update-associations.ts +++ b/packages/core/database/src/update-associations.ts @@ -64,6 +64,7 @@ interface UpdateAssociationOptions extends Transactionable, Hookable { sourceModel?: Model; context?: any; associationContext?: any; + recursive?: boolean; } export async function updateModelByValues(instance: Model, values: UpdateValue, options?: UpdateOptions) { @@ -120,6 +121,10 @@ export async function updateAssociations(instance: Model, values: any, options: return; } + if (options?.updateAssociationValues) { + options.recursive = true; + } + let newTransaction = false; let transaction = options.transaction; @@ -262,7 +267,7 @@ export async function updateSingleAssociation( throw new Error(`The value of '${key}' cannot be in array format`); } - const { context, updateAssociationValues = [], transaction } = options; + const { recursive, context, updateAssociationValues = [], transaction } = options; const keys = getKeysByPrefix(updateAssociationValues, key); try { @@ -313,6 +318,10 @@ export async function updateSingleAssociation( if (instance) { await model[setAccessor](instance, { context, transaction }); + if (!recursive) { + return; + } + if (updateAssociationValues.includes(key)) { await instance.update(value, { ...options, transaction }); } @@ -368,7 +377,7 @@ export async function updateMultipleAssociation( return false; } - const { context, updateAssociationValues = [], transaction } = options; + const { recursive, context, updateAssociationValues = [], transaction } = options; const keys = getKeysByPrefix(updateAssociationValues, key); try { @@ -449,6 +458,9 @@ export async function updateMultipleAssociation( const addAccessor = association.accessors.add; await model[addAccessor](item[pk], accessorOptions); + if (!recursive) { + continue; + } if (updateAssociationValues.includes(key)) { await instance.update(item, { ...options, transaction }); } diff --git a/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts index c1cd4571f..bce332781 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts @@ -1,10 +1,10 @@ import Database, { Collection as DBCollection } from '@nocobase/database'; -import Application from '@nocobase/server'; +import { MockServer } from '@nocobase/test'; import { createApp } from '..'; describe('reverseField options', () => { let db: Database; - let app: Application; + let app: MockServer; let Collection: DBCollection; let Field: DBCollection; @@ -144,6 +144,7 @@ describe('reverseField options', () => { await Field.repository.update({ filterByTk: field.get('key') as string, + updateAssociationValues: ['reverseField'], values: { reverseField: { key: reverseField.get('key'), @@ -172,4 +173,110 @@ describe('reverseField options', () => { const uiSchema = reverseField.get('uiSchema'); expect(uiSchema['schema']).toEqual({ title: '123' }); }); + + it('should update uiSchema', async () => { + await app + .agent() + .resource('collections') + .create({ + values: { + name: 'a', + }, + }); + + const f = await app + .agent() + .resource('collections.fields', 'a') + .create({ + values: { + name: 'f_i02fjvduwmv', + interface: 'input', + type: 'string', + uiSchema: { type: 'string', 'x-component': 'Input', title: 'A1' }, + }, + }); + + await app + .agent() + .resource('collections.fields', 'a') + .update({ + filterByTk: 'f_i02fjvduwmv', + values: { + ...f.body.data, + uiSchema: { + ...f.body.data.uiSchema, + title: 'A2', + }, + }, + }); + + const f2 = await app + .agent() + .resource('collections.fields', 'a') + .get({ + filterByTk: 'f_i02fjvduwmv', + appends: ['uiSchema'], + }); + + expect(f2.body.data.uiSchema.title).toBe('A2'); + }); + + it('should create reverseField uiSchema', async () => { + await app + .agent() + .resource('collections') + .create({ + values: { + name: 'a', + }, + }); + + await app + .agent() + .resource('collections') + .create({ + values: { + name: 'b', + }, + }); + + await app + .agent() + .resource('collections.fields', 'a') + .create({ + values: { + foreignKey: 'f_qnt8iaony6i', + onDelete: 'SET NULL', + reverseField: { + uiSchema: { + title: 'A', + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } }, + }, + interface: 'obo', + type: 'belongsTo', + name: 'f_dctw6v5gsio', + }, + name: 'f_d5ebrb4h85m', + type: 'hasOne', + uiSchema: { + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } }, + title: 'B', + }, + interface: 'oho', + target: 'b', + }, + }); + + const f1 = await app + .agent() + .resource('collections.fields', 'b') + .get({ + filterByTk: 'f_dctw6v5gsio', + appends: ['uiSchema'], + }); + + expect(f1.body.data.uiSchema.title).toBe('A'); + }); }); diff --git a/packages/plugins/collection-manager/src/server.ts b/packages/plugins/collection-manager/src/server.ts index efe74d1c6..7a0fd5039 100644 --- a/packages/plugins/collection-manager/src/server.ts +++ b/packages/plugins/collection-manager/src/server.ts @@ -13,7 +13,7 @@ import { beforeCreateForChildrenCollection, beforeCreateForReverseField, beforeDestroyForeignKey, - beforeInitOptions, + beforeInitOptions } from './hooks'; import { CollectionModel, FieldModel } from './models'; @@ -180,6 +180,15 @@ export class CollectionManagerPlugin extends Plugin { return ctx.throw(400, ctx.t(`The value of ${Object.keys(err.fields)} field duplicated`)); }, ); + + this.app.resourcer.use(async (ctx, next) => { + if (ctx.action.resourceName === 'collections.fields' && ['create', 'update'].includes(ctx.action.actionName)) { + ctx.action.mergeParams({ + updateAssociationValues: ['uiSchema', 'reverseField'], + }); + } + await next(); + }); } }