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:
ChengLei Shao 2023-07-21 10:51:56 +08:00 committed by GitHub
parent eb9ee38a2b
commit 08b2f374c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 587 additions and 399 deletions

View File

@ -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',
);

View File

@ -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) {

View File

@ -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',
); );

View File

@ -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} />

View File

@ -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();
}); });

View File

@ -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;

View File

@ -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;

View File

@ -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 || [];

View File

@ -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[];

View File

@ -1,5 +1,27 @@
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) {
@ -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,
});
} }
} }
}; };

View File

@ -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));
} }

View File

@ -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;

View File

@ -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 {
// 不需要 findOnefind 就是 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),
};
}
}

View File

@ -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;

View File

@ -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 {
// 不需要 findOnefind 就是 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> {}

View File

@ -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,
}); });
} }

View File

@ -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) {

View File

@ -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({

View File

@ -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));
} }

View File

@ -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: {