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';
|
||||
|
||||
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 { Context } from '../index';
|
||||
import { getRepositoryFromParams } from '../utils';
|
||||
|
||||
export function proxyToRepository(paramKeys: string[], repositoryMethod: string) {
|
||||
return async function (ctx: Context, next) {
|
||||
|
@ -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',
|
||||
);
|
||||
|
@ -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) && (
|
||||
<BlockProvider {...props} block={'form'}>
|
||||
<InternalFormBlockProvider {...props} />
|
||||
<BlockProvider {...props} block={'form'} params={{ ...props?.params, targetCollection: collection }}>
|
||||
<InternalFormBlockProvider {...props} params={{ ...props?.params, targetCollection: collection }} />
|
||||
</BlockProvider>
|
||||
)
|
||||
);
|
||||
@ -122,7 +122,6 @@ const RenderChildrenWithDataTemplates = ({ form }) => {
|
||||
const { findComponent } = useDesignable();
|
||||
const field = useField();
|
||||
const Component = findComponent(field.component?.[0]) || React.Fragment;
|
||||
|
||||
return (
|
||||
<Component {...field.componentProps}>
|
||||
<DataTemplateSelect style={{ marginBottom: 18 }} form={form} />
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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 { 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<any>;
|
||||
@ -13,6 +13,7 @@ interface EagerLoadingNode {
|
||||
parent?: EagerLoadingNode;
|
||||
instances?: Array<Model>;
|
||||
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 || [];
|
||||
|
||||
|
@ -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[];
|
||||
|
@ -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 }) => {
|
||||
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) {
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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<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?: 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> {
|
||||
export class BelongsToManyRepository extends MultipleRelationRepository {
|
||||
async aggregate(options: AggregateOptions) {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
|
||||
|
@ -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<M extends Model> {
|
||||
// 不需要 findOne,find 就是 findOne
|
||||
find(options?: BelongsToFindOptions): Promise<M>;
|
||||
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 {
|
||||
async filterOptions(sourceModel) {
|
||||
const association = this.association as BelongsTo;
|
||||
|
||||
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 { 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<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> {
|
||||
export class HasManyRepository extends MultipleRelationRepository {
|
||||
async find(options?: FindOptions): Promise<any> {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
|
||||
|
@ -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<M extends Model> {
|
||||
// 不需要 findOne,find 就是 findOne
|
||||
find(options?: SingleRelationFindOption): Promise<M>;
|
||||
findOne(options?: SingleRelationFindOption): Promise<M>;
|
||||
// 新增并关联,如果存在关联,解除之后,与新数据建立关联
|
||||
create(options?: CreateOptions): Promise<M>;
|
||||
// 更新
|
||||
update(options?): Promise<M>;
|
||||
// 删除
|
||||
destroy(): Promise<boolean>;
|
||||
// 建立关联
|
||||
set(primaryKey: any): Promise<void>;
|
||||
// 移除关联
|
||||
remove(): Promise<void>;
|
||||
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<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 {
|
||||
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<any> {
|
||||
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()) };
|
||||
return targetRepository.find({
|
||||
include: [appendFilter],
|
||||
...options,
|
||||
});
|
||||
|
||||
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]> {
|
||||
@ -162,6 +126,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
||||
}
|
||||
|
||||
@transaction()
|
||||
@injectTargetCollection
|
||||
async update(options?: UpdateOptions): Promise<any> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
@ -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<void> {
|
||||
const transaction = await this.getTransaction(options);
|
||||
@ -45,44 +48,22 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
||||
}
|
||||
|
||||
async find(options?: SingleRelationFindOption): Promise<any> {
|
||||
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<Model<any>> {
|
||||
@ -105,11 +86,13 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
||||
}
|
||||
|
||||
@transaction()
|
||||
@injectTargetCollection
|
||||
async update(options: UpdateOptions): Promise<any> {
|
||||
const transaction = await this.getTransaction(options);
|
||||
|
||||
const target = await this.find({
|
||||
transaction,
|
||||
targetCollection: options.targetCollection,
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
|
@ -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<SequelizeCountOptions, 'distinct' | 'where' | 'i
|
||||
|
||||
export interface FilterByTk {
|
||||
filterByTk?: TargetKey;
|
||||
targetCollection?: string;
|
||||
}
|
||||
|
||||
export type FindOptions = SequelizeFindOptions & CommonFindOptions & FilterByTk;
|
||||
@ -111,7 +113,9 @@ export interface CommonFindOptions extends Transactionable {
|
||||
tree?: boolean;
|
||||
}
|
||||
|
||||
export type FindOneOptions = Omit<FindOptions, 'limit'>;
|
||||
export type FindOneOptions = Omit<FindOptions, 'limit'> & {
|
||||
targetCollection?: string;
|
||||
};
|
||||
|
||||
export interface DestroyOptions extends SequelizeDestroyOptions {
|
||||
filter?: Filter;
|
||||
@ -137,6 +141,7 @@ export interface UpdateOptions extends Omit<SequelizeUpdateOptions, 'where'> {
|
||||
whitelist?: WhiteList;
|
||||
blacklist?: BlackList;
|
||||
updateAssociationValues?: AssociationKeysToBeUpdate;
|
||||
targetCollection?: string;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
@ -375,6 +380,10 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
* @param options
|
||||
*/
|
||||
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 transaction = await this.getTransaction(options);
|
||||
|
||||
@ -593,6 +602,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
*/
|
||||
@transaction()
|
||||
@mustHaveFilter()
|
||||
@injectTargetCollection
|
||||
async update(options: UpdateOptions & { forceUpdate?: boolean }) {
|
||||
if (Array.isArray(options.values)) {
|
||||
return this.updateMany({
|
||||
|
@ -272,7 +272,6 @@ export async function updateSingleAssociation(
|
||||
const { recursive, context, updateAssociationValues = [], transaction } = options;
|
||||
const keys = getKeysByPrefix(updateAssociationValues, key);
|
||||
|
||||
try {
|
||||
// set method of association
|
||||
const setAccessor = association.accessors.set;
|
||||
|
||||
@ -353,9 +352,6 @@ export async function updateSingleAssociation(
|
||||
if (association.targetKey) {
|
||||
model.setDataValue(association.foreignKey, instance[dataKey]);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -389,7 +385,6 @@ export async function updateMultipleAssociation(
|
||||
const { recursive, context, updateAssociationValues = [], transaction } = options;
|
||||
const keys = getKeysByPrefix(updateAssociationValues, key);
|
||||
|
||||
try {
|
||||
const setAccessor = association.accessors.set;
|
||||
|
||||
const createAccessor = association.accessors.create;
|
||||
@ -494,7 +489,4 @@ export async function updateMultipleAssociation(
|
||||
}
|
||||
|
||||
model.setDataValue(key, setItems.concat(newItems));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -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<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 () => {
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
|
Loading…
Reference in New Issue
Block a user