feat(database): handle targetCollection option in repository find (#2175)
* test: return child collection when get with filterByTk * refactor: targetCollection * chore: target collection args in repository find * feat: handle targetCollection option in repository find * feat: get child target at belongs to many association * chore: build * chore: build * chore: test * refactor: targetcollection * test: belongs to association with targetCollection * fix: test * fix: test * fix: test * fix: test * fix: test * feat: update with targetCollection option * feat: inject target collection options at repository update --------- Co-authored-by: katherinehhh <katherine_15995@163.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									eb9ee38a2b
								
							
						
					
					
						commit
						08b2f374c8
					
				| @ -1,3 +1,6 @@ | |||||||
| import { proxyToRepository } from './proxy-to-repository'; | import { proxyToRepository } from './proxy-to-repository'; | ||||||
| 
 | 
 | ||||||
| export const get = proxyToRepository(['filterByTk', 'fields', 'appends', 'except', 'filter'], 'findOne'); | export const get = proxyToRepository( | ||||||
|  |   ['filterByTk', 'fields', 'appends', 'except', 'filter', 'targetCollection'], | ||||||
|  |   'findOne', | ||||||
|  | ); | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { getRepositoryFromParams } from '../utils'; |  | ||||||
| import { Context } from '../index'; |  | ||||||
| import lodash from 'lodash'; | import lodash from 'lodash'; | ||||||
|  | import { Context } from '../index'; | ||||||
|  | import { getRepositoryFromParams } from '../utils'; | ||||||
| 
 | 
 | ||||||
| export function proxyToRepository(paramKeys: string[], repositoryMethod: string) { | export function proxyToRepository(paramKeys: string[], repositoryMethod: string) { | ||||||
|   return async function (ctx: Context, next) { |   return async function (ctx: Context, next) { | ||||||
|  | |||||||
| @ -1,6 +1,15 @@ | |||||||
| import { proxyToRepository } from './proxy-to-repository'; | import { proxyToRepository } from './proxy-to-repository'; | ||||||
| 
 | 
 | ||||||
| export const update = proxyToRepository( | export const update = proxyToRepository( | ||||||
|   ['filterByTk', 'values', 'whitelist', 'blacklist', 'filter', 'updateAssociationValues', 'forceUpdate'], |   [ | ||||||
|  |     'filterByTk', | ||||||
|  |     'values', | ||||||
|  |     'whitelist', | ||||||
|  |     'blacklist', | ||||||
|  |     'filter', | ||||||
|  |     'updateAssociationValues', | ||||||
|  |     'forceUpdate', | ||||||
|  |     'targetCollection', | ||||||
|  |   ], | ||||||
|   'update', |   'update', | ||||||
| ); | ); | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { createForm } from '@formily/core'; | import { createForm } from '@formily/core'; | ||||||
| import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; | import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; | ||||||
| import { isEmpty } from 'lodash'; |  | ||||||
| import { Spin } from 'antd'; | import { Spin } from 'antd'; | ||||||
|  | import { isEmpty } from 'lodash'; | ||||||
| import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; | import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; | ||||||
| import { useCollection } from '../collection-manager'; | import { useCollection } from '../collection-manager'; | ||||||
| import { RecordProvider, useRecord } from '../record-provider'; | import { RecordProvider, useRecord } from '../record-provider'; | ||||||
| @ -82,8 +82,8 @@ export const FormBlockProvider = (props) => { | |||||||
|     (currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name; |     (currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name; | ||||||
|   return ( |   return ( | ||||||
|     (detailFlag || createFlag) && ( |     (detailFlag || createFlag) && ( | ||||||
|       <BlockProvider {...props} block={'form'}> |       <BlockProvider {...props} block={'form'} params={{ ...props?.params, targetCollection: collection }}> | ||||||
|         <InternalFormBlockProvider {...props} /> |         <InternalFormBlockProvider {...props} params={{ ...props?.params, targetCollection: collection }} /> | ||||||
|       </BlockProvider> |       </BlockProvider> | ||||||
|     ) |     ) | ||||||
|   ); |   ); | ||||||
| @ -122,7 +122,6 @@ const RenderChildrenWithDataTemplates = ({ form }) => { | |||||||
|   const { findComponent } = useDesignable(); |   const { findComponent } = useDesignable(); | ||||||
|   const field = useField(); |   const field = useField(); | ||||||
|   const Component = findComponent(field.component?.[0]) || React.Fragment; |   const Component = findComponent(field.component?.[0]) || React.Fragment; | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <Component {...field.componentProps}> |     <Component {...field.componentProps}> | ||||||
|       <DataTemplateSelect style={{ marginBottom: 18 }} form={form} /> |       <DataTemplateSelect style={{ marginBottom: 18 }} form={form} /> | ||||||
|  | |||||||
| @ -486,7 +486,7 @@ describe('belongs to many', () => { | |||||||
| 
 | 
 | ||||||
|     const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id); |     const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id); | ||||||
| 
 | 
 | ||||||
|     const t1 = await PostTagRepository.findOne({ |     let t1 = await PostTagRepository.findOne({ | ||||||
|       filter: { |       filter: { | ||||||
|         name: 't1', |         name: 't1', | ||||||
|       }, |       }, | ||||||
| @ -502,7 +502,7 @@ describe('belongs to many', () => { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const Post2TagRepository = new BelongsToManyRepository(Post, 'tags', p2.id); |     const Post2TagRepository = new BelongsToManyRepository(Post, 'tags', p2.id); | ||||||
|     const p2Tag = await Post2TagRepository.findOne(); |     let p2Tag = await Post2TagRepository.findOne(); | ||||||
|     expect(p2Tag.posts_tags.tagged_at).toBeNull(); |     expect(p2Tag.posts_tags.tagged_at).toBeNull(); | ||||||
| 
 | 
 | ||||||
|     // 设置p1与t1关联的tagged_at
 |     // 设置p1与t1关联的tagged_at
 | ||||||
| @ -517,11 +517,15 @@ describe('belongs to many', () => { | |||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     await t1.reload(); |     t1 = await PostTagRepository.findOne({ | ||||||
|  |       filter: { | ||||||
|  |         name: 't1', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|     expect(t1.posts_tags.tagged_at).toEqual('456'); |     expect(t1.posts_tags.tagged_at).toEqual('456'); | ||||||
| 
 | 
 | ||||||
|     await p2Tag.reload(); |     p2Tag = await Post2TagRepository.findOne(); | ||||||
|     // p2-tag1 still not change
 |     // p2-tag1 still not change
 | ||||||
|     expect(p2Tag.posts_tags.tagged_at).toBeNull(); |     expect(p2Tag.posts_tags.tagged_at).toBeNull(); | ||||||
|   }); |   }); | ||||||
|  | |||||||
| @ -1,18 +1,18 @@ | |||||||
| const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { | const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { | ||||||
|   const oldValue = descriptor.value; |   const oldValue = descriptor.value; | ||||||
| 
 | 
 | ||||||
|   descriptor.value = function () { |   descriptor.value = function (...args: any[]) { | ||||||
|     const options = arguments[0]; |     const options = args[0]; | ||||||
| 
 | 
 | ||||||
|     if (Array.isArray(options.values)) { |     if (Array.isArray(options.values)) { | ||||||
|       return oldValue.apply(this, arguments); |       return oldValue.apply(this, args); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (!options?.filter && !options?.filterByTk && !options?.forceUpdate) { |     if (!options?.filter && !options?.filterByTk && !options?.forceUpdate) { | ||||||
|       throw new Error(`must provide filter or filterByTk for ${propertyKey} call, or set forceUpdate to true`); |       throw new Error(`must provide filter or filterByTk for ${propertyKey} call, or set forceUpdate to true`); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return oldValue.apply(this, arguments); |     return oldValue.apply(this, args); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return descriptor; |   return descriptor; | ||||||
|  | |||||||
| @ -0,0 +1,20 @@ | |||||||
|  | import lodash from 'lodash'; | ||||||
|  | 
 | ||||||
|  | const injectTargetCollection = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { | ||||||
|  |   const oldValue = descriptor.value; | ||||||
|  | 
 | ||||||
|  |   descriptor.value = function (...args: any[]) { | ||||||
|  |     const options = args[0]; | ||||||
|  |     const values = options.values; | ||||||
|  | 
 | ||||||
|  |     if (lodash.isPlainObject(values) && values.__collection) { | ||||||
|  |       options.targetCollection = values.__collection; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return oldValue.apply(this, args); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return descriptor; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default injectTargetCollection; | ||||||
| @ -1,8 +1,8 @@ | |||||||
| import { Association, HasOne, Includeable, Model, ModelStatic, Transaction } from 'sequelize'; |  | ||||||
| import lodash from 'lodash'; | import lodash from 'lodash'; | ||||||
|  | import { Association, HasOne, Includeable, Model, ModelStatic, Op, Transaction } from 'sequelize'; | ||||||
| import Database from '../database'; | import Database from '../database'; | ||||||
| import { OptionsParser } from '../options-parser'; |  | ||||||
| import { appendChildCollectionNameAfterRepositoryFind } from '../listeners/append-child-collection-name-after-repository-find'; | import { appendChildCollectionNameAfterRepositoryFind } from '../listeners/append-child-collection-name-after-repository-find'; | ||||||
|  | import { OptionsParser } from '../options-parser'; | ||||||
| 
 | 
 | ||||||
| interface EagerLoadingNode { | interface EagerLoadingNode { | ||||||
|   model: ModelStatic<any>; |   model: ModelStatic<any>; | ||||||
| @ -13,6 +13,7 @@ interface EagerLoadingNode { | |||||||
|   parent?: EagerLoadingNode; |   parent?: EagerLoadingNode; | ||||||
|   instances?: Array<Model>; |   instances?: Array<Model>; | ||||||
|   order?: any; |   order?: any; | ||||||
|  |   where?: any; | ||||||
|   inspectInheritAttribute?: boolean; |   inspectInheritAttribute?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -89,7 +90,10 @@ export class EagerLoadingTree { | |||||||
|           continue; |           continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const association = eagerLoadingTreeParent.model.associations[include.association]; |         const association = lodash.isString(include.association) | ||||||
|  |           ? eagerLoadingTreeParent.model.associations[include.association] | ||||||
|  |           : include.association; | ||||||
|  | 
 | ||||||
|         const associationType = association.associationType; |         const associationType = association.associationType; | ||||||
| 
 | 
 | ||||||
|         const child = buildNode({ |         const child = buildNode({ | ||||||
| @ -98,6 +102,7 @@ export class EagerLoadingTree { | |||||||
|           rawAttributes: lodash.cloneDeep(include.attributes), |           rawAttributes: lodash.cloneDeep(include.attributes), | ||||||
|           attributes: lodash.cloneDeep(include.attributes), |           attributes: lodash.cloneDeep(include.attributes), | ||||||
|           parent: eagerLoadingTreeParent, |           parent: eagerLoadingTreeParent, | ||||||
|  |           where: include.where, | ||||||
|           children: [], |           children: [], | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
| @ -177,8 +182,15 @@ export class EagerLoadingTree { | |||||||
|           const foreignKey = association.foreignKey; |           const foreignKey = association.foreignKey; | ||||||
|           const foreignKeyValues = node.parent.instances.map((instance) => instance.get(association.sourceKey)); |           const foreignKeyValues = node.parent.instances.map((instance) => instance.get(association.sourceKey)); | ||||||
| 
 | 
 | ||||||
|  |           let where: any = { [foreignKey]: foreignKeyValues }; | ||||||
|  |           if (node.where) { | ||||||
|  |             where = { | ||||||
|  |               [Op.and]: [where, node.where], | ||||||
|  |             }; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|           const findOptions = { |           const findOptions = { | ||||||
|             where: { [foreignKey]: foreignKeyValues }, |             where, | ||||||
|             attributes: node.attributes, |             attributes: node.attributes, | ||||||
|             order: orderOption(association), |             order: orderOption(association), | ||||||
|             transaction, |             transaction, | ||||||
| @ -275,7 +287,8 @@ export class EagerLoadingTree { | |||||||
|               } |               } | ||||||
| 
 | 
 | ||||||
|               if (associationType == 'HasOne') { |               if (associationType == 'HasOne') { | ||||||
|                 parentInstance.setDataValue(association.as, instance); |                 const key = association.options.realAs || association.as; | ||||||
|  |                 parentInstance[key] = parentInstance.dataValues[key] = instance; | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| @ -343,6 +356,11 @@ export class EagerLoadingTree { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       // skip pivot attributes
 | ||||||
|  |       if (node.association?.as == '_pivot_') { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       // if no attributes are specified, return empty fields
 |       // if no attributes are specified, return empty fields
 | ||||||
|       const nodeRawAttributes = node.rawAttributes || []; |       const nodeRawAttributes = node.rawAttributes || []; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { Collection, CollectionContext, CollectionOptions } from './collection'; |  | ||||||
| import { default as lodash } from 'lodash'; | import { default as lodash } from 'lodash'; | ||||||
| import { Field } from '.'; | import { Field } from '.'; | ||||||
|  | import { Collection, CollectionContext, CollectionOptions } from './collection'; | ||||||
| 
 | 
 | ||||||
| export class InheritedCollection extends Collection { | export class InheritedCollection extends Collection { | ||||||
|   parents?: Collection[]; |   parents?: Collection[]; | ||||||
|  | |||||||
| @ -1,11 +1,33 @@ | |||||||
| export const appendChildCollectionNameAfterRepositoryFind = (db) => { | import Database from '../database'; | ||||||
|  | 
 | ||||||
|  | const setRowAttribute = (row, attribute, value, raw) => { | ||||||
|  |   if (raw) { | ||||||
|  |     row[attribute] = value; | ||||||
|  |   } else { | ||||||
|  |     row.set(attribute, value, { raw: true }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const appendChildCollectionNameAfterRepositoryFind = (db: Database) => { | ||||||
|   return ({ findOptions, dataCollection, data }) => { |   return ({ findOptions, dataCollection, data }) => { | ||||||
|  |     if (findOptions.targetCollection) { | ||||||
|  |       const collection = db.getCollection(findOptions.targetCollection); | ||||||
|  | 
 | ||||||
|  |       for (const row of data) { | ||||||
|  |         setRowAttribute(row, '__collection', collection.name, findOptions.raw); | ||||||
|  |         setRowAttribute(row, '__schemaName', collection.collectionSchema(), findOptions.raw); | ||||||
|  |         setRowAttribute(row, '__tableName', collection.model.tableName, findOptions.raw); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (dataCollection.isParent()) { |     if (dataCollection.isParent()) { | ||||||
|       for (const row of data) { |       for (const row of data) { | ||||||
|         if (row.__collection) { |         if (row.__collection) { | ||||||
|           continue; |           continue; | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         const fullTableName = findOptions.raw |         const fullTableName = findOptions.raw | ||||||
|           ? `${row['__schemaName']}.${row['__tableName']}` |           ? `${row['__schemaName']}.${row['__tableName']}` | ||||||
|           : `${row.get('__schemaName')}.${row.get('__tableName')}`; |           : `${row.get('__schemaName')}.${row.get('__tableName')}`; | ||||||
| @ -24,11 +46,7 @@ export const appendChildCollectionNameAfterRepositoryFind = (db) => { | |||||||
| 
 | 
 | ||||||
|         const rowCollectionName = rowCollection.name; |         const rowCollectionName = rowCollection.name; | ||||||
| 
 | 
 | ||||||
|         findOptions.raw |         setRowAttribute(row, '__collection', rowCollectionName, findOptions.raw); | ||||||
|           ? (row['__collection'] = rowCollectionName) |  | ||||||
|           : row.set('__collection', rowCollectionName, { |  | ||||||
|               raw: true, |  | ||||||
|             }); |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import lodash from 'lodash'; | ||||||
| import { FindAttributeOptions, ModelStatic, Op, Sequelize } from 'sequelize'; | import { FindAttributeOptions, ModelStatic, Op, Sequelize } from 'sequelize'; | ||||||
| import { Collection } from './collection'; | import { Collection } from './collection'; | ||||||
| import { Database } from './database'; | import { Database } from './database'; | ||||||
| @ -79,6 +80,14 @@ export class OptionsParser { | |||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (this.options?.include) { | ||||||
|  |       if (!queryParams.include) { | ||||||
|  |         queryParams.include = []; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       queryParams.include.push(...lodash.castArray(this.options.include)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return this.parseSort(this.parseFields(queryParams)); |     return this.parseSort(this.parseFields(queryParams)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,51 +1,14 @@ | |||||||
| import lodash from 'lodash'; | import lodash from 'lodash'; | ||||||
| import { BelongsToMany, Op, Transaction } from 'sequelize'; | import { BelongsToMany, Op, Transaction } from 'sequelize'; | ||||||
| import { Model } from '../model'; | import { AggregateOptions, CreateOptions, DestroyOptions, TargetKey } from '../repository'; | ||||||
| import { |  | ||||||
|   AggregateOptions, |  | ||||||
|   CreateOptions, |  | ||||||
|   DestroyOptions, |  | ||||||
|   FindOneOptions, |  | ||||||
|   FindOptions, |  | ||||||
|   TargetKey, |  | ||||||
|   UpdateOptions, |  | ||||||
| } from '../repository'; |  | ||||||
| import { updateThroughTableValue } from '../update-associations'; | import { updateThroughTableValue } from '../update-associations'; | ||||||
| import { FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository'; | import { MultipleRelationRepository } from './multiple-relation-repository'; | ||||||
| import { transaction } from './relation-repository'; | import { transaction } from './relation-repository'; | ||||||
| import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types'; | import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types'; | ||||||
| 
 | 
 | ||||||
| type CreateBelongsToManyOptions = CreateOptions; | type CreateBelongsToManyOptions = CreateOptions; | ||||||
| 
 | 
 | ||||||
| interface IBelongsToManyRepository<M extends Model> { | export class BelongsToManyRepository extends MultipleRelationRepository { | ||||||
|   find(options?: FindOptions): Promise<M[]>; |  | ||||||
| 
 |  | ||||||
|   findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>; |  | ||||||
| 
 |  | ||||||
|   findOne(options?: FindOneOptions): Promise<M>; |  | ||||||
| 
 |  | ||||||
|   // 新增并关联,存在中间表数据
 |  | ||||||
|   create(options?: CreateOptions): Promise<M>; |  | ||||||
| 
 |  | ||||||
|   // 更新,存在中间表数据
 |  | ||||||
|   update(options?: UpdateOptions): Promise<M>; |  | ||||||
| 
 |  | ||||||
|   // 删除
 |  | ||||||
|   destroy(options?: number | string | number[] | string[] | DestroyOptions): Promise<boolean>; |  | ||||||
| 
 |  | ||||||
|   // 建立关联
 |  | ||||||
|   set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>; |  | ||||||
| 
 |  | ||||||
|   // 附加关联,存在中间表数据
 |  | ||||||
|   add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>; |  | ||||||
| 
 |  | ||||||
|   // 移除关联
 |  | ||||||
|   remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>; |  | ||||||
| 
 |  | ||||||
|   toggle(options: TargetKey | { pk?: TargetKey; transaction?: Transaction }): Promise<void>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class BelongsToManyRepository extends MultipleRelationRepository implements IBelongsToManyRepository<any> { |  | ||||||
|   async aggregate(options: AggregateOptions) { |   async aggregate(options: AggregateOptions) { | ||||||
|     const targetRepository = this.targetCollection.repository; |     const targetRepository = this.targetCollection.repository; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,23 +1,15 @@ | |||||||
| import { Model } from '../model'; | import { BelongsTo } from 'sequelize'; | ||||||
| import { CreateOptions, UpdateOptions } from '../repository'; |  | ||||||
| import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository'; | import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository'; | ||||||
| 
 | 
 | ||||||
| type BelongsToFindOptions = SingleRelationFindOption; | type BelongsToFindOptions = SingleRelationFindOption; | ||||||
| 
 | 
 | ||||||
| interface IBelongsToRepository<M extends Model> { | export class BelongsToRepository extends SingleRelationRepository { | ||||||
|   // 不需要 findOne,find 就是 findOne
 |   async filterOptions(sourceModel) { | ||||||
|   find(options?: BelongsToFindOptions): Promise<M>; |     const association = this.association as BelongsTo; | ||||||
|   findOne(options?: BelongsToFindOptions): Promise<M>; |  | ||||||
|   // 新增并关联,如果存在关联,解除之后,与新数据建立关联
 |  | ||||||
|   create(options?: CreateOptions): Promise<M>; |  | ||||||
|   // 更新
 |  | ||||||
|   update(options?: UpdateOptions): Promise<M>; |  | ||||||
|   // 删除
 |  | ||||||
|   destroy(): Promise<boolean>; |  | ||||||
|   // 建立关联
 |  | ||||||
|   set(primaryKey: any): Promise<void>; |  | ||||||
|   // 移除关联
 |  | ||||||
|   remove(): Promise<void>; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export class BelongsToRepository extends SingleRelationRepository implements IBelongsToRepository<any> {} |     return { | ||||||
|  |       // @ts-ignore
 | ||||||
|  |       [association.targetKey]: sourceModel.get(association.foreignKey), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,38 +1,9 @@ | |||||||
| import { omit } from 'lodash'; | import { omit } from 'lodash'; | ||||||
| import { HasMany, Op } from 'sequelize'; | import { HasMany, Op } from 'sequelize'; | ||||||
| import { Model } from '../model'; | import { AggregateOptions, DestroyOptions, FindOptions, TargetKey, TK } from '../repository'; | ||||||
| import { | import { AssociatedOptions, MultipleRelationRepository } from './multiple-relation-repository'; | ||||||
|   AggregateOptions, |  | ||||||
|   CreateOptions, |  | ||||||
|   DestroyOptions, |  | ||||||
|   FindOneOptions, |  | ||||||
|   FindOptions, |  | ||||||
|   TargetKey, |  | ||||||
|   TK, |  | ||||||
|   UpdateOptions, |  | ||||||
| } from '../repository'; |  | ||||||
| import { AssociatedOptions, FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository'; |  | ||||||
| import { transaction } from './relation-repository'; | import { transaction } from './relation-repository'; | ||||||
| 
 | export class HasManyRepository extends MultipleRelationRepository { | ||||||
| interface IHasManyRepository<M extends Model> { |  | ||||||
|   find(options?: FindOptions): Promise<M>; |  | ||||||
|   findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>; |  | ||||||
|   findOne(options?: FindOneOptions): Promise<M>; |  | ||||||
|   // 新增并关联
 |  | ||||||
|   create(options?: CreateOptions): Promise<M>; |  | ||||||
|   // 更新
 |  | ||||||
|   update(options?: UpdateOptions): Promise<M>; |  | ||||||
|   // 删除
 |  | ||||||
|   destroy(options?: TK | DestroyOptions): Promise<boolean>; |  | ||||||
|   // 建立关联
 |  | ||||||
|   set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>; |  | ||||||
|   // 附加关联
 |  | ||||||
|   add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>; |  | ||||||
|   // 移除关联
 |  | ||||||
|   remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class HasManyRepository extends MultipleRelationRepository implements IHasManyRepository<any> { |  | ||||||
|   async find(options?: FindOptions): Promise<any> { |   async find(options?: FindOptions): Promise<any> { | ||||||
|     const targetRepository = this.targetCollection.repository; |     const targetRepository = this.targetCollection.repository; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,21 +1,13 @@ | |||||||
| import { Model } from '../model'; | import { HasOne } from 'sequelize'; | ||||||
| import { CreateOptions } from '../repository'; | import { SingleRelationRepository } from './single-relation-repository'; | ||||||
| import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository'; |  | ||||||
| 
 | 
 | ||||||
| interface IHasOneRepository<M extends Model> { | export class HasOneRepository extends SingleRelationRepository { | ||||||
|   // 不需要 findOne,find 就是 findOne
 |   filterOptions(sourceModel) { | ||||||
|   find(options?: SingleRelationFindOption): Promise<M>; |     const association = this.association as HasOne; | ||||||
|   findOne(options?: SingleRelationFindOption): Promise<M>; | 
 | ||||||
|   // 新增并关联,如果存在关联,解除之后,与新数据建立关联
 |     return { | ||||||
|   create(options?: CreateOptions): Promise<M>; |       // @ts-ignore
 | ||||||
|   // 更新
 |       [association.foreignKey]: sourceModel.get(association.sourceKey), | ||||||
|   update(options?): Promise<M>; |     }; | ||||||
|   // 删除
 |   } | ||||||
|   destroy(): Promise<boolean>; |  | ||||||
|   // 建立关联
 |  | ||||||
|   set(primaryKey: any): Promise<void>; |  | ||||||
|   // 移除关联
 |  | ||||||
|   remove(): Promise<void>; |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export class HasOneRepository extends SingleRelationRepository implements IHasOneRepository<any> {} |  | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { MultiAssociationAccessors, Sequelize, Transaction, Transactionable } from 'sequelize'; | import { HasOne, MultiAssociationAccessors, Sequelize, Transaction, Transactionable } from 'sequelize'; | ||||||
|  | import injectTargetCollection from '../decorators/target-collection-decorator'; | ||||||
| import { | import { | ||||||
|   CommonFindOptions, |   CommonFindOptions, | ||||||
|   CountOptions, |   CountOptions, | ||||||
| @ -13,7 +14,6 @@ import { | |||||||
| import { updateModelByValues } from '../update-associations'; | import { updateModelByValues } from '../update-associations'; | ||||||
| import { UpdateGuard } from '../update-guard'; | import { UpdateGuard } from '../update-guard'; | ||||||
| import { RelationRepository, transaction } from './relation-repository'; | import { RelationRepository, transaction } from './relation-repository'; | ||||||
| import { EagerLoadingTree } from '../eager-loading/eager-loading-tree'; |  | ||||||
| 
 | 
 | ||||||
| export type FindAndCountOptions = CommonFindOptions; | export type FindAndCountOptions = CommonFindOptions; | ||||||
| 
 | 
 | ||||||
| @ -27,67 +27,31 @@ export abstract class MultipleRelationRepository extends RelationRepository { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async find(options?: FindOptions): Promise<any> { |   async find(options?: FindOptions): Promise<any> { | ||||||
|     const transaction = await this.getTransaction(options); |     const targetRepository = this.targetCollection.repository; | ||||||
| 
 | 
 | ||||||
|     const findOptions = { |     const association = this.association as any; | ||||||
|       ...this.extendFindOptions( | 
 | ||||||
|         this.buildQueryOptions({ |     const oneFromTargetOptions = { | ||||||
|           ...options, |       as: '_pivot_', | ||||||
|         }), |       foreignKey: association.otherKey, | ||||||
|       ), |       sourceKey: association.targetKey, | ||||||
|       subQuery: false, |       realAs: association.through.model.name, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const getAccessor = this.accessors().get; |     const pivotAssoc = new HasOne(association.target, association.through.model, oneFromTargetOptions); | ||||||
|     const sourceModel = await this.getSourceModel(transaction); |  | ||||||
| 
 | 
 | ||||||
|     if (!sourceModel) return []; |     const appendFilter = { | ||||||
|  |       isPivotFilter: true, | ||||||
|  |       association: pivotAssoc, | ||||||
|  |       where: { | ||||||
|  |         [association.foreignKey]: this.sourceKeyValue, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     if (findOptions.include && findOptions.include.length > 0) { |     return targetRepository.find({ | ||||||
|       const ids = ( |       include: [appendFilter], | ||||||
|         await sourceModel[getAccessor]({ |       ...options, | ||||||
|           ...findOptions, |  | ||||||
|           includeIgnoreAttributes: false, |  | ||||||
|           attributes: [this.targetKey()], |  | ||||||
|           group: `${this.targetModel.name}.${this.targetKey()}`, |  | ||||||
|           transaction, |  | ||||||
|         }) |  | ||||||
|       ).map((row) => { |  | ||||||
|         return { row, pk: row.get(this.targetKey()) }; |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       if (ids.length == 0) { |  | ||||||
|         return []; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       const eagerLoadingTree = EagerLoadingTree.buildFromSequelizeOptions({ |  | ||||||
|         model: this.targetModel, |  | ||||||
|         rootAttributes: findOptions.attributes, |  | ||||||
|         includeOption: findOptions.include, |  | ||||||
|         rootOrder: findOptions.order, |  | ||||||
|         db: this.db, |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       await eagerLoadingTree.load( |  | ||||||
|         ids.map((i) => i.pk), |  | ||||||
|         transaction, |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       return eagerLoadingTree.root.instances; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const data = await sourceModel[getAccessor]({ |  | ||||||
|       ...findOptions, |  | ||||||
|       transaction, |  | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     await this.collection.db.emitAsync('afterRepositoryFind', { |  | ||||||
|       findOptions: options, |  | ||||||
|       dataCollection: this.collection, |  | ||||||
|       data, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return data; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> { |   async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> { | ||||||
| @ -162,6 +126,7 @@ export abstract class MultipleRelationRepository extends RelationRepository { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @transaction() |   @transaction() | ||||||
|  |   @injectTargetCollection | ||||||
|   async update(options?: UpdateOptions): Promise<any> { |   async update(options?: UpdateOptions): Promise<any> { | ||||||
|     const transaction = await this.getTransaction(options); |     const transaction = await this.getTransaction(options); | ||||||
| 
 | 
 | ||||||
| @ -180,7 +145,7 @@ export abstract class MultipleRelationRepository extends RelationRepository { | |||||||
|       await updateModelByValues(instance, values, { |       await updateModelByValues(instance, values, { | ||||||
|         ...options, |         ...options, | ||||||
|         sanitized: true, |         sanitized: true, | ||||||
|         sourceModel: this.sourceInstance, |         sourceModel: await this.getSourceModel(transaction), | ||||||
|         transaction, |         transaction, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,16 +1,17 @@ | |||||||
| import lodash from 'lodash'; | import lodash from 'lodash'; | ||||||
| import { SingleAssociationAccessors, Transactionable } from 'sequelize'; | import { SingleAssociationAccessors, Transactionable } from 'sequelize'; | ||||||
|  | import injectTargetCollection from '../decorators/target-collection-decorator'; | ||||||
| import { Model } from '../model'; | import { Model } from '../model'; | ||||||
| import { Appends, Except, Fields, Filter, TargetKey, UpdateOptions } from '../repository'; | import { Appends, Except, Fields, Filter, TargetKey, UpdateOptions } from '../repository'; | ||||||
| import { updateModelByValues } from '../update-associations'; | import { updateModelByValues } from '../update-associations'; | ||||||
| import { RelationRepository, transaction } from './relation-repository'; | import { RelationRepository, transaction } from './relation-repository'; | ||||||
| import { EagerLoadingTree } from '../eager-loading/eager-loading-tree'; |  | ||||||
| 
 | 
 | ||||||
| export interface SingleRelationFindOption extends Transactionable { | export interface SingleRelationFindOption extends Transactionable { | ||||||
|   fields?: Fields; |   fields?: Fields; | ||||||
|   except?: Except; |   except?: Except; | ||||||
|   appends?: Appends; |   appends?: Appends; | ||||||
|   filter?: Filter; |   filter?: Filter; | ||||||
|  |   targetCollection?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface SetOption extends Transactionable { | interface SetOption extends Transactionable { | ||||||
| @ -18,6 +19,8 @@ interface SetOption extends Transactionable { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export abstract class SingleRelationRepository extends RelationRepository { | export abstract class SingleRelationRepository extends RelationRepository { | ||||||
|  |   abstract filterOptions(sourceModel); | ||||||
|  | 
 | ||||||
|   @transaction() |   @transaction() | ||||||
|   async remove(options?: Transactionable): Promise<void> { |   async remove(options?: Transactionable): Promise<void> { | ||||||
|     const transaction = await this.getTransaction(options); |     const transaction = await this.getTransaction(options); | ||||||
| @ -45,44 +48,22 @@ export abstract class SingleRelationRepository extends RelationRepository { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async find(options?: SingleRelationFindOption): Promise<any> { |   async find(options?: SingleRelationFindOption): Promise<any> { | ||||||
|     const transaction = await this.getTransaction(options); |     const targetRepository = this.targetCollection.repository; | ||||||
| 
 | 
 | ||||||
|     const findOptions = this.buildQueryOptions({ |     const sourceModel = await this.getSourceModel(await this.getTransaction(options)); | ||||||
|       ...options, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const getAccessor = this.accessors().get; |  | ||||||
|     const sourceModel = await this.getSourceModel(transaction); |  | ||||||
| 
 | 
 | ||||||
|     if (!sourceModel) return null; |     if (!sourceModel) return null; | ||||||
| 
 | 
 | ||||||
|     if (findOptions?.include?.length > 0) { |     const addFilter = await this.filterOptions(sourceModel); | ||||||
|       const templateModel = await sourceModel[getAccessor]({ |  | ||||||
|         ...findOptions, |  | ||||||
|         includeIgnoreAttributes: false, |  | ||||||
|         transaction, |  | ||||||
|         attributes: [this.targetModel.primaryKeyAttribute], |  | ||||||
|         group: `${this.targetModel.name}.${this.targetModel.primaryKeyAttribute}`, |  | ||||||
|       }); |  | ||||||
| 
 | 
 | ||||||
|       if (!templateModel) return null; |     const findOptions = { | ||||||
|  |       ...options, | ||||||
|  |       filter: { | ||||||
|  |         $and: [options?.filter || {}, addFilter], | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|       const eagerLoadingTree = EagerLoadingTree.buildFromSequelizeOptions({ |     return await targetRepository.findOne(findOptions); | ||||||
|         model: this.targetModel, |  | ||||||
|         rootAttributes: findOptions.attributes, |  | ||||||
|         includeOption: findOptions.include, |  | ||||||
|         db: this.db, |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       await eagerLoadingTree.load([templateModel.get(this.targetModel.primaryKeyAttribute)], transaction); |  | ||||||
| 
 |  | ||||||
|       return eagerLoadingTree.root.instances[0]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return await sourceModel[getAccessor]({ |  | ||||||
|       ...findOptions, |  | ||||||
|       transaction, |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async findOne(options?: SingleRelationFindOption): Promise<Model<any>> { |   async findOne(options?: SingleRelationFindOption): Promise<Model<any>> { | ||||||
| @ -105,11 +86,13 @@ export abstract class SingleRelationRepository extends RelationRepository { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @transaction() |   @transaction() | ||||||
|  |   @injectTargetCollection | ||||||
|   async update(options: UpdateOptions): Promise<any> { |   async update(options: UpdateOptions): Promise<any> { | ||||||
|     const transaction = await this.getTransaction(options); |     const transaction = await this.getTransaction(options); | ||||||
| 
 | 
 | ||||||
|     const target = await this.find({ |     const target = await this.find({ | ||||||
|       transaction, |       transaction, | ||||||
|  |       targetCollection: options.targetCollection, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (!target) { |     if (!target) { | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ import { | |||||||
| import { Collection } from './collection'; | import { Collection } from './collection'; | ||||||
| import { Database } from './database'; | import { Database } from './database'; | ||||||
| import mustHaveFilter from './decorators/must-have-filter-decorator'; | import mustHaveFilter from './decorators/must-have-filter-decorator'; | ||||||
|  | import injectTargetCollection from './decorators/target-collection-decorator'; | ||||||
| import { transactionWrapperBuilder } from './decorators/transaction-decorator'; | import { transactionWrapperBuilder } from './decorators/transaction-decorator'; | ||||||
| import { EagerLoadingTree } from './eager-loading/eager-loading-tree'; | import { EagerLoadingTree } from './eager-loading/eager-loading-tree'; | ||||||
| import { ArrayFieldRepository } from './field-repository/array-field-repository'; | import { ArrayFieldRepository } from './field-repository/array-field-repository'; | ||||||
| @ -97,6 +98,7 @@ export type CountOptions = Omit<SequelizeCountOptions, 'distinct' | 'where' | 'i | |||||||
| 
 | 
 | ||||||
| export interface FilterByTk { | export interface FilterByTk { | ||||||
|   filterByTk?: TargetKey; |   filterByTk?: TargetKey; | ||||||
|  |   targetCollection?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type FindOptions = SequelizeFindOptions & CommonFindOptions & FilterByTk; | export type FindOptions = SequelizeFindOptions & CommonFindOptions & FilterByTk; | ||||||
| @ -111,7 +113,9 @@ export interface CommonFindOptions extends Transactionable { | |||||||
|   tree?: boolean; |   tree?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type FindOneOptions = Omit<FindOptions, 'limit'>; | export type FindOneOptions = Omit<FindOptions, 'limit'> & { | ||||||
|  |   targetCollection?: string; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export interface DestroyOptions extends SequelizeDestroyOptions { | export interface DestroyOptions extends SequelizeDestroyOptions { | ||||||
|   filter?: Filter; |   filter?: Filter; | ||||||
| @ -137,6 +141,7 @@ export interface UpdateOptions extends Omit<SequelizeUpdateOptions, 'where'> { | |||||||
|   whitelist?: WhiteList; |   whitelist?: WhiteList; | ||||||
|   blacklist?: BlackList; |   blacklist?: BlackList; | ||||||
|   updateAssociationValues?: AssociationKeysToBeUpdate; |   updateAssociationValues?: AssociationKeysToBeUpdate; | ||||||
|  |   targetCollection?: string; | ||||||
|   context?: any; |   context?: any; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -375,6 +380,10 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e | |||||||
|    * @param options |    * @param options | ||||||
|    */ |    */ | ||||||
|   async find(options: FindOptions = {}) { |   async find(options: FindOptions = {}) { | ||||||
|  |     if (options?.targetCollection && options?.targetCollection !== this.collection.name) { | ||||||
|  |       return await this.database.getCollection(options.targetCollection).repository.find(options); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const model = this.collection.model; |     const model = this.collection.model; | ||||||
|     const transaction = await this.getTransaction(options); |     const transaction = await this.getTransaction(options); | ||||||
| 
 | 
 | ||||||
| @ -593,6 +602,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e | |||||||
|    */ |    */ | ||||||
|   @transaction() |   @transaction() | ||||||
|   @mustHaveFilter() |   @mustHaveFilter() | ||||||
|  |   @injectTargetCollection | ||||||
|   async update(options: UpdateOptions & { forceUpdate?: boolean }) { |   async update(options: UpdateOptions & { forceUpdate?: boolean }) { | ||||||
|     if (Array.isArray(options.values)) { |     if (Array.isArray(options.values)) { | ||||||
|       return this.updateMany({ |       return this.updateMany({ | ||||||
|  | |||||||
| @ -272,89 +272,85 @@ export async function updateSingleAssociation( | |||||||
|   const { recursive, context, updateAssociationValues = [], transaction } = options; |   const { recursive, context, updateAssociationValues = [], transaction } = options; | ||||||
|   const keys = getKeysByPrefix(updateAssociationValues, key); |   const keys = getKeysByPrefix(updateAssociationValues, key); | ||||||
| 
 | 
 | ||||||
|   try { |   // set method of association
 | ||||||
|     // set method of association
 |   const setAccessor = association.accessors.set; | ||||||
|     const setAccessor = association.accessors.set; |  | ||||||
| 
 | 
 | ||||||
|     const removeAssociation = async () => { |   const removeAssociation = async () => { | ||||||
|       await model[setAccessor](null, { transaction }); |     await model[setAccessor](null, { transaction }); | ||||||
|       model.setDataValue(key, null); |     model.setDataValue(key, null); | ||||||
|       return true; |     return true; | ||||||
|     }; |   }; | ||||||
| 
 | 
 | ||||||
|     if (isUndefinedOrNull(value)) { |   if (isUndefinedOrNull(value)) { | ||||||
|       return await removeAssociation(); |     return await removeAssociation(); | ||||||
|     } |   } | ||||||
| 
 | 
 | ||||||
|     if (isStringOrNumber(value)) { |   if (isStringOrNumber(value)) { | ||||||
|       await model[setAccessor](value, { context, transaction }); |     await model[setAccessor](value, { context, transaction }); | ||||||
|       return true; |     return true; | ||||||
|     } |   } | ||||||
| 
 | 
 | ||||||
|     if (value instanceof Model) { |   if (value instanceof Model) { | ||||||
|       await model[setAccessor](value, { context, transaction }); |     await model[setAccessor](value, { context, transaction }); | ||||||
|       model.setDataValue(key, value); |     model.setDataValue(key, value); | ||||||
|       return true; |     return true; | ||||||
|     } |   } | ||||||
| 
 | 
 | ||||||
|     const createAccessor = association.accessors.create; |   const createAccessor = association.accessors.create; | ||||||
|     let dataKey: string; |   let dataKey: string; | ||||||
|     let M: ModelStatic<Model>; |   let M: ModelStatic<Model>; | ||||||
|     if (association.associationType === 'BelongsTo') { |   if (association.associationType === 'BelongsTo') { | ||||||
|       M = association.target as ModelStatic<Model>; |     M = association.target as ModelStatic<Model>; | ||||||
|       // @ts-ignore
 |     // @ts-ignore
 | ||||||
|       dataKey = association.targetKey; |     dataKey = association.targetKey; | ||||||
|     } else { |   } else { | ||||||
|       M = association.target as ModelStatic<Model>; |     M = association.target as ModelStatic<Model>; | ||||||
|       dataKey = M.primaryKeyAttribute; |     dataKey = M.primaryKeyAttribute; | ||||||
|     } |   } | ||||||
| 
 | 
 | ||||||
|     if (isStringOrNumber(value[dataKey])) { |   if (isStringOrNumber(value[dataKey])) { | ||||||
|       const instance: any = await M.findOne({ |     const instance: any = await M.findOne({ | ||||||
|         where: { |       where: { | ||||||
|           [dataKey]: value[dataKey], |         [dataKey]: value[dataKey], | ||||||
|         }, |       }, | ||||||
|         transaction, |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       if (instance) { |  | ||||||
|         await model[setAccessor](instance, { context, transaction }); |  | ||||||
| 
 |  | ||||||
|         if (!recursive) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (updateAssociationValues.includes(key)) { |  | ||||||
|           await instance.update(value, { ...options, transaction }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         await updateAssociations(instance, value, { |  | ||||||
|           ...options, |  | ||||||
|           transaction, |  | ||||||
|           associationContext: association, |  | ||||||
|           updateAssociationValues: keys, |  | ||||||
|         }); |  | ||||||
|         model.setDataValue(key, instance); |  | ||||||
|         return true; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const instance = await model[createAccessor](value, { context, transaction }); |  | ||||||
| 
 |  | ||||||
|     await updateAssociations(instance, value, { |  | ||||||
|       ...options, |  | ||||||
|       transaction, |       transaction, | ||||||
|       associationContext: association, |  | ||||||
|       updateAssociationValues: keys, |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     model.setDataValue(key, instance); |     if (instance) { | ||||||
|     // @ts-ignore
 |       await model[setAccessor](instance, { context, transaction }); | ||||||
|     if (association.targetKey) { | 
 | ||||||
|       model.setDataValue(association.foreignKey, instance[dataKey]); |       if (!recursive) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (updateAssociationValues.includes(key)) { | ||||||
|  |         await instance.update(value, { ...options, transaction }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       await updateAssociations(instance, value, { | ||||||
|  |         ...options, | ||||||
|  |         transaction, | ||||||
|  |         associationContext: association, | ||||||
|  |         updateAssociationValues: keys, | ||||||
|  |       }); | ||||||
|  |       model.setDataValue(key, instance); | ||||||
|  |       return true; | ||||||
|     } |     } | ||||||
|   } catch (error) { |   } | ||||||
|     throw error; | 
 | ||||||
|  |   const instance = await model[createAccessor](value, { context, transaction }); | ||||||
|  | 
 | ||||||
|  |   await updateAssociations(instance, value, { | ||||||
|  |     ...options, | ||||||
|  |     transaction, | ||||||
|  |     associationContext: association, | ||||||
|  |     updateAssociationValues: keys, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   model.setDataValue(key, instance); | ||||||
|  |   // @ts-ignore
 | ||||||
|  |   if (association.targetKey) { | ||||||
|  |     model.setDataValue(association.foreignKey, instance[dataKey]); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -389,112 +385,108 @@ export async function updateMultipleAssociation( | |||||||
|   const { recursive, context, updateAssociationValues = [], transaction } = options; |   const { recursive, context, updateAssociationValues = [], transaction } = options; | ||||||
|   const keys = getKeysByPrefix(updateAssociationValues, key); |   const keys = getKeysByPrefix(updateAssociationValues, key); | ||||||
| 
 | 
 | ||||||
|   try { |   const setAccessor = association.accessors.set; | ||||||
|     const setAccessor = association.accessors.set; |  | ||||||
| 
 | 
 | ||||||
|     const createAccessor = association.accessors.create; |   const createAccessor = association.accessors.create; | ||||||
| 
 | 
 | ||||||
|     if (isUndefinedOrNull(value)) { |   if (isUndefinedOrNull(value)) { | ||||||
|       await model[setAccessor](null, { transaction, context }); |     await model[setAccessor](null, { transaction, context }); | ||||||
|       model.setDataValue(key, null); |     model.setDataValue(key, null); | ||||||
|       return; |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (isStringOrNumber(value)) { | ||||||
|  |     await model[setAccessor](value, { transaction, context, individualHooks: true }); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   value = lodash.castArray(value); | ||||||
|  | 
 | ||||||
|  |   const setItems = []; // to be setted
 | ||||||
|  |   const objectItems = []; // to be added
 | ||||||
|  | 
 | ||||||
|  |   // iterate item in value
 | ||||||
|  |   for (const item of value) { | ||||||
|  |     if (isUndefinedOrNull(item)) { | ||||||
|  |       continue; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (isStringOrNumber(value)) { |     if (isStringOrNumber(item)) { | ||||||
|       await model[setAccessor](value, { transaction, context, individualHooks: true }); |       setItems.push(item); | ||||||
|       return; |     } else if (item instanceof Model) { | ||||||
|  |       setItems.push(item); | ||||||
|  |     } else if (item.sequelize) { | ||||||
|  |       setItems.push(item); | ||||||
|  |     } else if (typeof item === 'object') { | ||||||
|  |       const targetKey = (association as any).targetKey || 'id'; | ||||||
|  | 
 | ||||||
|  |       if (item[targetKey]) { | ||||||
|  |         setItems.push(item[targetKey]); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       objectItems.push(item); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // associate targets in lists1
 | ||||||
|  |   await model[setAccessor](setItems, { transaction, context, individualHooks: true }); | ||||||
|  | 
 | ||||||
|  |   const newItems = []; | ||||||
|  | 
 | ||||||
|  |   for (const item of objectItems) { | ||||||
|  |     const pk = association.target.primaryKeyAttribute; | ||||||
|  | 
 | ||||||
|  |     const through = (<any>association).through ? (<any>association).through.model.name : null; | ||||||
|  | 
 | ||||||
|  |     const accessorOptions = { | ||||||
|  |       context, | ||||||
|  |       transaction, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const throughValue = item[through]; | ||||||
|  | 
 | ||||||
|  |     if (throughValue) { | ||||||
|  |       accessorOptions['through'] = throughValue; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     value = lodash.castArray(value); |     if (isUndefinedOrNull(item[pk])) { | ||||||
|  |       // create new record
 | ||||||
|  |       const instance = await model[createAccessor](item, accessorOptions); | ||||||
| 
 | 
 | ||||||
|     const setItems = []; // to be setted
 |       await updateAssociations(instance, item, { | ||||||
|     const objectItems = []; // to be added
 |         ...options, | ||||||
| 
 |         transaction, | ||||||
|     // iterate item in value
 |         associationContext: association, | ||||||
|     for (const item of value) { |         updateAssociationValues: keys, | ||||||
|       if (isUndefinedOrNull(item)) { |       }); | ||||||
|  |       newItems.push(instance); | ||||||
|  |     } else { | ||||||
|  |       // set & update record
 | ||||||
|  |       const instance = await association.target.findByPk<any>(item[pk], { | ||||||
|  |         transaction, | ||||||
|  |       }); | ||||||
|  |       if (!instance) { | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|  |       const addAccessor = association.accessors.add; | ||||||
| 
 | 
 | ||||||
|       if (isStringOrNumber(item)) { |       await model[addAccessor](item[pk], accessorOptions); | ||||||
|         setItems.push(item); |  | ||||||
|       } else if (item instanceof Model) { |  | ||||||
|         setItems.push(item); |  | ||||||
|       } else if (item.sequelize) { |  | ||||||
|         setItems.push(item); |  | ||||||
|       } else if (typeof item === 'object') { |  | ||||||
|         const targetKey = (association as any).targetKey || 'id'; |  | ||||||
| 
 | 
 | ||||||
|         if (item[targetKey]) { |       if (!recursive) { | ||||||
|           setItems.push(item[targetKey]); |         continue; | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         objectItems.push(item); |  | ||||||
|       } |       } | ||||||
|     } |       if (updateAssociationValues.includes(key)) { | ||||||
| 
 |         await instance.update(item, { ...options, transaction }); | ||||||
|     // associate targets in lists1
 |       } | ||||||
|     await model[setAccessor](setItems, { transaction, context, individualHooks: true }); |       await updateAssociations(instance, item, { | ||||||
| 
 |         ...options, | ||||||
|     const newItems = []; |  | ||||||
| 
 |  | ||||||
|     for (const item of objectItems) { |  | ||||||
|       const pk = association.target.primaryKeyAttribute; |  | ||||||
| 
 |  | ||||||
|       const through = (<any>association).through ? (<any>association).through.model.name : null; |  | ||||||
| 
 |  | ||||||
|       const accessorOptions = { |  | ||||||
|         context, |  | ||||||
|         transaction, |         transaction, | ||||||
|       }; |         associationContext: association, | ||||||
| 
 |         updateAssociationValues: keys, | ||||||
|       const throughValue = item[through]; |       }); | ||||||
| 
 |       newItems.push(instance); | ||||||
|       if (throughValue) { |  | ||||||
|         accessorOptions['through'] = throughValue; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (isUndefinedOrNull(item[pk])) { |  | ||||||
|         // create new record
 |  | ||||||
|         const instance = await model[createAccessor](item, accessorOptions); |  | ||||||
| 
 |  | ||||||
|         await updateAssociations(instance, item, { |  | ||||||
|           ...options, |  | ||||||
|           transaction, |  | ||||||
|           associationContext: association, |  | ||||||
|           updateAssociationValues: keys, |  | ||||||
|         }); |  | ||||||
|         newItems.push(instance); |  | ||||||
|       } else { |  | ||||||
|         // set & update record
 |  | ||||||
|         const instance = await association.target.findByPk<any>(item[pk], { |  | ||||||
|           transaction, |  | ||||||
|         }); |  | ||||||
|         if (!instance) { |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
|         const addAccessor = association.accessors.add; |  | ||||||
| 
 |  | ||||||
|         await model[addAccessor](item[pk], accessorOptions); |  | ||||||
| 
 |  | ||||||
|         if (!recursive) { |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
|         if (updateAssociationValues.includes(key)) { |  | ||||||
|           await instance.update(item, { ...options, transaction }); |  | ||||||
|         } |  | ||||||
|         await updateAssociations(instance, item, { |  | ||||||
|           ...options, |  | ||||||
|           transaction, |  | ||||||
|           associationContext: association, |  | ||||||
|           updateAssociationValues: keys, |  | ||||||
|         }); |  | ||||||
|         newItems.push(instance); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     model.setDataValue(key, setItems.concat(newItems)); |  | ||||||
|   } catch (error) { |  | ||||||
|     throw error; |  | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   model.setDataValue(key, setItems.concat(newItems)); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,12 @@ | |||||||
| import Database, { Repository } from '@nocobase/database'; | import Database, { | ||||||
|  |   BelongsToManyRepository, | ||||||
|  |   BelongsToRepository, | ||||||
|  |   HasManyRepository, | ||||||
|  |   Repository, | ||||||
|  | } from '@nocobase/database'; | ||||||
| import Application from '@nocobase/server'; | import Application from '@nocobase/server'; | ||||||
| import { createApp } from '..'; |  | ||||||
| import { pgOnly } from '@nocobase/test'; | import { pgOnly } from '@nocobase/test'; | ||||||
|  | import { createApp } from '..'; | ||||||
| 
 | 
 | ||||||
| pgOnly()('Inherited Collection', () => { | pgOnly()('Inherited Collection', () => { | ||||||
|   let db: Database; |   let db: Database; | ||||||
| @ -12,7 +17,11 @@ pgOnly()('Inherited Collection', () => { | |||||||
|   let fieldsRepository: Repository; |   let fieldsRepository: Repository; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     app = await createApp(); |     app = await createApp({ | ||||||
|  |       database: { | ||||||
|  |         prefix: '', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|     db = app.db; |     db = app.db; | ||||||
| 
 | 
 | ||||||
| @ -24,6 +33,237 @@ pgOnly()('Inherited Collection', () => { | |||||||
|     await app.destroy(); |     await app.destroy(); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   it('should return child model at get action in belongsTo', async () => { | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'parent', | ||||||
|  |         fields: [ | ||||||
|  |           { | ||||||
|  |             type: 'string', | ||||||
|  |             name: 'name', | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'child', | ||||||
|  |         fields: [{ type: 'string', name: 'childName' }], | ||||||
|  |         inherits: ['parent'], | ||||||
|  |       }, | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'users', | ||||||
|  |         fields: [ | ||||||
|  |           { type: 'string', name: 'name' }, | ||||||
|  |           { type: 'belongsTo', name: 'assoc', target: 'parent' }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child1 = await db.getRepository('child').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'child1', | ||||||
|  |         childName: 'child1', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const parent1 = await db.getRepository('parent').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'parent1', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const user1 = await db.getRepository('users').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'user1', | ||||||
|  |         assoc: { id: child1.id }, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child1ViaObject1 = await db.getRepository<BelongsToRepository>('users.assoc', user1.get('id')).findOne({ | ||||||
|  |       targetCollection: 'child', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(child1ViaObject1.get('childName')).toBe('child1'); | ||||||
|  |     expect(child1ViaObject1.get('__collection')).toBe('child'); | ||||||
|  | 
 | ||||||
|  |     await db.getRepository<BelongsToRepository>('users.assoc', user1.get('id')).update({ | ||||||
|  |       values: { | ||||||
|  |         childName: 'child2', | ||||||
|  |         __collection: 'child', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child2 = await db.getRepository<BelongsToRepository>('users.assoc', user1.get('id')).findOne({ | ||||||
|  |       targetCollection: 'child', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(child2.get('childName')).toBe('child2'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should return child model at get action in belongsToMany', async () => { | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'parent', | ||||||
|  |         fields: [ | ||||||
|  |           { | ||||||
|  |             type: 'string', | ||||||
|  |             name: 'name', | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'child', | ||||||
|  |         fields: [{ type: 'string', name: 'childName' }], | ||||||
|  |         inherits: ['parent'], | ||||||
|  |       }, | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'object', | ||||||
|  |         fields: [ | ||||||
|  |           { type: 'string', name: 'name' }, | ||||||
|  |           { type: 'belongsToMany', name: 'assocs', target: 'parent' }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  | 
 | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child1 = await db.getRepository('child').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'child1', | ||||||
|  |         childName: 'child1', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const parent1 = await db.getRepository('parent').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'parent1', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const object1 = await db.getRepository('object').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'object1', | ||||||
|  |         assocs: [{ id: parent1.id }, { id: child1.id }], | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child1ViaObject1 = await db.getRepository<BelongsToManyRepository>('object.assocs', object1.id).findOne({ | ||||||
|  |       filterByTk: child1.get('id'), | ||||||
|  |       targetCollection: 'child', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(child1ViaObject1.get('childName')).toBe('child1'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should return child model at get action in hasMany', async () => { | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'parent', | ||||||
|  |         fields: [ | ||||||
|  |           { | ||||||
|  |             type: 'string', | ||||||
|  |             name: 'name', | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'child', | ||||||
|  |         fields: [{ type: 'string', name: 'childName' }], | ||||||
|  |         inherits: ['parent'], | ||||||
|  |       }, | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await collectionRepository.create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'object', | ||||||
|  |         fields: [ | ||||||
|  |           { type: 'string', name: 'name' }, | ||||||
|  |           { type: 'hasMany', name: 'assocs', target: 'parent', foreignKey: 'object_id' }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  | 
 | ||||||
|  |       context: {}, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child1 = await db.getRepository('child').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'child1', | ||||||
|  |         childName: 'child1', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const parent1 = await db.getRepository('parent').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'parent1', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const object1 = await db.getRepository('object').create({ | ||||||
|  |       values: { | ||||||
|  |         name: 'object1', | ||||||
|  |         assocs: [{ id: parent1.id }, { id: child1.id }], | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child1ViaObject1 = await db.getRepository<HasManyRepository>('object.assocs', object1.id).findOne({ | ||||||
|  |       filterByTk: child1.get('id'), | ||||||
|  |       targetCollection: 'child', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(child1ViaObject1.get('childName')).toBe('child1'); | ||||||
|  | 
 | ||||||
|  |     await db.getRepository<HasManyRepository>('object.assocs', object1.id).update({ | ||||||
|  |       filterByTk: child1.get('id'), | ||||||
|  |       targetCollection: 'child', | ||||||
|  |       values: { | ||||||
|  |         childName: 'child2', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child2 = await db.getRepository<HasManyRepository>('object.assocs', object1.id).findOne({ | ||||||
|  |       filterByTk: child1.get('id'), | ||||||
|  |       targetCollection: 'child', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(child2.get('childName')).toBe('child2'); | ||||||
|  | 
 | ||||||
|  |     await db.getRepository<HasManyRepository>('object.assocs', object1.id).update({ | ||||||
|  |       filterByTk: child1.get('id'), | ||||||
|  |       values: { | ||||||
|  |         __collection: 'child', | ||||||
|  |         childName: 'child3', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const child3 = await db.getRepository<HasManyRepository>('object.assocs', object1.id).findOne({ | ||||||
|  |       filterByTk: child1.get('id'), | ||||||
|  |       targetCollection: 'child', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(child3.get('childName')).toBe('child3'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   it('should update overridden multiple select field', async () => { |   it('should update overridden multiple select field', async () => { | ||||||
|     await collectionRepository.create({ |     await collectionRepository.create({ | ||||||
|       values: { |       values: { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user