@loopback/repository
Version:
Define and implement a common set of interfaces for interacting with databases
159 lines (147 loc) • 5.71 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2019,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, InclusionFilter} from '@loopback/filter';
import {includeFieldIfNot, InvalidPolymorphismError} from '../../';
import {AnyObject, Options} from '../../common-types';
import {Entity} from '../../model';
import {EntityCrudRepository} from '../../repositories';
import {
findByForeignKeys,
flattenTargetsOfOneToOneRelation,
StringKeyOf,
} from '../relation.helpers';
import {Getter, HasOneDefinition, InclusionResolver} from '../relation.types';
import {resolveHasOneMetadata} from './has-one.helpers';
/**
* Creates InclusionResolver for HasOne relation.
* Notice that this function only generates the inclusionResolver.
* It doesn't register it for the source repository.
*
* Notice: scope field for inclusion is not supported yet.
*
* @param meta - resolved HasOneMetadata
* @param getTargetRepoDict - dictionary of target model type - target repository
* i.e where related instances for different types are
*/
export function createHasOneInclusionResolver<
Target extends Entity,
TargetID,
TargetRelations extends object,
>(
meta: HasOneDefinition,
getTargetRepoDict: {
[repoType: string]: Getter<
EntityCrudRepository<Target, TargetID, TargetRelations>
>;
},
): InclusionResolver<Entity, Target> {
const relationMeta = resolveHasOneMetadata(meta);
return async function fetchHasOneModel(
entities: Entity[],
inclusion: InclusionFilter,
options?: Options,
): Promise<((Target & TargetRelations) | undefined)[]> {
if (!entities.length) return [];
// Source ids are grouped by their target polymorphic types
// Each type search for target instances and then merge together in a merge-sort-like manner
const sourceKey = relationMeta.keyFrom;
const targetKey = relationMeta.keyTo as StringKeyOf<Target>;
const targetDiscriminator: keyof Entity | undefined =
relationMeta.polymorphic
? (relationMeta.polymorphic.discriminator as keyof Entity)
: undefined;
const scope =
typeof inclusion === 'string' ? {} : (inclusion.scope as Filter<Target>);
// sourceIds in {targetType -> sourceId}
const sourceIdsCategorized: {
[concreteItemType: string]: Target[StringKeyOf<Target>][];
} = {};
if (targetDiscriminator) {
entities.forEach((value, index, allEntites) => {
const concreteType = String(value[targetDiscriminator]);
if (!getTargetRepoDict[concreteType]) {
throw new InvalidPolymorphismError(concreteType, targetDiscriminator);
}
if (!sourceIdsCategorized[concreteType]) {
sourceIdsCategorized[concreteType] = [];
}
sourceIdsCategorized[concreteType].push(
(value as AnyObject)[sourceKey],
);
});
} else {
const concreteType = relationMeta.target().name;
if (!getTargetRepoDict[concreteType]) {
throw new InvalidPolymorphismError(concreteType);
}
entities.forEach((value, index, allEntites) => {
if (!sourceIdsCategorized[concreteType]) {
sourceIdsCategorized[concreteType] = [];
}
sourceIdsCategorized[concreteType].push(
(value as AnyObject)[sourceKey],
);
});
}
// Ensure targetKey is included otherwise flatten function cannot work
const changedTargetKeyField = includeFieldIfNot(scope?.fields, targetKey);
let needToRemoveTargetKeyFieldLater = false;
if (changedTargetKeyField !== false) {
scope.fields = changedTargetKeyField;
needToRemoveTargetKeyFieldLater = true;
}
// Each sourceIds array with same target type extract target instances
const targetCategorized: {
[concreteItemType: string]: ((Target & TargetRelations) | undefined)[];
} = {};
for (const k of Object.keys(sourceIdsCategorized)) {
const targetRepo = await getTargetRepoDict[k]();
const targetsFound = await findByForeignKeys(
targetRepo,
targetKey,
sourceIdsCategorized[k],
scope,
{...options, polymorphicType: k},
);
targetCategorized[k] = flattenTargetsOfOneToOneRelation(
sourceIdsCategorized[k],
targetsFound,
targetKey,
);
// Remove targetKey if should be excluded but included above
if (needToRemoveTargetKeyFieldLater) {
targetCategorized[k] = targetCategorized[k].map(e => {
if (e) {
delete e[targetKey];
}
return e;
});
}
}
// Merge
// Why the order is correct:
// e.g. target model 1 = a, target model 2 = b
// all entities: [S(a-1), S(a-2), S(b-3), S(a-4), S(b-5)]
// a-result: [a-1, a-2, a-4]
// b-result: [b-3, b-4]
// merged:
// entities[1]->a => targets: [a-1 from a-result.shift()]
// entities[2]->a => targets: [a-1, a-2 from a-result.shift()]
// entities[3]->b => targets: [a-1, a-2, b-3 from b-result.shift()]
// entities[4]->a => targets: [a-1, a-2, b-3, a-4 from a-result.shift()]
// entities[5]->b => targets: [a-1, a-2, b-3, a-4, b-5 from b-result.shift()]
if (targetDiscriminator) {
const allTargets: ((Target & TargetRelations) | undefined)[] = [];
entities.forEach((value, index, allEntites) => {
allTargets.push(
targetCategorized[String(value[targetDiscriminator])].shift(),
);
});
return allTargets;
} else {
return targetCategorized[relationMeta.target().name];
}
};
}