UNPKG

5.75 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
2// Node module: @loopback/repository
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {Filter, InclusionFilter} from '@loopback/filter';
7import {includeFieldIfNot, InvalidPolymorphismError} from '../../';
8import {AnyObject, Options} from '../../common-types';
9import {Entity} from '../../model';
10import {EntityCrudRepository} from '../../repositories';
11import {
12 deduplicate,
13 findByForeignKeys,
14 flattenTargetsOfOneToOneRelation,
15 StringKeyOf,
16} from '../relation.helpers';
17import {
18 BelongsToDefinition,
19 Getter,
20 InclusionResolver,
21} from '../relation.types';
22import {resolveBelongsToMetadata} from './belongs-to.helpers';
23
24/**
25 * Creates InclusionResolver for BelongsTo relation.
26 * Notice that this function only generates the inclusionResolver.
27 * It doesn't register it for the source repository.
28 *
29 * Notice: scope field for inclusion is not supported yet
30 *
31 * @param meta - resolved BelongsToMetadata
32 * @param getTargetRepoDict - dictionary of target model type - target repository
33 * i.e where related instances for different types are
34 */
35export function createBelongsToInclusionResolver<
36 Target extends Entity,
37 TargetID,
38 TargetRelations extends object,
39>(
40 meta: BelongsToDefinition,
41 getTargetRepoDict: {
42 [repoType: string]: Getter<
43 EntityCrudRepository<Target, TargetID, TargetRelations>
44 >;
45 },
46): InclusionResolver<Entity, Target> {
47 const relationMeta = resolveBelongsToMetadata(meta);
48
49 return async function fetchBelongsToModel(
50 entities: Entity[],
51 inclusion: InclusionFilter,
52 options?: Options,
53 ): Promise<((Target & object) | undefined)[]> {
54 if (!entities.length) return [];
55
56 // Source ids are grouped by their target polymorphic types
57 // Each type search for target instances and then merge together in a merge-sort-like manner
58
59 const sourceKey = relationMeta.keyFrom;
60 const targetKey = relationMeta.keyTo as StringKeyOf<Target>;
61 const targetDiscriminator: keyof Entity | undefined =
62 relationMeta.polymorphic
63 ? (relationMeta.polymorphic.discriminator as keyof Entity)
64 : undefined;
65
66 const scope =
67 typeof inclusion === 'string' ? {} : (inclusion.scope as Filter<Target>);
68
69 // sourceIds in {targetType -> sourceId}
70 const sourceIdsCategorized: {
71 [concreteItemType: string]: Target[StringKeyOf<Target>][];
72 } = {};
73 if (targetDiscriminator) {
74 entities.forEach((value, index, allEntites) => {
75 const concreteType = String(value[targetDiscriminator]);
76 if (!getTargetRepoDict[concreteType]) {
77 throw new InvalidPolymorphismError(concreteType, targetDiscriminator);
78 }
79 if (!sourceIdsCategorized[concreteType]) {
80 sourceIdsCategorized[concreteType] = [];
81 }
82 sourceIdsCategorized[concreteType].push(
83 (value as AnyObject)[sourceKey],
84 );
85 });
86 } else {
87 const concreteType = relationMeta.target().name;
88 if (!getTargetRepoDict[concreteType]) {
89 throw new InvalidPolymorphismError(concreteType);
90 }
91 entities.forEach((value, index, allEntites) => {
92 if (!sourceIdsCategorized[concreteType]) {
93 sourceIdsCategorized[concreteType] = [];
94 }
95 sourceIdsCategorized[concreteType].push(
96 (value as AnyObject)[sourceKey],
97 );
98 });
99 }
100
101 // Ensure targetKey is included otherwise flatten function cannot work
102 const changedTargetKeyField = includeFieldIfNot(scope?.fields, targetKey);
103 let needToRemoveTargetKeyFieldLater = false;
104 if (changedTargetKeyField !== false) {
105 scope.fields = changedTargetKeyField;
106 needToRemoveTargetKeyFieldLater = true;
107 }
108 // Each sourceIds array with same target type extract target instances
109 const targetCategorized: {
110 [concreteItemType: string]: ((Target & TargetRelations) | undefined)[];
111 } = {};
112 for (const k of Object.keys(sourceIdsCategorized)) {
113 const targetRepo = await getTargetRepoDict[k]();
114 const targetsFound = await findByForeignKeys(
115 targetRepo,
116 targetKey,
117 deduplicate(sourceIdsCategorized[k]).filter(e => e),
118 scope,
119 options,
120 );
121 targetCategorized[k] = flattenTargetsOfOneToOneRelation(
122 sourceIdsCategorized[k],
123 targetsFound,
124 targetKey,
125 );
126
127 // Remove targetKey if should be excluded but included above
128 if (needToRemoveTargetKeyFieldLater) {
129 targetCategorized[k] = targetCategorized[k].map(e => {
130 if (e) {
131 delete e[targetKey];
132 }
133 return e;
134 });
135 }
136 }
137
138 // Merge
139 // Why the order is correct:
140 // e.g. target model 1 = a, target model 2 = b
141 // all entities: [S(a-1), S(a-2), S(b-3), S(a-4), S(b-5)]
142 // a-result: [a-1, a-2, a-4]
143 // b-result: [b-3, b-4]
144 // merged:
145 // entities[1]->a => targets: [a-1 from a-result.shift()]
146 // entities[2]->a => targets: [a-1, a-2 from a-result.shift()]
147 // entities[3]->b => targets: [a-1, a-2, b-3 from b-result.shift()]
148 // entities[4]->a => targets: [a-1, a-2, b-3, a-4 from a-result.shift()]
149 // entities[5]->b => targets: [a-1, a-2, b-3, a-4, b-5 from b-result.shift()]
150 if (targetDiscriminator) {
151 const allTargets: ((Target & TargetRelations) | undefined)[] = [];
152 entities.forEach((value, index, allEntites) => {
153 allTargets.push(
154 targetCategorized[String(value[targetDiscriminator])].shift(),
155 );
156 });
157 return allTargets;
158 } else {
159 return targetCategorized[relationMeta.target().name];
160 }
161 };
162}