diff --git a/packages/core/actions/src/actions/get.ts b/packages/core/actions/src/actions/get.ts index 42b3fb77b..679a212e6 100644 --- a/packages/core/actions/src/actions/get.ts +++ b/packages/core/actions/src/actions/get.ts @@ -1,3 +1,6 @@ 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', +); diff --git a/packages/core/actions/src/actions/proxy-to-repository.ts b/packages/core/actions/src/actions/proxy-to-repository.ts index 493f30cd8..f126c53ae 100644 --- a/packages/core/actions/src/actions/proxy-to-repository.ts +++ b/packages/core/actions/src/actions/proxy-to-repository.ts @@ -1,6 +1,6 @@ -import { getRepositoryFromParams } from '../utils'; -import { Context } from '../index'; import lodash from 'lodash'; +import { Context } from '../index'; +import { getRepositoryFromParams } from '../utils'; export function proxyToRepository(paramKeys: string[], repositoryMethod: string) { return async function (ctx: Context, next) { diff --git a/packages/core/actions/src/actions/update.ts b/packages/core/actions/src/actions/update.ts index dc72b469d..81dd8fdaa 100644 --- a/packages/core/actions/src/actions/update.ts +++ b/packages/core/actions/src/actions/update.ts @@ -1,6 +1,15 @@ import { proxyToRepository } from './proxy-to-repository'; export const update = proxyToRepository( - ['filterByTk', 'values', 'whitelist', 'blacklist', 'filter', 'updateAssociationValues', 'forceUpdate'], + [ + 'filterByTk', + 'values', + 'whitelist', + 'blacklist', + 'filter', + 'updateAssociationValues', + 'forceUpdate', + 'targetCollection', + ], 'update', ); diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index bd1a6f0c1..ff4f5ae64 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -1,7 +1,7 @@ import { createForm } from '@formily/core'; import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; -import { isEmpty } from 'lodash'; import { Spin } from 'antd'; +import { isEmpty } from 'lodash'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { useCollection } from '../collection-manager'; import { RecordProvider, useRecord } from '../record-provider'; @@ -82,8 +82,8 @@ export const FormBlockProvider = (props) => { (currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name; return ( (detailFlag || createFlag) && ( - - + + ) ); @@ -122,7 +122,6 @@ const RenderChildrenWithDataTemplates = ({ form }) => { const { findComponent } = useDesignable(); const field = useField(); const Component = findComponent(field.component?.[0]) || React.Fragment; - return ( diff --git a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts index 60fe83288..b674fd740 100644 --- a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts +++ b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts @@ -486,7 +486,7 @@ describe('belongs to many', () => { const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id); - const t1 = await PostTagRepository.findOne({ + let t1 = await PostTagRepository.findOne({ filter: { name: 't1', }, @@ -502,7 +502,7 @@ describe('belongs to many', () => { }); 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(); // 设置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'); - await p2Tag.reload(); + p2Tag = await Post2TagRepository.findOne(); // p2-tag1 still not change expect(p2Tag.posts_tags.tagged_at).toBeNull(); }); diff --git a/packages/core/database/src/decorators/must-have-filter-decorator.ts b/packages/core/database/src/decorators/must-have-filter-decorator.ts index a02340b83..5b83aea07 100644 --- a/packages/core/database/src/decorators/must-have-filter-decorator.ts +++ b/packages/core/database/src/decorators/must-have-filter-decorator.ts @@ -1,18 +1,18 @@ const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { const oldValue = descriptor.value; - descriptor.value = function () { - const options = arguments[0]; + descriptor.value = function (...args: any[]) { + const options = args[0]; if (Array.isArray(options.values)) { - return oldValue.apply(this, arguments); + return oldValue.apply(this, args); } if (!options?.filter && !options?.filterByTk && !options?.forceUpdate) { 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; diff --git a/packages/core/database/src/decorators/target-collection-decorator.ts b/packages/core/database/src/decorators/target-collection-decorator.ts new file mode 100644 index 000000000..02d4a3cd1 --- /dev/null +++ b/packages/core/database/src/decorators/target-collection-decorator.ts @@ -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; diff --git a/packages/core/database/src/eager-loading/eager-loading-tree.ts b/packages/core/database/src/eager-loading/eager-loading-tree.ts index 4d7203fb9..49466d710 100644 --- a/packages/core/database/src/eager-loading/eager-loading-tree.ts +++ b/packages/core/database/src/eager-loading/eager-loading-tree.ts @@ -1,8 +1,8 @@ -import { Association, HasOne, Includeable, Model, ModelStatic, Transaction } from 'sequelize'; import lodash from 'lodash'; +import { Association, HasOne, Includeable, Model, ModelStatic, Op, Transaction } from 'sequelize'; import Database from '../database'; -import { OptionsParser } from '../options-parser'; import { appendChildCollectionNameAfterRepositoryFind } from '../listeners/append-child-collection-name-after-repository-find'; +import { OptionsParser } from '../options-parser'; interface EagerLoadingNode { model: ModelStatic; @@ -13,6 +13,7 @@ interface EagerLoadingNode { parent?: EagerLoadingNode; instances?: Array; order?: any; + where?: any; inspectInheritAttribute?: boolean; } @@ -89,7 +90,10 @@ export class EagerLoadingTree { 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 child = buildNode({ @@ -98,6 +102,7 @@ export class EagerLoadingTree { rawAttributes: lodash.cloneDeep(include.attributes), attributes: lodash.cloneDeep(include.attributes), parent: eagerLoadingTreeParent, + where: include.where, children: [], }); @@ -177,8 +182,15 @@ export class EagerLoadingTree { const foreignKey = association.foreignKey; 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 = { - where: { [foreignKey]: foreignKeyValues }, + where, attributes: node.attributes, order: orderOption(association), transaction, @@ -275,7 +287,8 @@ export class EagerLoadingTree { } 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 const nodeRawAttributes = node.rawAttributes || []; diff --git a/packages/core/database/src/inherited-collection.ts b/packages/core/database/src/inherited-collection.ts index 15657a840..5fe6e01be 100644 --- a/packages/core/database/src/inherited-collection.ts +++ b/packages/core/database/src/inherited-collection.ts @@ -1,6 +1,6 @@ -import { Collection, CollectionContext, CollectionOptions } from './collection'; import { default as lodash } from 'lodash'; import { Field } from '.'; +import { Collection, CollectionContext, CollectionOptions } from './collection'; export class InheritedCollection extends Collection { parents?: Collection[]; diff --git a/packages/core/database/src/listeners/append-child-collection-name-after-repository-find.ts b/packages/core/database/src/listeners/append-child-collection-name-after-repository-find.ts index 036736b51..b19ea606b 100644 --- a/packages/core/database/src/listeners/append-child-collection-name-after-repository-find.ts +++ b/packages/core/database/src/listeners/append-child-collection-name-after-repository-find.ts @@ -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 }) => { + 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()) { for (const row of data) { if (row.__collection) { continue; } - + const fullTableName = findOptions.raw ? `${row['__schemaName']}.${row['__tableName']}` : `${row.get('__schemaName')}.${row.get('__tableName')}`; @@ -24,11 +46,7 @@ export const appendChildCollectionNameAfterRepositoryFind = (db) => { const rowCollectionName = rowCollection.name; - findOptions.raw - ? (row['__collection'] = rowCollectionName) - : row.set('__collection', rowCollectionName, { - raw: true, - }); + setRowAttribute(row, '__collection', rowCollectionName, findOptions.raw); } } }; diff --git a/packages/core/database/src/options-parser.ts b/packages/core/database/src/options-parser.ts index 0d3ec3690..330b97aa1 100644 --- a/packages/core/database/src/options-parser.ts +++ b/packages/core/database/src/options-parser.ts @@ -1,3 +1,4 @@ +import lodash from 'lodash'; import { FindAttributeOptions, ModelStatic, Op, Sequelize } from 'sequelize'; import { Collection } from './collection'; 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)); } diff --git a/packages/core/database/src/relation-repository/belongs-to-many-repository.ts b/packages/core/database/src/relation-repository/belongs-to-many-repository.ts index 356dd05e2..b73c70faa 100644 --- a/packages/core/database/src/relation-repository/belongs-to-many-repository.ts +++ b/packages/core/database/src/relation-repository/belongs-to-many-repository.ts @@ -1,51 +1,14 @@ import lodash from 'lodash'; import { BelongsToMany, Op, Transaction } from 'sequelize'; -import { Model } from '../model'; -import { - AggregateOptions, - CreateOptions, - DestroyOptions, - FindOneOptions, - FindOptions, - TargetKey, - UpdateOptions, -} from '../repository'; +import { AggregateOptions, CreateOptions, DestroyOptions, TargetKey } from '../repository'; import { updateThroughTableValue } from '../update-associations'; -import { FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository'; +import { MultipleRelationRepository } from './multiple-relation-repository'; import { transaction } from './relation-repository'; import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types'; type CreateBelongsToManyOptions = CreateOptions; -interface IBelongsToManyRepository { - find(options?: FindOptions): Promise; - - findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>; - - findOne(options?: FindOneOptions): Promise; - - // 新增并关联,存在中间表数据 - create(options?: CreateOptions): Promise; - - // 更新,存在中间表数据 - update(options?: UpdateOptions): Promise; - - // 删除 - destroy(options?: number | string | number[] | string[] | DestroyOptions): Promise; - - // 建立关联 - set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; - - // 附加关联,存在中间表数据 - add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; - - // 移除关联 - remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; - - toggle(options: TargetKey | { pk?: TargetKey; transaction?: Transaction }): Promise; -} - -export class BelongsToManyRepository extends MultipleRelationRepository implements IBelongsToManyRepository { +export class BelongsToManyRepository extends MultipleRelationRepository { async aggregate(options: AggregateOptions) { const targetRepository = this.targetCollection.repository; diff --git a/packages/core/database/src/relation-repository/belongs-to-repository.ts b/packages/core/database/src/relation-repository/belongs-to-repository.ts index 5b3f923fe..1675c20d0 100644 --- a/packages/core/database/src/relation-repository/belongs-to-repository.ts +++ b/packages/core/database/src/relation-repository/belongs-to-repository.ts @@ -1,23 +1,15 @@ -import { Model } from '../model'; -import { CreateOptions, UpdateOptions } from '../repository'; +import { BelongsTo } from 'sequelize'; import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository'; type BelongsToFindOptions = SingleRelationFindOption; -interface IBelongsToRepository { - // 不需要 findOne,find 就是 findOne - find(options?: BelongsToFindOptions): Promise; - findOne(options?: BelongsToFindOptions): Promise; - // 新增并关联,如果存在关联,解除之后,与新数据建立关联 - create(options?: CreateOptions): Promise; - // 更新 - update(options?: UpdateOptions): Promise; - // 删除 - destroy(): Promise; - // 建立关联 - set(primaryKey: any): Promise; - // 移除关联 - remove(): Promise; -} +export class BelongsToRepository extends SingleRelationRepository { + async filterOptions(sourceModel) { + const association = this.association as BelongsTo; -export class BelongsToRepository extends SingleRelationRepository implements IBelongsToRepository {} + return { + // @ts-ignore + [association.targetKey]: sourceModel.get(association.foreignKey), + }; + } +} diff --git a/packages/core/database/src/relation-repository/hasmany-repository.ts b/packages/core/database/src/relation-repository/hasmany-repository.ts index 0234bc478..21f95c9d9 100644 --- a/packages/core/database/src/relation-repository/hasmany-repository.ts +++ b/packages/core/database/src/relation-repository/hasmany-repository.ts @@ -1,38 +1,9 @@ import { omit } from 'lodash'; import { HasMany, Op } from 'sequelize'; -import { Model } from '../model'; -import { - AggregateOptions, - CreateOptions, - DestroyOptions, - FindOneOptions, - FindOptions, - TargetKey, - TK, - UpdateOptions, -} from '../repository'; -import { AssociatedOptions, FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository'; +import { AggregateOptions, DestroyOptions, FindOptions, TargetKey, TK } from '../repository'; +import { AssociatedOptions, MultipleRelationRepository } from './multiple-relation-repository'; import { transaction } from './relation-repository'; - -interface IHasManyRepository { - find(options?: FindOptions): Promise; - findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>; - findOne(options?: FindOneOptions): Promise; - // 新增并关联 - create(options?: CreateOptions): Promise; - // 更新 - update(options?: UpdateOptions): Promise; - // 删除 - destroy(options?: TK | DestroyOptions): Promise; - // 建立关联 - set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; - // 附加关联 - add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; - // 移除关联 - remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise; -} - -export class HasManyRepository extends MultipleRelationRepository implements IHasManyRepository { +export class HasManyRepository extends MultipleRelationRepository { async find(options?: FindOptions): Promise { const targetRepository = this.targetCollection.repository; diff --git a/packages/core/database/src/relation-repository/hasone-repository.ts b/packages/core/database/src/relation-repository/hasone-repository.ts index 10f873a86..4547a2043 100644 --- a/packages/core/database/src/relation-repository/hasone-repository.ts +++ b/packages/core/database/src/relation-repository/hasone-repository.ts @@ -1,21 +1,13 @@ -import { Model } from '../model'; -import { CreateOptions } from '../repository'; -import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository'; +import { HasOne } from 'sequelize'; +import { SingleRelationRepository } from './single-relation-repository'; -interface IHasOneRepository { - // 不需要 findOne,find 就是 findOne - find(options?: SingleRelationFindOption): Promise; - findOne(options?: SingleRelationFindOption): Promise; - // 新增并关联,如果存在关联,解除之后,与新数据建立关联 - create(options?: CreateOptions): Promise; - // 更新 - update(options?): Promise; - // 删除 - destroy(): Promise; - // 建立关联 - set(primaryKey: any): Promise; - // 移除关联 - remove(): Promise; +export class HasOneRepository extends SingleRelationRepository { + filterOptions(sourceModel) { + const association = this.association as HasOne; + + return { + // @ts-ignore + [association.foreignKey]: sourceModel.get(association.sourceKey), + }; + } } - -export class HasOneRepository extends SingleRelationRepository implements IHasOneRepository {} diff --git a/packages/core/database/src/relation-repository/multiple-relation-repository.ts b/packages/core/database/src/relation-repository/multiple-relation-repository.ts index ed044bd3a..f254f2eb2 100644 --- a/packages/core/database/src/relation-repository/multiple-relation-repository.ts +++ b/packages/core/database/src/relation-repository/multiple-relation-repository.ts @@ -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 { CommonFindOptions, CountOptions, @@ -13,7 +14,6 @@ import { import { updateModelByValues } from '../update-associations'; import { UpdateGuard } from '../update-guard'; import { RelationRepository, transaction } from './relation-repository'; -import { EagerLoadingTree } from '../eager-loading/eager-loading-tree'; export type FindAndCountOptions = CommonFindOptions; @@ -27,67 +27,31 @@ export abstract class MultipleRelationRepository extends RelationRepository { } async find(options?: FindOptions): Promise { - const transaction = await this.getTransaction(options); + const targetRepository = this.targetCollection.repository; - const findOptions = { - ...this.extendFindOptions( - this.buildQueryOptions({ - ...options, - }), - ), - subQuery: false, + const association = this.association as any; + + const oneFromTargetOptions = { + as: '_pivot_', + foreignKey: association.otherKey, + sourceKey: association.targetKey, + realAs: association.through.model.name, }; - const getAccessor = this.accessors().get; - const sourceModel = await this.getSourceModel(transaction); + const pivotAssoc = new HasOne(association.target, association.through.model, oneFromTargetOptions); - if (!sourceModel) return []; + const appendFilter = { + isPivotFilter: true, + association: pivotAssoc, + where: { + [association.foreignKey]: this.sourceKeyValue, + }, + }; - if (findOptions.include && findOptions.include.length > 0) { - const ids = ( - await sourceModel[getAccessor]({ - ...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, + return targetRepository.find({ + include: [appendFilter], + ...options, }); - - await this.collection.db.emitAsync('afterRepositoryFind', { - findOptions: options, - dataCollection: this.collection, - data, - }); - - return data; } async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> { @@ -162,6 +126,7 @@ export abstract class MultipleRelationRepository extends RelationRepository { } @transaction() + @injectTargetCollection async update(options?: UpdateOptions): Promise { const transaction = await this.getTransaction(options); @@ -180,7 +145,7 @@ export abstract class MultipleRelationRepository extends RelationRepository { await updateModelByValues(instance, values, { ...options, sanitized: true, - sourceModel: this.sourceInstance, + sourceModel: await this.getSourceModel(transaction), transaction, }); } diff --git a/packages/core/database/src/relation-repository/single-relation-repository.ts b/packages/core/database/src/relation-repository/single-relation-repository.ts index c484511c4..4db3978d2 100644 --- a/packages/core/database/src/relation-repository/single-relation-repository.ts +++ b/packages/core/database/src/relation-repository/single-relation-repository.ts @@ -1,16 +1,17 @@ import lodash from 'lodash'; import { SingleAssociationAccessors, Transactionable } from 'sequelize'; +import injectTargetCollection from '../decorators/target-collection-decorator'; import { Model } from '../model'; import { Appends, Except, Fields, Filter, TargetKey, UpdateOptions } from '../repository'; import { updateModelByValues } from '../update-associations'; import { RelationRepository, transaction } from './relation-repository'; -import { EagerLoadingTree } from '../eager-loading/eager-loading-tree'; export interface SingleRelationFindOption extends Transactionable { fields?: Fields; except?: Except; appends?: Appends; filter?: Filter; + targetCollection?: string; } interface SetOption extends Transactionable { @@ -18,6 +19,8 @@ interface SetOption extends Transactionable { } export abstract class SingleRelationRepository extends RelationRepository { + abstract filterOptions(sourceModel); + @transaction() async remove(options?: Transactionable): Promise { const transaction = await this.getTransaction(options); @@ -45,44 +48,22 @@ export abstract class SingleRelationRepository extends RelationRepository { } async find(options?: SingleRelationFindOption): Promise { - const transaction = await this.getTransaction(options); + const targetRepository = this.targetCollection.repository; - const findOptions = this.buildQueryOptions({ - ...options, - }); - - const getAccessor = this.accessors().get; - const sourceModel = await this.getSourceModel(transaction); + const sourceModel = await this.getSourceModel(await this.getTransaction(options)); if (!sourceModel) return null; - if (findOptions?.include?.length > 0) { - const templateModel = await sourceModel[getAccessor]({ - ...findOptions, - includeIgnoreAttributes: false, - transaction, - attributes: [this.targetModel.primaryKeyAttribute], - group: `${this.targetModel.name}.${this.targetModel.primaryKeyAttribute}`, - }); + const addFilter = await this.filterOptions(sourceModel); - if (!templateModel) return null; + const findOptions = { + ...options, + filter: { + $and: [options?.filter || {}, addFilter], + }, + }; - const eagerLoadingTree = EagerLoadingTree.buildFromSequelizeOptions({ - 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, - }); + return await targetRepository.findOne(findOptions); } async findOne(options?: SingleRelationFindOption): Promise> { @@ -105,11 +86,13 @@ export abstract class SingleRelationRepository extends RelationRepository { } @transaction() + @injectTargetCollection async update(options: UpdateOptions): Promise { const transaction = await this.getTransaction(options); const target = await this.find({ transaction, + targetCollection: options.targetCollection, }); if (!target) { diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index 0b7147e16..7f3215180 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -18,6 +18,7 @@ import { import { Collection } from './collection'; import { Database } from './database'; import mustHaveFilter from './decorators/must-have-filter-decorator'; +import injectTargetCollection from './decorators/target-collection-decorator'; import { transactionWrapperBuilder } from './decorators/transaction-decorator'; import { EagerLoadingTree } from './eager-loading/eager-loading-tree'; import { ArrayFieldRepository } from './field-repository/array-field-repository'; @@ -97,6 +98,7 @@ export type CountOptions = Omit; +export type FindOneOptions = Omit & { + targetCollection?: string; +}; export interface DestroyOptions extends SequelizeDestroyOptions { filter?: Filter; @@ -137,6 +141,7 @@ export interface UpdateOptions extends Omit { whitelist?: WhiteList; blacklist?: BlackList; updateAssociationValues?: AssociationKeysToBeUpdate; + targetCollection?: string; context?: any; } @@ -375,6 +380,10 @@ export class Repository { - await model[setAccessor](null, { transaction }); - model.setDataValue(key, null); - return true; - }; + const removeAssociation = async () => { + await model[setAccessor](null, { transaction }); + model.setDataValue(key, null); + return true; + }; - if (isUndefinedOrNull(value)) { - return await removeAssociation(); - } + if (isUndefinedOrNull(value)) { + return await removeAssociation(); + } - if (isStringOrNumber(value)) { - await model[setAccessor](value, { context, transaction }); - return true; - } + if (isStringOrNumber(value)) { + await model[setAccessor](value, { context, transaction }); + return true; + } - if (value instanceof Model) { - await model[setAccessor](value, { context, transaction }); - model.setDataValue(key, value); - return true; - } + if (value instanceof Model) { + await model[setAccessor](value, { context, transaction }); + model.setDataValue(key, value); + return true; + } - const createAccessor = association.accessors.create; - let dataKey: string; - let M: ModelStatic; - if (association.associationType === 'BelongsTo') { - M = association.target as ModelStatic; - // @ts-ignore - dataKey = association.targetKey; - } else { - M = association.target as ModelStatic; - dataKey = M.primaryKeyAttribute; - } + const createAccessor = association.accessors.create; + let dataKey: string; + let M: ModelStatic; + if (association.associationType === 'BelongsTo') { + M = association.target as ModelStatic; + // @ts-ignore + dataKey = association.targetKey; + } else { + M = association.target as ModelStatic; + dataKey = M.primaryKeyAttribute; + } - if (isStringOrNumber(value[dataKey])) { - const instance: any = await M.findOne({ - where: { - [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, + if (isStringOrNumber(value[dataKey])) { + const instance: any = await M.findOne({ + where: { + [dataKey]: value[dataKey], + }, transaction, - associationContext: association, - updateAssociationValues: keys, }); - model.setDataValue(key, instance); - // @ts-ignore - if (association.targetKey) { - model.setDataValue(association.foreignKey, instance[dataKey]); + 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; } - } 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 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)) { - await model[setAccessor](null, { transaction, context }); - model.setDataValue(key, null); - return; + if (isUndefinedOrNull(value)) { + await model[setAccessor](null, { transaction, context }); + model.setDataValue(key, null); + 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)) { - await model[setAccessor](value, { transaction, context, individualHooks: true }); - return; + if (isStringOrNumber(item)) { + 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]) { + 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 = (association).through ? (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 - const objectItems = []; // to be added - - // iterate item in value - for (const item of value) { - if (isUndefinedOrNull(item)) { + await updateAssociations(instance, item, { + ...options, + transaction, + associationContext: association, + updateAssociationValues: keys, + }); + newItems.push(instance); + } else { + // set & update record + const instance = await association.target.findByPk(item[pk], { + transaction, + }); + if (!instance) { continue; } + const addAccessor = association.accessors.add; - if (isStringOrNumber(item)) { - 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'; + await model[addAccessor](item[pk], accessorOptions); - if (item[targetKey]) { - setItems.push(item[targetKey]); - } - - objectItems.push(item); + if (!recursive) { + continue; } - } - - // 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 = (association).through ? (association).through.model.name : null; - - const accessorOptions = { - context, + if (updateAssociationValues.includes(key)) { + await instance.update(item, { ...options, transaction }); + } + await updateAssociations(instance, item, { + ...options, transaction, - }; - - const throughValue = item[through]; - - 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(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); - } + associationContext: association, + updateAssociationValues: keys, + }); + newItems.push(instance); } - - model.setDataValue(key, setItems.concat(newItems)); - } catch (error) { - throw error; } + + model.setDataValue(key, setItems.concat(newItems)); } diff --git a/packages/plugins/collection-manager/src/server/__tests__/inherits/inherited-collection.test.ts b/packages/plugins/collection-manager/src/server/__tests__/inherits/inherited-collection.test.ts index 1bc419bb8..6a98942f6 100644 --- a/packages/plugins/collection-manager/src/server/__tests__/inherits/inherited-collection.test.ts +++ b/packages/plugins/collection-manager/src/server/__tests__/inherits/inherited-collection.test.ts @@ -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 { createApp } from '..'; import { pgOnly } from '@nocobase/test'; +import { createApp } from '..'; pgOnly()('Inherited Collection', () => { let db: Database; @@ -12,7 +17,11 @@ pgOnly()('Inherited Collection', () => { let fieldsRepository: Repository; beforeEach(async () => { - app = await createApp(); + app = await createApp({ + database: { + prefix: '', + }, + }); db = app.db; @@ -24,6 +33,237 @@ pgOnly()('Inherited Collection', () => { 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('users.assoc', user1.get('id')).findOne({ + targetCollection: 'child', + }); + + expect(child1ViaObject1.get('childName')).toBe('child1'); + expect(child1ViaObject1.get('__collection')).toBe('child'); + + await db.getRepository('users.assoc', user1.get('id')).update({ + values: { + childName: 'child2', + __collection: 'child', + }, + }); + + const child2 = await db.getRepository('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('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('object.assocs', object1.id).findOne({ + filterByTk: child1.get('id'), + targetCollection: 'child', + }); + + expect(child1ViaObject1.get('childName')).toBe('child1'); + + await db.getRepository('object.assocs', object1.id).update({ + filterByTk: child1.get('id'), + targetCollection: 'child', + values: { + childName: 'child2', + }, + }); + + const child2 = await db.getRepository('object.assocs', object1.id).findOne({ + filterByTk: child1.get('id'), + targetCollection: 'child', + }); + + expect(child2.get('childName')).toBe('child2'); + + await db.getRepository('object.assocs', object1.id).update({ + filterByTk: child1.get('id'), + values: { + __collection: 'child', + childName: 'child3', + }, + }); + + const child3 = await db.getRepository('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 () => { await collectionRepository.create({ values: {