fix: sync collection field default value (#907)
This commit is contained in:
		
							parent
							
								
									3e6d1a3427
								
							
						
					
					
						commit
						76f5754e20
					
				| @ -5,7 +5,7 @@ import { | |||||||
|   ModelIndexesOptions, |   ModelIndexesOptions, | ||||||
|   QueryInterfaceOptions, |   QueryInterfaceOptions, | ||||||
|   SyncOptions, |   SyncOptions, | ||||||
|   Transactionable |   Transactionable, | ||||||
| } from 'sequelize'; | } from 'sequelize'; | ||||||
| import { Collection } from '../collection'; | import { Collection } from '../collection'; | ||||||
| import { Database } from '../database'; | import { Database } from '../database'; | ||||||
| @ -53,7 +53,6 @@ export abstract class Field { | |||||||
|     this.init(); |     this.init(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // TODO
 |  | ||||||
|   async sync(syncOptions: SyncOptions) { |   async sync(syncOptions: SyncOptions) { | ||||||
|     await this.collection.sync({ |     await this.collection.sync({ | ||||||
|       ...syncOptions, |       ...syncOptions, | ||||||
|  | |||||||
| @ -1,9 +1,12 @@ | |||||||
| import { MockServer } from '@nocobase/test'; | import { MockServer } from '@nocobase/test'; | ||||||
| import { createApp } from '..'; | import { createApp } from '..'; | ||||||
|  | import { Collection } from '@nocobase/database'; | ||||||
| 
 | 
 | ||||||
| describe('field defaultValue', () => { | describe('field defaultValue', () => { | ||||||
|   let app: MockServer; |   let app: MockServer; | ||||||
| 
 | 
 | ||||||
|  |   let TestCollection: Collection; | ||||||
|  | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     app = await createApp(); |     app = await createApp(); | ||||||
|     await app |     await app | ||||||
| @ -14,13 +17,9 @@ describe('field defaultValue', () => { | |||||||
|           name: 'test1', |           name: 'test1', | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|   afterEach(async () => { |     TestCollection = app.db.getCollection('test1'); | ||||||
|     await app.destroy(); |  | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|   it('should be updated', async () => { |  | ||||||
|     await app |     await app | ||||||
|       .agent() |       .agent() | ||||||
|       .resource('collections.fields', 'test1') |       .resource('collections.fields', 'test1') | ||||||
| @ -31,6 +30,19 @@ describe('field defaultValue', () => { | |||||||
|           defaultValue: 'abc', |           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(); |     const response1 = await app.agent().resource('test1').create(); | ||||||
|     expect(response1.body.data.field1).toBe('abc'); |     expect(response1.body.data.field1).toBe('abc'); | ||||||
|     await app |     await app | ||||||
| @ -43,7 +55,28 @@ describe('field defaultValue', () => { | |||||||
|           defaultValue: 'cba', |           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(); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
| @ -1,5 +1,5 @@ | |||||||
| import Database, { Field, MagicAttributeModel } from '@nocobase/database'; | import Database, { Collection, Field, MagicAttributeModel } from '@nocobase/database'; | ||||||
| import { SyncOptions, Transactionable, UniqueConstraintError, Utils } from 'sequelize'; | import { SyncOptions, Transactionable, UniqueConstraintError } from 'sequelize'; | ||||||
| 
 | 
 | ||||||
| interface LoadOptions extends Transactionable { | interface LoadOptions extends Transactionable { | ||||||
|   // TODO
 |   // TODO
 | ||||||
| @ -13,16 +13,23 @@ interface MigrateOptions extends SyncOptions, Transactionable { | |||||||
| async function migrate(field: Field, options: MigrateOptions): Promise<void> { | async function migrate(field: Field, options: MigrateOptions): Promise<void> { | ||||||
|   const { unique } = field.options; |   const { unique } = field.options; | ||||||
|   const { model } = field.collection; |   const { model } = field.collection; | ||||||
|  | 
 | ||||||
|   const ukName = `${model.tableName}_${field.name}_uk`; |   const ukName = `${model.tableName}_${field.name}_uk`; | ||||||
|   const queryInterface = model.sequelize.getQueryInterface(); |   const queryInterface = model.sequelize.getQueryInterface(); | ||||||
|   const fieldAttribute = model.rawAttributes[field.name]; |   const fieldAttribute = model.rawAttributes[field.name]; | ||||||
|  | 
 | ||||||
|   // @ts-ignore
 |   // @ts-ignore
 | ||||||
|   const existedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[]; |   const existedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, { | ||||||
|   const constraintBefore = existedConstraints.find(item => item.constraintName === ukName); |     transaction: options.transaction, | ||||||
|  |   })) as any[]; | ||||||
|  | 
 | ||||||
|  |   const constraintBefore = existedConstraints.find((item) => item.constraintName === ukName); | ||||||
|  | 
 | ||||||
|   if (typeof fieldAttribute?.unique !== 'undefined') { |   if (typeof fieldAttribute?.unique !== 'undefined') { | ||||||
|     if (constraintBefore && !unique) { |     if (constraintBefore && !unique) { | ||||||
|       await queryInterface.removeConstraint(model.tableName, ukName, { transaction: options.transaction }); |       await queryInterface.removeConstraint(model.tableName, ukName, { transaction: options.transaction }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     fieldAttribute.unique = Boolean(constraintBefore); |     fieldAttribute.unique = Boolean(constraintBefore); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -33,19 +40,24 @@ async function migrate(field: Field, options: MigrateOptions): Promise<void> { | |||||||
|       type: 'unique', |       type: 'unique', | ||||||
|       fields: [field.name], |       fields: [field.name], | ||||||
|       name: ukName, |       name: ukName, | ||||||
|       transaction: options.transaction |       transaction: options.transaction, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   if (typeof fieldAttribute?.unique !== 'undefined') { |   if (typeof fieldAttribute?.unique !== 'undefined') { | ||||||
|     fieldAttribute.unique = unique; |     fieldAttribute.unique = unique; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // @ts-ignore
 |   // @ts-ignore
 | ||||||
|   const updatedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[]; |   const updatedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, { | ||||||
|   const indexAfter = updatedConstraints.find(item => item.constraintName === ukName); |     transaction: options.transaction, | ||||||
|  |   })) as any[]; | ||||||
|  | 
 | ||||||
|  |   const indexAfter = updatedConstraints.find((item) => item.constraintName === ukName); | ||||||
|  | 
 | ||||||
|   if (unique && !indexAfter) { |   if (unique && !indexAfter) { | ||||||
|     throw new UniqueConstraintError({ |     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)) { |     if (!this.db.hasCollection(collectionName)) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     const collection = this.db.getCollection(collectionName); |     const collection = this.db.getCollection(collectionName); | ||||||
|     const name = this.get('name'); |     const name = this.get('name'); | ||||||
|     if (skipExist && collection.hasField(name)) { |     if (skipExist && collection.hasField(name)) { | ||||||
|       return collection.getField(name); |       return collection.getField(name); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     const options = this.get(); |     const options = this.get(); | ||||||
|  | 
 | ||||||
|     if (options.uiSchemaUid) { |     if (options.uiSchemaUid) { | ||||||
|       const UISchema = this.db.getModel('uiSchemas'); |       const UISchema = this.db.getModel('uiSchemas'); | ||||||
|       const uiSchema = await UISchema.findByPk(options.uiSchemaUid, { |       const uiSchema = await UISchema.findByPk(options.uiSchemaUid, { | ||||||
| @ -74,6 +89,7 @@ export class FieldModel extends MagicAttributeModel { | |||||||
|       }); |       }); | ||||||
|       Object.assign(options, { uiSchema: uiSchema.get() }); |       Object.assign(options, { uiSchema: uiSchema.get() }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     return collection.setField(name, options); |     return collection.setField(name, options); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -81,6 +97,7 @@ export class FieldModel extends MagicAttributeModel { | |||||||
|     const field = await this.load({ |     const field = await this.load({ | ||||||
|       transaction: options.transaction, |       transaction: options.transaction, | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|     if (!field) { |     if (!field) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -98,11 +115,11 @@ export class FieldModel extends MagicAttributeModel { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async remove(options?: any) { |   async remove(options?: any) { | ||||||
|     const collectionName = this.get('collectionName'); |     const collection = this.getFieldCollection(); | ||||||
|     if (!this.db.hasCollection(collectionName)) { |     if (!collection) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const collection = this.db.getCollection(collectionName); | 
 | ||||||
|     const field = collection.getField(this.get('name')); |     const field = collection.getField(this.get('name')); | ||||||
|     if (!field) { |     if (!field) { | ||||||
|       return; |       return; | ||||||
| @ -111,4 +128,42 @@ export class FieldModel extends MagicAttributeModel { | |||||||
|       transaction: options.transaction, |       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, |         database: this.app.db, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|     for (const key in beforeInitOptions) { |     for (const key in beforeInitOptions) { | ||||||
|       if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) { |       if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) { | ||||||
|         const fn = beforeInitOptions[key]; |         const fn = beforeInitOptions[key]; | ||||||
|         this.app.db.on(`fields.${key}.beforeInitOptions`, fn); |         this.app.db.on(`fields.${key}.beforeInitOptions`, fn); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db)); |     this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db)); | ||||||
| 
 | 
 | ||||||
|     this.app.db.on('collections.afterCreateWithAssociations', async (model, { context, transaction }) => { |     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 }) => { |     this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => { | ||||||
|  |       const prevOptions = model.previous('options'); | ||||||
|  |       const currentOptions = model.get('options'); | ||||||
|  | 
 | ||||||
|       if (context) { |       if (context) { | ||||||
|         const { unique: prev } = model.previous('options'); |         const prev = prevOptions['unique']; | ||||||
|         const { unique: next } = model.get('options'); |         const next = currentOptions['unique']; | ||||||
|  | 
 | ||||||
|         if (Boolean(prev) !== Boolean(next)) { |         if (Boolean(prev) !== Boolean(next)) { | ||||||
|           await model.migrate({ transaction }); |           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 }) => { |     this.app.db.on('fields.afterSaveWithAssociations', async (model, { context, transaction }) => { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user