UNPKG

8.7 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 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 debugFactory from 'debug';
8import {InvalidPolymorphismError} from '../..';
9import {AnyObject, Options} from '../../common-types';
10import {Entity} from '../../model';
11import {EntityCrudRepository} from '../../repositories';
12import {
13 findByForeignKeys,
14 flattenTargetsOfOneToManyRelation,
15 StringKeyOf,
16} from '../relation.helpers';
17import {Getter, HasManyDefinition, InclusionResolver} from '../relation.types';
18import {resolveHasManyThroughMetadata} from './has-many-through.helpers';
19
20const debug = debugFactory(
21 'loopback:repository:relations:has-many-through:inclusion-resolver',
22);
23
24/**
25 * Creates InclusionResolver for HasManyThrough relation.
26 * Notice that this function only generates the inclusionResolver.
27 * It doesn't register it for the source repository.
28 *
29 *
30 * @param meta - metadata of the hasMany relation (including through)
31 * @param getThroughRepo - through repository getter i.e. where through
32 * instances are
33 * @param getTargetRepo - target repository getter i.e where target instances
34 * are
35 */
36export function createHasManyThroughInclusionResolver<
37 Through extends Entity,
38 ThroughID,
39 ThroughRelations extends object,
40 Target extends Entity,
41 TargetID,
42 TargetRelations extends object,
43>(
44 meta: HasManyDefinition,
45 getThroughRepo: Getter<
46 EntityCrudRepository<Through, ThroughID, ThroughRelations>
47 >,
48 getTargetRepoDict: {
49 [repoType: string]: Getter<
50 EntityCrudRepository<Target, TargetID, TargetRelations>
51 >;
52 },
53): InclusionResolver<Entity, Target> {
54 const relationMeta = resolveHasManyThroughMetadata(meta);
55
56 return async function fetchHasManyThroughModels(
57 entities: Entity[],
58 inclusion: InclusionFilter,
59 options?: Options,
60 ): Promise<((Target & TargetRelations)[] | undefined)[]> {
61 if (!relationMeta.through) {
62 throw new Error(
63 `relationMeta.through must be defined on ${relationMeta}`,
64 );
65 }
66
67 if (!entities.length) return [];
68
69 debug('Fetching target models for entities:', entities);
70 debug('Relation metadata:', relationMeta);
71
72 const sourceKey = relationMeta.keyFrom;
73 const sourceIds = entities.map(e => (e as AnyObject)[sourceKey]);
74 const targetKey = relationMeta.keyTo as StringKeyOf<Target>;
75 if (!relationMeta.through) {
76 throw new Error(
77 `relationMeta.through must be defined on ${relationMeta}`,
78 );
79 }
80 const throughKeyTo = relationMeta.through.keyTo as StringKeyOf<Through>;
81 const throughKeyFrom = relationMeta.through.keyFrom as StringKeyOf<Through>;
82
83 debug('Parameters:', {
84 sourceKey,
85 sourceIds,
86 targetKey,
87 throughKeyTo,
88 throughKeyFrom,
89 });
90
91 debug(
92 'sourceId types',
93 sourceIds.map(i => typeof i),
94 );
95
96 const throughRepo = await getThroughRepo();
97
98 // find through models
99 const throughFound = await findByForeignKeys(
100 throughRepo,
101 throughKeyFrom,
102 sourceIds,
103 {}, // scope will be applied at the target level
104 options,
105 );
106
107 const throughResult = flattenTargetsOfOneToManyRelation(
108 sourceIds,
109 throughFound,
110 throughKeyFrom,
111 );
112
113 const scope =
114 typeof inclusion === 'string' ? {} : (inclusion.scope as Filter<Target>);
115
116 // whether the polymorphism is configured
117 const targetDiscriminator: keyof (Through & ThroughRelations) | undefined =
118 relationMeta.through!.polymorphic
119 ? (relationMeta.through!.polymorphic.discriminator as keyof (Through &
120 ThroughRelations))
121 : undefined;
122 if (targetDiscriminator) {
123 // put through results into arrays based on the target polymorphic types
124 const throughArrayByTargetType: {
125 [targetType: string]: (Through & ThroughRelations)[];
126 } = {};
127 for (const throughArray of throughResult) {
128 if (throughArray) {
129 for (const throughItem of throughArray) {
130 const targetType = String(throughItem[targetDiscriminator]);
131 if (!getTargetRepoDict[targetType]) {
132 throw new InvalidPolymorphismError(
133 targetType,
134 String(targetDiscriminator),
135 );
136 }
137 if (!throughArrayByTargetType[targetType]) {
138 throughArrayByTargetType[targetType] = [];
139 }
140 throughArrayByTargetType[targetType].push(throughItem);
141 }
142 }
143 }
144 // get targets based on their polymorphic types
145 const targetOfTypes: {
146 [targetType: string]: (Target & TargetRelations)[];
147 } = {};
148 for (const targetType of Object.keys(throughArrayByTargetType)) {
149 const targetIds = throughArrayByTargetType[targetType].map(
150 throughItem => throughItem[throughKeyTo],
151 );
152 const targetRepo = await getTargetRepoDict[targetType]();
153 const targetEntityList = await findByForeignKeys<
154 Target,
155 TargetRelations,
156 StringKeyOf<Target>
157 >(targetRepo, targetKey, targetIds as unknown as [], scope, options);
158 targetOfTypes[targetType] = targetEntityList;
159 }
160 // put targets into arrays reflecting their throughs
161 // Why the order is correct:
162 // e.g. through model = T(target instance), target model 1 = a, target model 2 = b
163 // all entities: [S1, S2, S2]
164 // 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)]]
165 // 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)]}
166 // 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]}
167 // merged:
168 // through-result[0][0]->b => targets: [[b-11 from b.shift()]]
169 // through-result[0][1]->a => targets: [[b-11, a-12 from a.shift()]]
170 // through-result[0][2]->b => targets: [[b-11, a-12, b-13 from b.shift()]]
171 // through-result[0][3]->b => targets: [[b-11, a-12, b-13, b-14 from b.shift()]]
172 // through-result[1][0]->a => targets: [[b-11, a-12, b-13, b-14], [a-21, from a.shift()]]
173 // through-result[1][1]->a => targets: [[b-11, a-12, b-13, b-14], [a-21, a-22 from a.shift()]]
174 // through-result[1][2]->b => targets: [[b-11, a-12, b-13, b-14], [a-21, a-22, b-23 from b.shift()]]
175 // 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()]]
176 // 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()]]
177 // 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()]]
178 const allTargetsOfThrough: ((Target & TargetRelations)[] | undefined)[] =
179 [];
180 for (const throughArray of throughResult) {
181 if (throughArray && throughArray.length > 0) {
182 const currentTargetThroughArray: (Target & TargetRelations)[] = [];
183 for (const throughItem of throughArray) {
184 const itemToAdd =
185 targetOfTypes[String(throughItem[targetDiscriminator])].shift();
186 if (itemToAdd) {
187 currentTargetThroughArray.push(itemToAdd);
188 }
189 }
190 allTargetsOfThrough.push(currentTargetThroughArray);
191 } else {
192 allTargetsOfThrough.push(undefined);
193 }
194 }
195 return allTargetsOfThrough;
196 } else {
197 const targetRepo = await getTargetRepoDict[relationMeta.target().name]();
198 const result = [];
199
200 // convert from through entities to the target entities
201 for (const entityList of throughResult) {
202 if (entityList) {
203 // get target ids from the through entities by foreign key
204 const targetIds = entityList.map(entity => entity[throughKeyTo]);
205
206 // the explicit types and casts are needed
207 const targetEntityList = await findByForeignKeys<
208 Target,
209 TargetRelations,
210 StringKeyOf<Target>
211 >(targetRepo, targetKey, targetIds as unknown as [], scope, {
212 ...options,
213 isThroughModelInclude: true,
214 });
215 result.push(targetEntityList);
216 } else {
217 // no entities found, add undefined to results
218 result.push(entityList);
219 }
220 }
221
222 debug('fetchHasManyThroughModels result', result);
223 return result;
224 }
225 };
226}