fix: sync collection field default value (#907)
This commit is contained in:
		
							parent
							
								
									3e6d1a3427
								
							
						
					
					
						commit
						76f5754e20
					
				| @ -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, | ||||
|  | ||||
| @ -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 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(); | ||||
|   }); | ||||
| }); | ||||
| @ -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<void> { | ||||
|   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<void> { | ||||
|       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); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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 }) => { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user