@loopback/repository
Version:
Define and implement a common set of interfaces for interacting with databases
291 lines (265 loc) • 10.9 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved.
// Node module: @loopback/repository
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {Filter, FilterBuilder, InclusionFilter} from '@loopback/filter';
import debugFactory from 'debug';
import _ from 'lodash';
import {InvalidPolymorphismError} from '../..';
import {AnyObject, Options} from '../../common-types';
import {Entity} from '../../model';
import {EntityCrudRepository} from '../../repositories';
import {
StringKeyOf,
findByForeignKeys,
flattenTargetsOfOneToManyRelation,
} from '../relation.helpers';
import {Getter, HasManyDefinition, InclusionResolver} from '../relation.types';
import {resolveHasManyThroughMetadata} from './has-many-through.helpers';
const debug = debugFactory(
'loopback:repository:relations:has-many-through:inclusion-resolver',
);
/**
* Creates InclusionResolver for HasManyThrough relation.
* Notice that this function only generates the inclusionResolver.
* It doesn't register it for the source repository.
*
*
* @param meta - metadata of the hasMany relation (including through)
* @param getThroughRepo - through repository getter i.e. where through
* instances are
* @param getTargetRepo - target repository getter i.e where target instances
* are
*/
export function createHasManyThroughInclusionResolver<
Through extends Entity,
ThroughID,
ThroughRelations extends object,
Target extends Entity,
TargetID,
TargetRelations extends object,
>(
meta: HasManyDefinition,
getThroughRepo: Getter<
EntityCrudRepository<Through, ThroughID, ThroughRelations>
>,
getTargetRepoDict: {
[repoType: string]: Getter<
EntityCrudRepository<Target, TargetID, TargetRelations>
>;
},
): InclusionResolver<Entity, Target> {
const relationMeta = resolveHasManyThroughMetadata(meta);
return async function fetchHasManyThroughModels(
entities: Entity[],
inclusion: InclusionFilter,
options?: Options,
): Promise<((Target & TargetRelations)[] | undefined)[]> {
if (!relationMeta.through) {
throw new Error(
`relationMeta.through must be defined on ${relationMeta}`,
);
}
if (!entities.length) return [];
debug('Fetching target models for entities:', entities);
debug('Relation metadata:', relationMeta);
const sourceKey = relationMeta.keyFrom;
const sourceIds = entities.map(e => (e as AnyObject)[sourceKey]);
const targetKey = relationMeta.keyTo as StringKeyOf<Target>;
if (!relationMeta.through) {
throw new Error(
`relationMeta.through must be defined on ${relationMeta}`,
);
}
const throughKeyTo = relationMeta.through.keyTo as StringKeyOf<Through>;
const throughKeyFrom = relationMeta.through.keyFrom as StringKeyOf<Through>;
debug('Parameters:', {
sourceKey,
sourceIds,
targetKey,
throughKeyTo,
throughKeyFrom,
});
debug(
'sourceId types',
sourceIds.map(i => typeof i),
);
const throughRepo = await getThroughRepo();
// find through models
const throughFound = await findByForeignKeys(
throughRepo,
throughKeyFrom,
sourceIds,
{}, // scope will be applied at the target level
options,
);
const throughResult = flattenTargetsOfOneToManyRelation(
sourceIds,
throughFound,
throughKeyFrom,
);
const scope =
typeof inclusion === 'string'
? {}
: (inclusion.scope as Filter<Target> | undefined);
// whether the polymorphism is configured
const targetDiscriminator: keyof (Through & ThroughRelations) | undefined =
relationMeta.through!.polymorphic
? (relationMeta.through!.polymorphic.discriminator as keyof (Through &
ThroughRelations))
: undefined;
if (targetDiscriminator) {
// put through results into arrays based on the target polymorphic types
const throughArrayByTargetType: {
[targetType: string]: (Through & ThroughRelations)[];
} = {};
for (const throughArray of throughResult) {
if (throughArray) {
for (const throughItem of throughArray) {
const targetType = String(throughItem[targetDiscriminator]);
if (!getTargetRepoDict[targetType]) {
throw new InvalidPolymorphismError(
targetType,
String(targetDiscriminator),
);
}
if (!throughArrayByTargetType[targetType]) {
throughArrayByTargetType[targetType] = [];
}
throughArrayByTargetType[targetType].push(throughItem);
}
}
}
// get targets based on their polymorphic types
const targetOfTypes: {
[targetType: string]: (Target & TargetRelations)[];
} = {};
for (const targetType of Object.keys(throughArrayByTargetType)) {
const targetIds = throughArrayByTargetType[targetType].map(
throughItem => throughItem[throughKeyTo],
);
const targetRepo = await getTargetRepoDict[targetType]();
const targetEntityList = await findByForeignKeys<
Target,
TargetRelations,
StringKeyOf<Target>
>(targetRepo, targetKey, targetIds as unknown as [], scope, options);
targetOfTypes[targetType] = targetEntityList;
}
// put targets into arrays reflecting their throughs
// Why the order is correct:
// e.g. through model = T(target instance), target model 1 = a, target model 2 = b
// all entities: [S1, S2, S2]
// through-result: [[T(b-11), T(a-12), T(b-13), T(b-14)], [T(a-21), T(a-22), T(b-23)], [T(b-31), T(b-32), T(a-33)]]
// through-array-by-target-type: {a:[T(a-12), T(a-21), T(a-22), T(a-33)] b: [T(b-11), T(b-13), T(b-14), T(b-23), T(b-31), T(b-32)]}
// target-array-by-target-type: {a:[a-12, a-21, a-22, a-33] b: [b-11, b-13, b-14, b-23, b-31, b-32]}
// merged:
// through-result[0][0]->b => targets: [[b-11 from b.shift()]]
// through-result[0][1]->a => targets: [[b-11, a-12 from a.shift()]]
// through-result[0][2]->b => targets: [[b-11, a-12, b-13 from b.shift()]]
// through-result[0][3]->b => targets: [[b-11, a-12, b-13, b-14 from b.shift()]]
// through-result[1][0]->a => targets: [[b-11, a-12, b-13, b-14], [a-21, from a.shift()]]
// through-result[1][1]->a => targets: [[b-11, a-12, b-13, b-14], [a-21, a-22 from a.shift()]]
// through-result[1][2]->b => targets: [[b-11, a-12, b-13, b-14], [a-21, a-22, b-23 from b.shift()]]
// through-result[2][0]->b => targets: [[b-11, a-12, b-13, b-14], [a-21, a-22, b-23], [b-31, from b.shift()]]
// through-result[2][1]->b => targets: [[b-11, a-12, b-13, b-14], [a-21, a-22, b-23], [b-31, b-32 from b.shift()]]
// through-result[2][1]->b => targets: [[b-11, a-12, b-13, b-14], [a-21, a-22, b-23], [b-31, b-32, a-33 from a.shift()]]
const allTargetsOfThrough: ((Target & TargetRelations)[] | undefined)[] =
[];
for (const throughArray of throughResult) {
if (throughArray && throughArray.length > 0) {
const currentTargetThroughArray: (Target & TargetRelations)[] = [];
for (const throughItem of throughArray) {
const itemToAdd =
targetOfTypes[String(throughItem[targetDiscriminator])].shift();
if (itemToAdd) {
currentTargetThroughArray.push(itemToAdd);
}
}
allTargetsOfThrough.push(currentTargetThroughArray);
} else {
allTargetsOfThrough.push(undefined);
}
}
return allTargetsOfThrough;
} else {
const targetRepo = await getTargetRepoDict[relationMeta.target().name]();
const result = [];
// Normalize field filter to an object like {[field]: boolean}
const filterBuilder = new FilterBuilder<Target>();
const fieldFilter = filterBuilder.fields(scope?.fields ?? {}).filter
.fields as {[P in keyof Target]?: boolean};
// We need targetKey to create a map, as such it always needs to be included.
// Keep track of whether targetKey should be removed from the final result,
// whether by explicit omission (targetKey: false) or by implicit omission
// (anyOtherKey: true but no targetKey: true).
const omitTargetKeyFromFields =
(Object.values(fieldFilter).includes(true) &&
fieldFilter[targetKey] !== true) ||
fieldFilter[targetKey] === false;
if (omitTargetKeyFromFields) {
if (fieldFilter[targetKey] === false) {
// Undo explicit omission
delete fieldFilter[targetKey];
} else {
// Undo implicit omission
fieldFilter[targetKey] = true;
}
}
// get target ids from the through entities by foreign key
const allIds = _.uniq(
throughResult
.filter(throughEntitySet => throughEntitySet !== undefined)
.map(throughEntitySet =>
throughEntitySet?.map(entity => entity[throughKeyTo]),
)
.flat(),
);
// Omit limit from scope as those need to be applied per fK
const targetEntityList = await findByForeignKeys<
Target,
TargetRelations,
StringKeyOf<Target>
>(
targetRepo,
targetKey,
allIds as unknown as [],
{..._.omit(scope ?? {}, ['limit', 'fields']), fields: fieldFilter},
{
...options,
isThroughModelInclude: true,
},
);
const targetEntityIds = targetEntityList.map(
targetEntity =>
targetEntity[targetKey]?.toString() ?? targetEntity[targetKey],
);
const targetEntityMap = Object.fromEntries(
targetEntityList.map(x => [
x[targetKey],
omitTargetKeyFromFields ? _.omit(x, [targetKey]) : x,
]),
);
// convert from through entities to the target entities
for (const entityList of throughResult) {
if (entityList) {
const relatedIds = entityList.map(
x => x[throughKeyTo]?.toString() ?? x[throughKeyTo],
);
// Use the order of the original result set & apply limit
const sortedIds = _.intersection(
targetEntityIds as unknown as string[],
relatedIds as unknown as string[],
).slice(0, scope?.limit ?? entityList.length);
// Make each result its own instance to avoid shenanigans by reference
result.push(_.cloneDeep(sortedIds.map(x => targetEntityMap[x])));
} else {
// no entities found, add undefined to results
result.push(entityList);
}
}
debug('fetchHasManyThroughModels result', result);
return result;
}
};
}