From 76f5754e200a5f847154fd4a31ef64a5664fb9c6 Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Fri, 14 Oct 2022 09:56:16 +0800 Subject: [PATCH] fix: sync collection field default value (#907) --- packages/core/database/src/fields/field.ts | 3 +- ...ultValue.test.ts => default-value.test.ts} | 47 +++++++++-- .../collection-manager/src/models/field.ts | 77 ++++++++++++++++--- .../plugins/collection-manager/src/server.ts | 17 +++- 4 files changed, 122 insertions(+), 22 deletions(-) rename packages/plugins/collection-manager/src/__tests__/field-options/{defaultValue.test.ts => default-value.test.ts} (52%) diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index 029a19e74..bc2c4cf9c 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -5,7 +5,7 @@ import { ModelIndexesOptions, QueryInterfaceOptions, SyncOptions, - Transactionable + Transactionable, } from 'sequelize'; import { Collection } from '../collection'; import { Database } from '../database'; @@ -53,7 +53,6 @@ export abstract class Field { this.init(); } - // TODO async sync(syncOptions: SyncOptions) { await this.collection.sync({ ...syncOptions, diff --git a/packages/plugins/collection-manager/src/__tests__/field-options/defaultValue.test.ts b/packages/plugins/collection-manager/src/__tests__/field-options/default-value.test.ts similarity index 52% rename from packages/plugins/collection-manager/src/__tests__/field-options/defaultValue.test.ts rename to packages/plugins/collection-manager/src/__tests__/field-options/default-value.test.ts index de07448f5..be546ab3f 100644 --- a/packages/plugins/collection-manager/src/__tests__/field-options/defaultValue.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/field-options/default-value.test.ts @@ -1,9 +1,12 @@ import { MockServer } from '@nocobase/test'; import { createApp } from '..'; +import { Collection } from '@nocobase/database'; describe('field defaultValue', () => { let app: MockServer; + let TestCollection: Collection; + beforeEach(async () => { app = await createApp(); await app @@ -14,13 +17,9 @@ describe('field defaultValue', () => { name: 'test1', }, }); - }); - afterEach(async () => { - await app.destroy(); - }); + TestCollection = app.db.getCollection('test1'); - it('should be updated', async () => { await app .agent() .resource('collections.fields', 'test1') @@ -31,6 +30,19 @@ describe('field defaultValue', () => { defaultValue: 'abc', }, }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should be updated', async () => { + const dialect = app.db.sequelize.getDialect(); + + if (dialect != 'postgres') { + return; + } + const response1 = await app.agent().resource('test1').create(); expect(response1.body.data.field1).toBe('abc'); await app @@ -43,7 +55,28 @@ describe('field defaultValue', () => { defaultValue: 'cba', }, }); - const response2 = await app.agent().resource('test1').create(); - expect(response2.body.data.field1).toBe('cba'); + + const response2 = await app.agent().resource('test1').create(); + expect(response2.body.data.field1).toBe('cba'); + + const results = await app.db.sequelize.getQueryInterface().describeTable(TestCollection.model.tableName); + + expect(results.field1.defaultValue).toBe('cba'); + }); + + it('should be cleaned', async () => { + await app + .agent() + .resource('collections.fields', 'test1') + .update({ + filterByTk: 'field1', + values: { + type: 'string', + defaultValue: null, + }, + }); + + const response2 = await app.agent().resource('test1').create(); + expect(response2.body.data.field1).toBeNull(); }); }); diff --git a/packages/plugins/collection-manager/src/models/field.ts b/packages/plugins/collection-manager/src/models/field.ts index 55194ab62..7e344e863 100644 --- a/packages/plugins/collection-manager/src/models/field.ts +++ b/packages/plugins/collection-manager/src/models/field.ts @@ -1,5 +1,5 @@ -import Database, { Field, MagicAttributeModel } from '@nocobase/database'; -import { SyncOptions, Transactionable, UniqueConstraintError, Utils } from 'sequelize'; +import Database, { Collection, Field, MagicAttributeModel } from '@nocobase/database'; +import { SyncOptions, Transactionable, UniqueConstraintError } from 'sequelize'; interface LoadOptions extends Transactionable { // TODO @@ -13,16 +13,23 @@ interface MigrateOptions extends SyncOptions, Transactionable { async function migrate(field: Field, options: MigrateOptions): Promise { const { unique } = field.options; const { model } = field.collection; + const ukName = `${model.tableName}_${field.name}_uk`; const queryInterface = model.sequelize.getQueryInterface(); const fieldAttribute = model.rawAttributes[field.name]; + // @ts-ignore - const existedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[]; - const constraintBefore = existedConstraints.find(item => item.constraintName === ukName); + const existedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, { + transaction: options.transaction, + })) as any[]; + + const constraintBefore = existedConstraints.find((item) => item.constraintName === ukName); + if (typeof fieldAttribute?.unique !== 'undefined') { if (constraintBefore && !unique) { await queryInterface.removeConstraint(model.tableName, ukName, { transaction: options.transaction }); } + fieldAttribute.unique = Boolean(constraintBefore); } @@ -33,19 +40,24 @@ async function migrate(field: Field, options: MigrateOptions): Promise { type: 'unique', fields: [field.name], name: ukName, - transaction: options.transaction + transaction: options.transaction, }); } + if (typeof fieldAttribute?.unique !== 'undefined') { fieldAttribute.unique = unique; } // @ts-ignore - const updatedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[]; - const indexAfter = updatedConstraints.find(item => item.constraintName === ukName); + const updatedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, { + transaction: options.transaction, + })) as any[]; + + const indexAfter = updatedConstraints.find((item) => item.constraintName === ukName); + if (unique && !indexAfter) { throw new UniqueConstraintError({ - fields: { [field.name]: undefined } + fields: { [field.name]: undefined }, }); } } @@ -61,12 +73,15 @@ export class FieldModel extends MagicAttributeModel { if (!this.db.hasCollection(collectionName)) { return; } + const collection = this.db.getCollection(collectionName); const name = this.get('name'); if (skipExist && collection.hasField(name)) { return collection.getField(name); } + const options = this.get(); + if (options.uiSchemaUid) { const UISchema = this.db.getModel('uiSchemas'); const uiSchema = await UISchema.findByPk(options.uiSchemaUid, { @@ -74,6 +89,7 @@ export class FieldModel extends MagicAttributeModel { }); Object.assign(options, { uiSchema: uiSchema.get() }); } + return collection.setField(name, options); } @@ -81,6 +97,7 @@ export class FieldModel extends MagicAttributeModel { const field = await this.load({ transaction: options.transaction, }); + if (!field) { return; } @@ -98,11 +115,11 @@ export class FieldModel extends MagicAttributeModel { } async remove(options?: any) { - const collectionName = this.get('collectionName'); - if (!this.db.hasCollection(collectionName)) { + const collection = this.getFieldCollection(); + if (!collection) { return; } - const collection = this.db.getCollection(collectionName); + const field = collection.getField(this.get('name')); if (!field) { return; @@ -111,4 +128,42 @@ export class FieldModel extends MagicAttributeModel { transaction: options.transaction, }); } + + async syncDefaultValue( + options: Transactionable & { + defaultValue: any; + }, + ) { + const collection = this.getFieldCollection(); + + if (!collection) { + return; + } + + const field = collection.getField(this.get('name')); + + const queryInterface = collection.db.sequelize.getQueryInterface(); + + await queryInterface.changeColumn( + collection.model.tableName, + this.get('name'), + { + type: field.dataType, + defaultValue: options.defaultValue, + }, + { + transaction: options.transaction, + }, + ); + } + + protected getFieldCollection(): Collection | null { + const collectionName = this.get('collectionName'); + + if (!this.db.hasCollection(collectionName)) { + return; + } + + return this.db.getCollection(collectionName); + } } diff --git a/packages/plugins/collection-manager/src/server.ts b/packages/plugins/collection-manager/src/server.ts index 1971373b3..59664d94e 100644 --- a/packages/plugins/collection-manager/src/server.ts +++ b/packages/plugins/collection-manager/src/server.ts @@ -60,12 +60,14 @@ export class CollectionManagerPlugin extends Plugin { database: this.app.db, }); }); + 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.afterCreateWithAssociations', async (model, { context, transaction }) => { @@ -87,13 +89,24 @@ export class CollectionManagerPlugin extends Plugin { }); this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => { + const prevOptions = model.previous('options'); + const currentOptions = model.get('options'); + if (context) { - const { unique: prev } = model.previous('options'); - const { unique: next } = model.get('options'); + const prev = prevOptions['unique']; + const next = currentOptions['unique']; + if (Boolean(prev) !== Boolean(next)) { await model.migrate({ transaction }); } } + + const prevDefaultValue = prevOptions['defaultValue']; + const currentDefaultValue = currentOptions['defaultValue']; + + if (prevDefaultValue != currentDefaultValue) { + await model.syncDefaultValue({ transaction, defaultValue: currentDefaultValue }); + } }); this.app.db.on('fields.afterSaveWithAssociations', async (model, { context, transaction }) => {