feat: sort in collection fields (#207)
* feat: sort in collection fields * fix: sort should call before hidden * mov: test file * refactor: toJSON with traverseJSON * fix: toJSON test * fix: sortBy with hidden field
This commit is contained in:
		
							parent
							
								
									c28a1e34ec
								
							
						
					
					
						commit
						3be12644ed
					
				
							
								
								
									
										160
									
								
								packages/database/src/__tests__/field-options/sort-by.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								packages/database/src/__tests__/field-options/sort-by.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,160 @@ | |||||||
|  | import { mockDatabase } from '@nocobase/test'; | ||||||
|  | import { Database } from '../../index'; | ||||||
|  | 
 | ||||||
|  | describe('associated field order', () => { | ||||||
|  |   let db: Database; | ||||||
|  | 
 | ||||||
|  |   afterEach(async () => { | ||||||
|  |     await db.close(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     db = mockDatabase(); | ||||||
|  |     await db.clean({ drop: true }); | ||||||
|  | 
 | ||||||
|  |     db.collection({ | ||||||
|  |       name: 'users', | ||||||
|  |       fields: [ | ||||||
|  |         { | ||||||
|  |           type: 'string', | ||||||
|  |           name: 'name', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'hasMany', | ||||||
|  |           name: 'posts', | ||||||
|  |           sortBy: 'title', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'hasMany', | ||||||
|  |           name: 'records', | ||||||
|  |           sortBy: 'count', | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     db.collection({ | ||||||
|  |       name: 'records', | ||||||
|  |       fields: [ | ||||||
|  |         { | ||||||
|  |           type: 'integer', | ||||||
|  |           name: 'count', | ||||||
|  |           hidden: true, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'string', | ||||||
|  |           name: 'name', | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     db.collection({ | ||||||
|  |       name: 'posts', | ||||||
|  |       fields: [ | ||||||
|  |         { | ||||||
|  |           type: 'string', | ||||||
|  |           name: 'title', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'belongsTo', | ||||||
|  |           name: 'user', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           type: 'belongsToMany', | ||||||
|  |           name: 'tags', | ||||||
|  |           sortBy: 'name', | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     db.collection({ | ||||||
|  |       name: 'tags', | ||||||
|  |       fields: [ | ||||||
|  |         { type: 'string', name: 'name' }, | ||||||
|  |         { | ||||||
|  |           type: 'belongsToMany', | ||||||
|  |           name: 'posts', | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }); | ||||||
|  |     await db.sync(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should sort hasMany association', async () => { | ||||||
|  |     await db.getRepository('users').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'u1', | ||||||
|  |         posts: [{ title: 'c' }, { title: 'b' }, { title: 'a' }], | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const u1 = await db.getRepository('users').findOne({ | ||||||
|  |       appends: ['posts'], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const u1Json = u1.toJSON(); | ||||||
|  | 
 | ||||||
|  |     const u1Posts = u1Json['posts']; | ||||||
|  |     expect(u1Posts.map((p) => p['title'])).toEqual(['a', 'b', 'c']); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should sort belongsToMany association', async () => { | ||||||
|  |     await db.getRepository('posts').create({ | ||||||
|  |       values: { | ||||||
|  |         title: 'p1', | ||||||
|  |         tags: [{ name: 'c' }, { name: 'b' }, { name: 'a' }], | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const p1 = await db.getRepository('posts').findOne({ | ||||||
|  |       appends: ['tags'], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const p1JSON = p1.toJSON(); | ||||||
|  | 
 | ||||||
|  |     const p1Tags = p1JSON['tags']; | ||||||
|  |     expect(p1Tags.map((p) => p['name'])).toEqual(['a', 'b', 'c']); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should sort nested associations', async () => { | ||||||
|  |     await db.getRepository('users').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'u1', | ||||||
|  |         posts: [{ title: 'c', tags: [{ name: 'c' }, { name: 'b' }, { name: 'a' }] }, { title: 'b' }, { title: 'a' }], | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const u1 = await db.getRepository('users').findOne({ | ||||||
|  |       appends: ['posts.tags'], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const u1Json = u1.toJSON(); | ||||||
|  |     const u1Posts = u1Json['posts']; | ||||||
|  |     expect(u1Posts.map((p) => p['title'])).toEqual(['a', 'b', 'c']); | ||||||
|  | 
 | ||||||
|  |     const postCTags = u1Posts[2]['tags']; | ||||||
|  |     expect(postCTags.map((p) => p['name'])).toEqual(['a', 'b', 'c']); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should sortBy hidden field', async () => { | ||||||
|  |     await db.getRepository('users').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'u1', | ||||||
|  |         records: [ | ||||||
|  |           { count: 3, name: 'c' }, | ||||||
|  |           { count: 2, name: 'b' }, | ||||||
|  |           { count: 1, name: 'a' }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const u1 = await db.getRepository('users').findOne({ | ||||||
|  |       appends: ['records'], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const u1Json = u1.toJSON(); | ||||||
|  | 
 | ||||||
|  |     const u1Records = u1Json['records']; | ||||||
|  |     expect(u1Records[0].count).toBeUndefined(); | ||||||
|  |     expect(u1Records.map((p) => p['name'])).toEqual(['a', 'b', 'c']); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -1,7 +1,7 @@ | |||||||
| import { omit } from 'lodash'; | import { omit } from 'lodash'; | ||||||
| import { BelongsToManyOptions as SequelizeBelongsToManyOptions } from 'sequelize'; | import { BelongsToManyOptions as SequelizeBelongsToManyOptions } from 'sequelize'; | ||||||
| import { Collection } from '../collection'; | import { Collection } from '../collection'; | ||||||
| import { BaseRelationFieldOptions, RelationField } from './relation-field'; | import { BaseRelationFieldOptions, MultipleRelationFieldOptions, RelationField } from './relation-field'; | ||||||
| 
 | 
 | ||||||
| export class BelongsToManyField extends RelationField { | export class BelongsToManyField extends RelationField { | ||||||
|   get through() { |   get through() { | ||||||
| @ -62,7 +62,7 @@ export class BelongsToManyField extends RelationField { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface BelongsToManyFieldOptions | export interface BelongsToManyFieldOptions | ||||||
|   extends BaseRelationFieldOptions, |   extends MultipleRelationFieldOptions, | ||||||
|     Omit<SequelizeBelongsToManyOptions, 'through'> { |     Omit<SequelizeBelongsToManyOptions, 'through'> { | ||||||
|   type: 'belongsToMany'; |   type: 'belongsToMany'; | ||||||
|   through?: string; |   through?: string; | ||||||
|  | |||||||
| @ -5,9 +5,10 @@ import { | |||||||
|   ForeignKeyOptions, |   ForeignKeyOptions, | ||||||
|   HasManyOptions, |   HasManyOptions, | ||||||
|   HasManyOptions as SequelizeHasManyOptions, |   HasManyOptions as SequelizeHasManyOptions, | ||||||
|   Utils |   Utils, | ||||||
| } from 'sequelize'; | } from 'sequelize'; | ||||||
| import { BaseRelationFieldOptions, RelationField } from './relation-field'; | 
 | ||||||
|  | import { BaseRelationFieldOptions, MultipleRelationFieldOptions, RelationField } from './relation-field'; | ||||||
| 
 | 
 | ||||||
| export interface HasManyFieldOptions extends HasManyOptions { | export interface HasManyFieldOptions extends HasManyOptions { | ||||||
|   /** |   /** | ||||||
| @ -136,7 +137,7 @@ export class HasManyField extends RelationField { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface HasManyFieldOptions extends BaseRelationFieldOptions, SequelizeHasManyOptions { | export interface HasManyFieldOptions extends MultipleRelationFieldOptions, SequelizeHasManyOptions { | ||||||
|   type: 'hasMany'; |   type: 'hasMany'; | ||||||
|   target?: string; |   target?: string; | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,6 +2,10 @@ import { BaseFieldOptions, Field } from './field'; | |||||||
| 
 | 
 | ||||||
| export interface BaseRelationFieldOptions extends BaseFieldOptions {} | export interface BaseRelationFieldOptions extends BaseFieldOptions {} | ||||||
| 
 | 
 | ||||||
|  | export interface MultipleRelationFieldOptions extends BaseRelationFieldOptions { | ||||||
|  |   sortBy?: string | string[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export abstract class RelationField extends Field { | export abstract class RelationField extends Field { | ||||||
|   /** |   /** | ||||||
|    * target relation name |    * target relation name | ||||||
|  | |||||||
| @ -1,62 +1,112 @@ | |||||||
| import { Model as SequelizeModel } from 'sequelize'; | import { Model as SequelizeModel, ModelCtor } from 'sequelize'; | ||||||
| import { Collection } from './collection'; | import { Collection } from './collection'; | ||||||
| import { Database } from './database'; | import { Database } from './database'; | ||||||
|  | import lodash from 'lodash'; | ||||||
|  | import { Field } from './fields'; | ||||||
| 
 | 
 | ||||||
| interface IModel { | interface IModel { | ||||||
|   [key: string]: any; |   [key: string]: any; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | interface JSONTransformerOptions { | ||||||
|  |   model: ModelCtor<any>; | ||||||
|  |   collection: Collection; | ||||||
|  |   db: Database; | ||||||
|  |   key?: string; | ||||||
|  |   field?: Field; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes> | export class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes> | ||||||
|   extends SequelizeModel<TModelAttributes, TCreationAttributes> |   extends SequelizeModel<TModelAttributes, TCreationAttributes> | ||||||
|   implements IModel |   implements IModel | ||||||
| { | { | ||||||
|   public static database: Database; |   public static database: Database; | ||||||
|   public static collection: Collection; |   public static collection: Collection; | ||||||
|   // [key: string]: any;
 |  | ||||||
| 
 |  | ||||||
|   private toJsonWithoutHiddenFields(data, { model, collection }): any { |  | ||||||
|     if (!data) { |  | ||||||
|       return data; |  | ||||||
|     } |  | ||||||
|     if (typeof data.toJSON === 'function') { |  | ||||||
|       data = data.toJSON(); |  | ||||||
|     } |  | ||||||
|     const db = (this.constructor as any).database as Database; |  | ||||||
|     const hidden = []; |  | ||||||
|     collection.forEachField((field) => { |  | ||||||
|       if (field.options.hidden) { |  | ||||||
|         hidden.push(field.options.name); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     const json = {}; |  | ||||||
|     Object.keys(data).forEach((key) => { |  | ||||||
|       if (hidden.includes(key)) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (model.hasAlias(key)) { |  | ||||||
|         const association = model.associations[key]; |  | ||||||
|         const opts = { |  | ||||||
|           model: association.target, |  | ||||||
|           collection: db.getCollection(association.target.name), |  | ||||||
|         }; |  | ||||||
|         if (['HasMany', 'BelongsToMany'].includes(association.associationType)) { |  | ||||||
|           if (Array.isArray(data[key])) { |  | ||||||
|             json[key] = data[key].map((item) => this.toJsonWithoutHiddenFields(item, opts)); |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           json[key] = this.toJsonWithoutHiddenFields(data[key], opts); |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         json[key] = data[key]; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     return json; |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   public toJSON<T extends TModelAttributes>(): T { |   public toJSON<T extends TModelAttributes>(): T { | ||||||
|     return this.toJsonWithoutHiddenFields(super.toJSON(), { |     const handleObj = (obj, options: JSONTransformerOptions) => { | ||||||
|       model: this.constructor, |       const handles = [ | ||||||
|  |         (data) => { | ||||||
|  |           if (data instanceof Model) { | ||||||
|  |             return data.toJSON(); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return data; | ||||||
|  |         }, | ||||||
|  |         this.hiddenObjKey, | ||||||
|  |       ]; | ||||||
|  |       return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), obj); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleArray = (arrayOfObj, options: JSONTransformerOptions) => { | ||||||
|  |       const handles = [this.sortAssociations]; | ||||||
|  |       return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), arrayOfObj); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const opts = { | ||||||
|  |       model: this.constructor as ModelCtor<any>, | ||||||
|       collection: (this.constructor as any).collection, |       collection: (this.constructor as any).collection, | ||||||
|  |       db: (this.constructor as any).database as Database, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const traverseJSON = (data: T, options: JSONTransformerOptions): T => { | ||||||
|  |       const { model, db, collection } = options; | ||||||
|  |       // handle Object
 | ||||||
|  |       data = handleObj(data, options); | ||||||
|  | 
 | ||||||
|  |       const result = {}; | ||||||
|  |       for (const key of Object.keys(data)) { | ||||||
|  |         // @ts-ignore
 | ||||||
|  |         if (model.hasAlias(key)) { | ||||||
|  |           const association = model.associations[key]; | ||||||
|  |           const opts = { | ||||||
|  |             model: association.target, | ||||||
|  |             collection: db.getCollection(association.target.name), | ||||||
|  |             db, | ||||||
|  |             key, | ||||||
|  |             field: collection.getField(key), | ||||||
|  |           }; | ||||||
|  | 
 | ||||||
|  |           if (['HasMany', 'BelongsToMany'].includes(association.associationType)) { | ||||||
|  |             result[key] = handleArray(data[key], opts).map((item) => traverseJSON(item, opts)); | ||||||
|  |           } else { | ||||||
|  |             result[key] = traverseJSON(data[key], opts); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           result[key] = data[key]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return result as T; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return traverseJSON(super.toJSON(), opts); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private hiddenObjKey(obj, options: JSONTransformerOptions) { | ||||||
|  |     const hiddenFields = Array.from(options.collection.fields.values()) | ||||||
|  |       .filter((field) => field.options.hidden) | ||||||
|  |       .map((field) => field.options.name); | ||||||
|  | 
 | ||||||
|  |     return lodash.omit(obj, hiddenFields); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private sortAssociations(data, { field }: JSONTransformerOptions): any { | ||||||
|  |     const sortBy = field.options.sortBy; | ||||||
|  |     return sortBy ? this.sortArray(data, sortBy) : data; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private sortArray(data, sortBy: string | string[]) { | ||||||
|  |     if (!lodash.isArray(sortBy)) { | ||||||
|  |       sortBy = [sortBy]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const orders = sortBy.map((sortItem) => { | ||||||
|  |       const direction = sortItem.startsWith('-') ? 'desc' : 'asc'; | ||||||
|  |       sortItem.replace('-', ''); | ||||||
|  |       return [sortItem, direction]; | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     return lodash.sortBy(data, ...orders); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user