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';
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 { Context } from '../index';
import { getRepositoryFromParams } from '../utils';
export function proxyToRepository(paramKeys: string[], repositoryMethod: string) {
return async function (ctx: Context, next) {

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -272,89 +272,85 @@ 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;
// set method of association
const setAccessor = association.accessors.set;
const removeAssociation = async () => {
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<Model>;
if (association.associationType === 'BelongsTo') {
M = association.target as ModelStatic<Model>;
// @ts-ignore
dataKey = association.targetKey;
} else {
M = association.target as ModelStatic<Model>;
dataKey = M.primaryKeyAttribute;
}
const createAccessor = association.accessors.create;
let dataKey: string;
let M: ModelStatic<Model>;
if (association.associationType === 'BelongsTo') {
M = association.target as ModelStatic<Model>;
// @ts-ignore
dataKey = association.targetKey;
} else {
M = association.target as ModelStatic<Model>;
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 = (<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
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<any>(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 = (<any>association).through ? (<any>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<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);
}
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 { 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: {