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