mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
* feat(collection-tree): add collection tree plugin * feat(collection-tree): add collection tree path handle function * feat(collection-tree): add collection tree path root and depth column handle function * feat(collection-tree): add exist tree collection data migrate function * feat(collection-tree): improve exist tree collection data migrate function * feat(collection-tree): improve exist tree collection data migrate function * feat(collection-tree): add collection tree to build in plugin * feat(collection-tree): modify collection tree plugin version * feat(collection-tree): fix collection tree build bug * feat(collection-tree): fix tree collection path create bug and add tree table search function * feat(collection-tree): add tree table pagination function * feat(collection-tree): fix tree table pagination function * feat(collection-tree): fix tree table pagination function * feat(collection-tree): fix tree table search function * feat(collection-tree): fix tree table search function * feat(collection-tree): improve tree table search filter function * feat(collection-tree): update collection tree plugin version and preset package dependancy * feat(collection-tree): improve tree collection function * feat(collection-tree): remove the duplicate function * feat(collection-tree): fix tree collection db exist bug * feat(collection-tree): improve tree collection db variable name * feat(collection-tree): remove migration file * feat(collection-tree): add collection tree sync create function * feat(collection-tree): add collection tree sync create function * feat(collection-tree): disable collection tree search function * feat(collection-tree): enable collection tree search function * feat(collection-tree): modify collection tree signal to adapt test case * feat(collection-tree): modify code to fix test case * feat(collection-tree): improve code and abstract define collection tree path * feat(collection-tree): modify rootPK to rootPk to avoid underscored behavior * feat(collection-tree): update collection tree version and add collection tree plugin for test * feat(collection-tree): fix filter collection tree root null bug * feat(collection-tree): migrate tree test case to collection tree plugin directory * feat(collection-tree): add transaction for collection tree table delete * feat(collection-tree): fix collection tree switch bug * feat(collection-tree): remove tree filter switch * feat(collection-tree): fix test case * feat(collection-tree): fix test case * feat(collection-tree): fix DB UNDERSCORED bug * feat(collection-tree): fix relate collections not exist bug * feat(collection-tree): add common parent function * feat(collection-tree): add compatible function for sqlite * feat(collection-tree): modify collection tree path create method * feat(collection-tree): migrate tree test case to no acl * feat(collection-tree): migrate tree test case to no acl and fix collections undefined bug * feat(collection-tree): migrate tree test case * feat(collection-tree): fix test case bug * feat(collection-tree): fix test case bug * feat(collection-tree): fix test case bug * feat(collection-tree): improve tree search function * feat(collection-tree): merge the next branch code to fix confilct * feat(collection-tree): merge the next branch code to fix confilct * feat(collection-tree): merge the next branch code to fix confilct * feat(collection-tree): split the collection tree test to new file and fix filterbytk bug * feat(collection-tree): fix filter tree collection primary key bug * feat(collection-tree): remove recursive test case and fix collection tree filter bug * feat(collection-tree): fix collection tree filter bug * feat(collection-tree): fix collection tree filter bug and modify test case * feat(collection-tree): add parentid column for tree collection and modify test case * feat(collection-tree): disable sync exist tree collection path table create logic * feat(collection-tree): add sync exist tree collection path table create logic on plugin afterLoad * feat(collection-tree): remove debug code * feat(collection-tree): fix collection tree delete bug * feat(collection-tree): improve collection tree filter find and count implement * feat(collection-tree): improve path table name variable implement * feat(collection-tree): remove unnecessary plugin for test case code * feat(collection-tree): add await for delete synchronous path function * feat(collection-tree): improve tree path create function * feat(collection-tree): remove unnecessary code * feat(collection-tree): remove unnecessary code * feat(collection-tree): improve tree path create function * feat(collection-tree): improve tree filter function * feat(collection-tree): improve tree filter datasource to dynamic * feat(collection-tree): improve find common parent code * feat(collection-tree): add collection tree path table not found warning log * feat(collection-tree): improve get collection primary key implementation * feat(collection-tree): fix tree root path not delete bug and tree path definition bug * feat(collection-tree): fix findAndCount function variable definition bug * feat(collection-tree): modify migrate exist collection tree migration function * feat(collection-tree): correct variable name * feat(collection-tree): remove duplicate code * feat(collection-tree): fix sync exist collection tree path function variable bug * feat(collection-tree): improve collection tree path update logic * feat(collection-tree): remove await for get collection * test: add test cases * feat(collection-tree): modify filter parameter for collection tree * feat(collection-tree): remove await for get collection * feat(collection-tree): remove necessary code * feat(collection-tree): add exist tree collection path migration function * feat(collection-tree): remove unused sync exist tree collection table function * feat(collection-tree): use get method to replace use dataValues function * feat(collection-tree): use get method to replace use dataValues function * feat(collection-tree): fix migration rootPk variable bug * feat(collection-tree): use get method to replace use dataValues function * feat(collection-tree): improve get tree path logic * feat(collection-tree): remove unused test case code * feat(collection-tree): add sync collection tree test case * feat(collection-tree): add tree path test case and fix migration bug * feat(collection-tree): add migration db variable * feat(collection-tree): change logger function * feat(collection-tree): remove unused library * feat(collection-tree): add plugin information * feat(collection-tree): remove await for get collection and use this.db instead of this.app.db * feat(collection-tree): improve the performance of exist data migration to path table * feat(collection-tree): modify get tree path implement to avoid infinite loop bug * feat(collection-tree): fix path create bug * feat(collection-tree): add index for path table * feat(collection-tree): fix related node path bug when some node parent changed * feat(collection-tree): add transaction for get tree path function * feat(collection-tree): add tree path test case * feat(collection-tree): change parent field id name to dynamic * feat(collection-tree): migrate some test case to path.test.ts file * feat(collection-tree): add some test case for path table * feat(collection-tree): fix sqlite and mysql json type search path data bug * feat(collection-tree): fix sqlite query related data sql bug * feat(collection-tree): fix list action test case query data with related data bug * feat(collection-tree): try to fix mysql test case bug to remove index * feat(collection-tree): remove unnecessary code * chore: string field with length option * feat(collection-tree): change path type to string and set max length to 1024 * fix: merge conflicts * feat(collection-tree): modify query path filter to adapt the path change to string * feat(collection-tree): remove append parent condition * feat(collection-tree): split the path test case * feat(collection-tree): remove unused code and fix test case plugin bug * feat(collection-tree): improve get tree path implementation * feat(collection-tree): disable one failed test case for full test * feat(collection-tree): add transaction for collection tree migration * feat(collection-tree): fix collection tree migration bug * feat(collection-tree): remove sqlite handle condition code * feat(collection-tree): add tree test case * feat(collection-tree): add tree test case * feat(collection-tree): modify test case to match the expection * feat(collection-tree): modify tree implementation to root parent * feat(collection-tree): remove unused function * feat(collection-tree): add count function to solve tree data pagination implementation * feat(collection-tree): split tree failed test case to legacy file * feat(collection-tree): correct legacy tree test case to expection * feat(collection-tree): add new tree test for legacy test case to meet expection * feat(collection-tree): fix legacy test case to meet expection * feat(collection-tree): modify legacy test case to meet expection and add new test case * feat(collection-tree): improve tree test case * feat(collection-tree): improve tree test case * feat(collection-tree): improve tree test case * feat(collection-tree): add tree test case * feat(collection-tree): add tree count test case * feat(collection-tree): add tree find and count test case * feat(collection-tree): add tree find one test case * feat(collection-tree): improve migration get path performance * feat(collection-tree): improve get tree path function to avoid undefined parent data situation * feat(collection-tree): add tree and raw parameter variable * fix: test * feat(collection-tree): modify index length * feat(collection-tree): fix index snake case bug * feat(collection-tree): improve tree filter search * feat(collection-tree): fix tree search pagination parameter omit bug * chore: optimize * chore: optimize * fix: build * fix: type * fix: bug * chore: move tree repo to plugin * fix: build * fix: test * chore: remove ts-ignore * fix: issue of getting filter target key --------- Co-authored-by: xilesun <2013xile@gmail.com> Co-authored-by: Chareice <chareice@live.com>
631 lines
20 KiB
TypeScript
631 lines
20 KiB
TypeScript
/**
|
|
* This file is part of the NocoBase (R) project.
|
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
* Authors: NocoBase Team.
|
|
*
|
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
*/
|
|
|
|
import lodash, { flatten } from 'lodash';
|
|
import { Association, HasOne, HasOneOptions, Includeable, Model, ModelStatic, Op, Transaction } from 'sequelize';
|
|
import Database from '../database';
|
|
import { appendChildCollectionNameAfterRepositoryFind } from '../listeners/append-child-collection-name-after-repository-find';
|
|
import { OptionsParser } from '../options-parser';
|
|
import { Collection } from '../collection';
|
|
|
|
interface EagerLoadingNode {
|
|
model: ModelStatic<any>;
|
|
association: Association;
|
|
attributes: Array<string>;
|
|
rawAttributes: Array<string>;
|
|
children: Array<EagerLoadingNode>;
|
|
parent?: EagerLoadingNode;
|
|
instances?: Array<Model>;
|
|
order?: any;
|
|
where?: any;
|
|
inspectInheritAttribute?: boolean;
|
|
includeOptions?: any;
|
|
}
|
|
|
|
const pushAttribute = (node, attribute) => {
|
|
if (lodash.isArray(node.attributes) && !node.attributes.includes(attribute)) {
|
|
node.attributes.push(attribute);
|
|
}
|
|
};
|
|
|
|
const EagerLoadingNodeProto = {
|
|
afterBuild(db: Database) {
|
|
const collection = db.modelCollection.get(this.model);
|
|
|
|
if (collection && collection.isParent()) {
|
|
if (!this.attributes) {
|
|
this.attributes = {
|
|
include: [],
|
|
};
|
|
}
|
|
|
|
OptionsParser.appendInheritInspectAttribute(
|
|
lodash.isArray(this.attributes) ? this.attributes : this.attributes.include,
|
|
collection,
|
|
);
|
|
|
|
this.inspectInheritAttribute = true;
|
|
}
|
|
},
|
|
};
|
|
|
|
const queryParentSQL = (options: {
|
|
db: Database;
|
|
nodeIds: any[];
|
|
collection: Collection;
|
|
foreignKey: string;
|
|
targetKey: string;
|
|
}) => {
|
|
const { collection, db, nodeIds } = options;
|
|
const tableName = collection.quotedTableName();
|
|
const { foreignKey, targetKey } = options;
|
|
const foreignKeyField = collection.model.rawAttributes[foreignKey].field;
|
|
const targetKeyField = collection.model.rawAttributes[targetKey].field;
|
|
|
|
const queryInterface = db.sequelize.getQueryInterface();
|
|
const q = queryInterface.quoteIdentifier.bind(queryInterface);
|
|
return `WITH RECURSIVE cte AS (
|
|
SELECT ${q(targetKeyField)}, ${q(foreignKeyField)}
|
|
FROM ${tableName}
|
|
WHERE ${q(targetKeyField)} IN (${nodeIds.join(',')})
|
|
UNION ALL
|
|
SELECT t.${q(targetKeyField)}, t.${q(foreignKeyField)}
|
|
FROM ${tableName} AS t
|
|
INNER JOIN cte ON t.${q(targetKeyField)} = cte.${q(foreignKeyField)}
|
|
)
|
|
SELECT ${q(targetKeyField)} AS ${q(targetKey)}, ${q(foreignKeyField)} AS ${q(foreignKey)} FROM cte`;
|
|
};
|
|
|
|
export class EagerLoadingTree {
|
|
public root: EagerLoadingNode;
|
|
db: Database;
|
|
private rootQueryOptions: any = {};
|
|
|
|
constructor(root: EagerLoadingNode) {
|
|
this.root = root;
|
|
}
|
|
|
|
static buildFromSequelizeOptions(options: {
|
|
model: ModelStatic<any>;
|
|
rootAttributes: Array<string>;
|
|
rootOrder?: any;
|
|
rootQueryOptions?: any;
|
|
includeOption: Includeable | Includeable[];
|
|
db: Database;
|
|
}): EagerLoadingTree {
|
|
const { model, rootAttributes, includeOption, db, rootQueryOptions } = options;
|
|
|
|
const buildNode = (node) => {
|
|
Object.setPrototypeOf(node, EagerLoadingNodeProto);
|
|
node.afterBuild(db);
|
|
return node;
|
|
};
|
|
|
|
const root = buildNode({
|
|
model,
|
|
association: null,
|
|
rawAttributes: lodash.cloneDeep(rootAttributes),
|
|
attributes: lodash.cloneDeep(rootAttributes),
|
|
order: options.rootOrder,
|
|
children: [],
|
|
});
|
|
|
|
const traverseIncludeOption = (includeOption, eagerLoadingTreeParent) => {
|
|
const includeOptions = lodash.castArray(includeOption);
|
|
|
|
if (includeOption.length > 0) {
|
|
const modelPrimaryKey = eagerLoadingTreeParent.model.primaryKeyAttribute;
|
|
pushAttribute(eagerLoadingTreeParent, modelPrimaryKey);
|
|
}
|
|
|
|
for (const include of includeOptions) {
|
|
// skip fromFilter include option
|
|
if (include.fromFilter) {
|
|
continue;
|
|
}
|
|
|
|
const association = lodash.isString(include.association)
|
|
? eagerLoadingTreeParent.model.associations[include.association]
|
|
: include.association;
|
|
|
|
if (!association) {
|
|
throw new Error(
|
|
`Association "${include.association}" not found in model "${eagerLoadingTreeParent.model.name}"`,
|
|
);
|
|
}
|
|
|
|
const associationType = association.associationType;
|
|
|
|
const child = buildNode({
|
|
model: association.target,
|
|
association,
|
|
rawAttributes: lodash.cloneDeep(include.attributes),
|
|
attributes: lodash.cloneDeep(include.attributes),
|
|
parent: eagerLoadingTreeParent,
|
|
where: include.where,
|
|
children: [],
|
|
includeOption: include.options || {},
|
|
});
|
|
|
|
if (associationType == 'HasOne' || associationType == 'HasMany') {
|
|
const { sourceKey, foreignKey } = association;
|
|
|
|
pushAttribute(eagerLoadingTreeParent, sourceKey);
|
|
pushAttribute(child, foreignKey);
|
|
}
|
|
|
|
if (associationType == 'BelongsTo') {
|
|
const { targetKey, foreignKey } = association;
|
|
|
|
pushAttribute(eagerLoadingTreeParent, foreignKey);
|
|
pushAttribute(child, targetKey);
|
|
}
|
|
|
|
if (associationType == 'BelongsToMany') {
|
|
const { sourceKey } = association;
|
|
pushAttribute(eagerLoadingTreeParent, sourceKey);
|
|
}
|
|
|
|
eagerLoadingTreeParent.children.push(child);
|
|
|
|
if (include.include) {
|
|
traverseIncludeOption(include.include, child);
|
|
}
|
|
}
|
|
};
|
|
|
|
traverseIncludeOption(includeOption, root);
|
|
|
|
const tree = new EagerLoadingTree(root);
|
|
tree.db = db;
|
|
tree.rootQueryOptions = rootQueryOptions;
|
|
return tree;
|
|
}
|
|
|
|
async load(transaction?: Transaction) {
|
|
const result = {};
|
|
|
|
const orderOption = (association) => {
|
|
const targetModel = association.target;
|
|
const order = [];
|
|
|
|
if (targetModel.primaryKeyAttribute && targetModel.rawAttributes[targetModel.primaryKeyAttribute].autoIncrement) {
|
|
order.push([targetModel.primaryKeyAttribute, 'ASC']);
|
|
}
|
|
|
|
return order;
|
|
};
|
|
|
|
const loadRecursive = async (node, ids = []) => {
|
|
let instances = [];
|
|
|
|
if (!node.parent) {
|
|
// load root instances
|
|
const rootInclude = this.rootQueryOptions?.include || node.includeOption;
|
|
|
|
const includeForFilter = rootInclude.filter((include) => {
|
|
return (
|
|
Object.keys(include.where || {}).length > 0 ||
|
|
JSON.stringify(this.rootQueryOptions?.filter)?.includes(include.association)
|
|
);
|
|
});
|
|
|
|
const isBelongsToAssociationOnly = (includes, model) => {
|
|
for (const include of includes) {
|
|
const association = model.associations[include.association];
|
|
if (!association) {
|
|
return false;
|
|
}
|
|
|
|
if (association.associationType != 'BelongsTo') {
|
|
return false;
|
|
}
|
|
|
|
if (!isBelongsToAssociationOnly(include.include || [], association.target)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const belongsToAssociationsOnly = isBelongsToAssociationOnly(includeForFilter, node.model);
|
|
|
|
if (belongsToAssociationsOnly) {
|
|
instances = await node.model.findAll({
|
|
...this.rootQueryOptions,
|
|
attributes: node.attributes,
|
|
distinct: true,
|
|
include: includeForFilter,
|
|
transaction,
|
|
});
|
|
} else {
|
|
const primaryKeyField = node.model.primaryKeyField || node.model.primaryKeyAttribute;
|
|
|
|
if (!primaryKeyField) {
|
|
throw new Error(`Model ${node.model.name} does not have primary key`);
|
|
}
|
|
|
|
includeForFilter.forEach((include: { association: string }, index: number) => {
|
|
const association = node.model.associations[include.association];
|
|
if (association?.associationType == 'BelongsToArray') {
|
|
includeForFilter[index] = {
|
|
...include,
|
|
...association.generateInclude(),
|
|
};
|
|
}
|
|
});
|
|
|
|
// find all ids
|
|
const ids = (
|
|
await node.model.findAll({
|
|
...this.rootQueryOptions,
|
|
includeIgnoreAttributes: false,
|
|
attributes: [primaryKeyField],
|
|
group: `${node.model.name}.${primaryKeyField}`,
|
|
transaction,
|
|
include: includeForFilter,
|
|
} as any)
|
|
).map((row) => {
|
|
return { row, pk: row[primaryKeyField] };
|
|
});
|
|
|
|
const findOptions = {
|
|
where: { [primaryKeyField]: ids.map((i) => i.pk) },
|
|
attributes: node.attributes,
|
|
};
|
|
|
|
if (node.order) {
|
|
findOptions['order'] = node.order;
|
|
}
|
|
|
|
instances = await node.model.findAll({
|
|
...findOptions,
|
|
transaction,
|
|
});
|
|
}
|
|
|
|
// clear filter association value
|
|
const associations = node.model.associations;
|
|
for (const [name, association] of Object.entries(associations)) {
|
|
// if ((association as any).associationType === 'belongsToArray') {
|
|
// continue;
|
|
// }
|
|
for (const instance of instances) {
|
|
delete instance[name];
|
|
delete instance.dataValues[name];
|
|
}
|
|
}
|
|
} else if (ids.length > 0) {
|
|
const association = node.association;
|
|
const associationType = association.associationType;
|
|
|
|
let params: any = {};
|
|
|
|
const otherFindOptions = lodash.pick(node.includeOption, ['sort']) || {};
|
|
|
|
const collection = this.db.modelCollection.get(node.model);
|
|
|
|
if (collection && !lodash.isEmpty(otherFindOptions)) {
|
|
const parser = new OptionsParser(otherFindOptions, {
|
|
collection,
|
|
});
|
|
|
|
params = parser.toSequelizeParams();
|
|
}
|
|
|
|
if (associationType == 'HasOne' || associationType == 'HasMany') {
|
|
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,
|
|
attributes: node.attributes,
|
|
order: params.order || orderOption(association),
|
|
transaction,
|
|
};
|
|
|
|
instances = await node.model.findAll(findOptions);
|
|
}
|
|
|
|
if (associationType === 'BelongsToArray') {
|
|
const targetKey = association.targetKey;
|
|
const targetKeyValues = node.parent.instances.map((instance) => {
|
|
return instance.get(association.foreignKey);
|
|
});
|
|
|
|
let where: any = { [targetKey]: Array.from(new Set(flatten(targetKeyValues))) };
|
|
|
|
if (node.where) {
|
|
where = {
|
|
[Op.and]: [where, node.where],
|
|
};
|
|
}
|
|
|
|
const findOptions = {
|
|
where,
|
|
attributes: node.attributes,
|
|
order: params.order || orderOption(association),
|
|
transaction,
|
|
};
|
|
|
|
instances = await node.model.findAll(findOptions);
|
|
}
|
|
|
|
if (associationType == 'BelongsTo') {
|
|
const foreignKey = association.foreignKey;
|
|
const parentInstancesForeignKeyValues = node.parent.instances.map((instance) => instance.get(foreignKey));
|
|
|
|
const collection = this.db.modelCollection.get(node.model);
|
|
|
|
instances = await node.model.findAll({
|
|
transaction,
|
|
where: {
|
|
[association.targetKey]: parentInstancesForeignKeyValues,
|
|
},
|
|
attributes: node.attributes,
|
|
});
|
|
|
|
// load parent instances recursively
|
|
if (node.includeOption.recursively && instances.length > 0) {
|
|
const targetKey = association.targetKey;
|
|
const sql = queryParentSQL({
|
|
db: this.db,
|
|
collection,
|
|
foreignKey,
|
|
targetKey,
|
|
nodeIds: instances.map((instance) => instance.get(targetKey)),
|
|
});
|
|
|
|
const results = await this.db.sequelize.query(sql, {
|
|
type: 'SELECT',
|
|
transaction,
|
|
});
|
|
|
|
const parentInstances = await node.model.findAll({
|
|
transaction,
|
|
where: {
|
|
[association.targetKey]: results.map((result) => result[targetKey]),
|
|
},
|
|
attributes: node.attributes,
|
|
});
|
|
|
|
const setInstanceParent = (instance) => {
|
|
const parentInstance = parentInstances.find(
|
|
(parentInstance) => parentInstance.get(targetKey) == instance.get(foreignKey),
|
|
);
|
|
if (!parentInstance) {
|
|
return;
|
|
}
|
|
|
|
setInstanceParent(parentInstance);
|
|
instance[association.as] = instance.dataValues[association.as] = parentInstance;
|
|
};
|
|
|
|
for (const instance of instances) {
|
|
setInstanceParent(instance);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (associationType == 'BelongsToMany') {
|
|
const foreignKeyValues = node.parent.instances.map((instance) => instance.get(association.sourceKey));
|
|
|
|
const hasOneOptions: HasOneOptions = {
|
|
as: '_pivot_',
|
|
foreignKey: association.otherKey,
|
|
sourceKey: association.targetKey,
|
|
};
|
|
if (association.through.scope) {
|
|
hasOneOptions.scope = association.through.scope;
|
|
}
|
|
const pivotAssoc = new HasOne(association.target, association.through.model, hasOneOptions);
|
|
|
|
instances = await node.model.findAll({
|
|
transaction,
|
|
attributes: node.attributes,
|
|
include: [
|
|
{
|
|
association: pivotAssoc,
|
|
where: {
|
|
[association.foreignKey]: foreignKeyValues,
|
|
},
|
|
},
|
|
],
|
|
order: params.order || orderOption(association),
|
|
});
|
|
}
|
|
}
|
|
|
|
node.instances = instances;
|
|
|
|
for (const child of node.children) {
|
|
const modelPrimaryKey = node.model.primaryKeyField || node.model.primaryKeyAttribute;
|
|
const nodeIds = instances.map((instance) => instance.get(modelPrimaryKey));
|
|
await loadRecursive(child, nodeIds);
|
|
}
|
|
|
|
// merge instances to parent
|
|
if (!node.parent) {
|
|
return;
|
|
} else {
|
|
const association = node.association;
|
|
const associationType = association.associationType;
|
|
|
|
const setParentAccessor = (parentInstance) => {
|
|
const key = association.as;
|
|
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
const children = parentInstance.getDataValue(association.as);
|
|
|
|
if (association.isSingleAssociation) {
|
|
const isEmpty = !children;
|
|
parentInstance[key] = parentInstance.dataValues[key] = isEmpty ? null : children;
|
|
} else {
|
|
const isEmpty = !children || children.length == 0;
|
|
parentInstance[key] = parentInstance.dataValues[key] = isEmpty ? [] : children;
|
|
}
|
|
};
|
|
|
|
if (associationType == 'HasMany' || associationType == 'HasOne') {
|
|
const foreignKey = association.foreignKey;
|
|
const sourceKey = association.sourceKey;
|
|
|
|
for (const instance of node.instances) {
|
|
const parentInstance = node.parent.instances.find(
|
|
(parentInstance) => parentInstance.get(sourceKey) == instance.get(foreignKey),
|
|
);
|
|
|
|
if (parentInstance) {
|
|
if (associationType == 'HasMany') {
|
|
const children = parentInstance.getDataValue(association.as);
|
|
if (!children) {
|
|
parentInstance.setDataValue(association.as, [instance]);
|
|
} else {
|
|
children.push(instance);
|
|
}
|
|
}
|
|
|
|
if (associationType == 'HasOne') {
|
|
const key = association.options.realAs || association.as;
|
|
parentInstance[key] = parentInstance.dataValues[key] = instance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (associationType === 'BelongsToArray') {
|
|
const { foreignKey, targetKey } = association;
|
|
|
|
const instanceMap = node.instances.reduce((mp: { [targetKey: string]: Model }, instance: Model) => {
|
|
mp[instance.get(targetKey)] = instance;
|
|
return mp;
|
|
}, {});
|
|
|
|
node.parent.instances.forEach((parentInstance: Model) => {
|
|
const targetKeys = parentInstance.getDataValue(foreignKey);
|
|
parentInstance.setDataValue(
|
|
association.as,
|
|
targetKeys?.map((targetKey: any) => instanceMap[targetKey]).filter(Boolean),
|
|
);
|
|
});
|
|
}
|
|
|
|
if (associationType == 'BelongsTo') {
|
|
const foreignKey = association.foreignKey;
|
|
const targetKey = association.targetKey;
|
|
|
|
for (const instance of node.instances) {
|
|
const parentInstances = node.parent.instances.filter(
|
|
(parentInstance) => parentInstance.get(foreignKey) == instance.get(targetKey),
|
|
);
|
|
|
|
for (const parentInstance of parentInstances) {
|
|
parentInstance.setDataValue(association.as, instance);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (associationType == 'BelongsToMany') {
|
|
const sourceKey = association.sourceKey;
|
|
const foreignKey = association.foreignKey;
|
|
|
|
const as = association.oneFromTarget.as;
|
|
|
|
for (const instance of node.instances) {
|
|
// set instance accessor
|
|
instance[as] = instance.dataValues[as] = instance['_pivot_'];
|
|
delete instance.dataValues['_pivot_'];
|
|
delete instance['_pivot_'];
|
|
|
|
const parentInstance = node.parent.instances.find(
|
|
(parentInstance) => parentInstance.get(sourceKey) == instance.dataValues[as].get(foreignKey),
|
|
);
|
|
|
|
if (parentInstance) {
|
|
const children = parentInstance.getDataValue(association.as);
|
|
|
|
if (!children) {
|
|
parentInstance.setDataValue(association.as, [instance]);
|
|
} else {
|
|
children.push(instance);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const parent of node.parent.instances) {
|
|
setParentAccessor(parent);
|
|
}
|
|
}
|
|
};
|
|
|
|
await loadRecursive(this.root);
|
|
|
|
const appendChildCollectionName = appendChildCollectionNameAfterRepositoryFind(this.db);
|
|
|
|
const setInstanceAttributes = (node) => {
|
|
if (node.inspectInheritAttribute) {
|
|
appendChildCollectionName({
|
|
findOptions: {},
|
|
data: node.instances,
|
|
dataCollection: this.db.modelCollection.get(node.model),
|
|
});
|
|
}
|
|
|
|
// skip pivot attributes
|
|
if (node.association?.as == '_pivot_') {
|
|
return;
|
|
}
|
|
|
|
// if no attributes are specified, return empty fields
|
|
const nodeRawAttributes = node.rawAttributes || [];
|
|
|
|
if (!lodash.isArray(nodeRawAttributes)) {
|
|
return;
|
|
}
|
|
|
|
const nodeChildrenAs = node.children.map((child) => child.association.as);
|
|
|
|
const includeAttributes = [...nodeRawAttributes, ...nodeChildrenAs];
|
|
|
|
if (node.inspectInheritAttribute) {
|
|
includeAttributes.push('__schemaName', '__tableName', '__collection');
|
|
}
|
|
|
|
for (const instance of node.instances) {
|
|
instance.dataValues = lodash.pick(instance.dataValues, includeAttributes);
|
|
}
|
|
};
|
|
|
|
// traverse tree and set instance attributes
|
|
const traverse = (node) => {
|
|
setInstanceAttributes(node);
|
|
|
|
for (const child of node.children) {
|
|
traverse(child);
|
|
}
|
|
};
|
|
|
|
traverse(this.root);
|
|
return result;
|
|
}
|
|
}
|