Skip to content

Commit 34f8201

Browse files
0x0aNLdhmlau
authored andcommitted
fix(repository): improve HasManyThrough inclusion performance
Signed-off-by: Roderik van Heijst <rvanheijst@0x0a.nl>
1 parent 4ffd20f commit 34f8201

1 file changed

Lines changed: 78 additions & 16 deletions

File tree

packages/repository/src/relations/has-many/has-many-through.inclusion-resolver.ts

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {Filter, InclusionFilter} from '@loopback/filter';
6+
import {Filter, FilterBuilder, InclusionFilter} from '@loopback/filter';
77
import debugFactory from 'debug';
8+
import _ from 'lodash';
89
import {InvalidPolymorphismError} from '../..';
910
import {AnyObject, Options} from '../../common-types';
1011
import {Entity} from '../../model';
1112
import {EntityCrudRepository} from '../../repositories';
1213
import {
14+
StringKeyOf,
1315
findByForeignKeys,
1416
flattenTargetsOfOneToManyRelation,
15-
StringKeyOf,
1617
} from '../relation.helpers';
1718
import {Getter, HasManyDefinition, InclusionResolver} from '../relation.types';
1819
import {resolveHasManyThroughMetadata} from './has-many-through.helpers';
@@ -111,7 +112,9 @@ export function createHasManyThroughInclusionResolver<
111112
);
112113

113114
const scope =
114-
typeof inclusion === 'string' ? {} : (inclusion.scope as Filter<Target>);
115+
typeof inclusion === 'string'
116+
? {}
117+
: (inclusion.scope as Filter<Target> | undefined);
115118

116119
// whether the polymorphism is configured
117120
const targetDiscriminator: keyof (Through & ThroughRelations) | undefined =
@@ -197,22 +200,81 @@ export function createHasManyThroughInclusionResolver<
197200
const targetRepo = await getTargetRepoDict[relationMeta.target().name]();
198201
const result = [];
199202

203+
// Normalize field filter to an object like {[field]: boolean}
204+
const filterBuilder = new FilterBuilder<Target>();
205+
const fieldFilter = filterBuilder.fields(scope?.fields ?? {}).filter
206+
.fields as {[P in keyof Target]?: boolean};
207+
208+
// We need targetKey to create a map, as such it always needs to be included.
209+
// Keep track of whether targetKey should be removed from the final result,
210+
// whether by explicit omission (targetKey: false) or by implicit omission
211+
// (anyOtherKey: true but no targetKey: true).
212+
const omitTargetKeyFromFields =
213+
(Object.values(fieldFilter).includes(true) &&
214+
fieldFilter[targetKey] !== true) ||
215+
fieldFilter[targetKey] === false;
216+
217+
if (omitTargetKeyFromFields) {
218+
if (fieldFilter[targetKey] === false) {
219+
// Undo explicit omission
220+
delete fieldFilter[targetKey];
221+
} else {
222+
// Undo implicit omission
223+
fieldFilter[targetKey] = true;
224+
}
225+
}
226+
227+
// get target ids from the through entities by foreign key
228+
const allIds = _.uniq(
229+
throughResult
230+
.filter(throughEntitySet => throughEntitySet !== undefined)
231+
.map(
232+
throughEntitySet =>
233+
throughEntitySet?.map(entity => entity[throughKeyTo]),
234+
)
235+
.flat(),
236+
);
237+
238+
// Omit limit from scope as those need to be applied per fK
239+
const targetEntityList = await findByForeignKeys<
240+
Target,
241+
TargetRelations,
242+
StringKeyOf<Target>
243+
>(
244+
targetRepo,
245+
targetKey,
246+
allIds as unknown as [],
247+
{..._.omit(scope ?? {}, ['limit', 'fields']), fields: fieldFilter},
248+
{
249+
...options,
250+
isThroughModelInclude: true,
251+
},
252+
);
253+
254+
const targetEntityIds = targetEntityList.map(
255+
targetEntity => targetEntity[targetKey],
256+
);
257+
258+
const targetEntityMap = Object.fromEntries(
259+
targetEntityList.map(x => [
260+
x[targetKey],
261+
omitTargetKeyFromFields ? _.omit(x, [targetKey]) : x,
262+
]),
263+
);
264+
200265
// convert from through entities to the target entities
201266
for (const entityList of throughResult) {
202267
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);
268+
const relatedIds = entityList.map(x => x[throughKeyTo]);
269+
270+
// Use the order of the original result set & apply limit
271+
const sortedIds = _.intersection(
272+
targetEntityIds as unknown as string[],
273+
relatedIds as unknown as string[],
274+
).slice(0, scope?.limit ?? entityList.length);
275+
276+
// Make each result its own instance to avoid shenanigans by reference
277+
result.push(_.cloneDeep(sortedIds.map(x => targetEntityMap[x])));
216278
} else {
217279
// no entities found, add undefined to results
218280
result.push(entityList);

0 commit comments

Comments
 (0)