|
3 | 3 | // This file is licensed under the MIT License. |
4 | 4 | // License text available at https://opensource.org/licenses/MIT |
5 | 5 |
|
6 | | -import {Filter, InclusionFilter} from '@loopback/filter'; |
| 6 | +import {Filter, FilterBuilder, InclusionFilter} from '@loopback/filter'; |
7 | 7 | import debugFactory from 'debug'; |
| 8 | +import _ from 'lodash'; |
8 | 9 | import {InvalidPolymorphismError} from '../..'; |
9 | 10 | import {AnyObject, Options} from '../../common-types'; |
10 | 11 | import {Entity} from '../../model'; |
11 | 12 | import {EntityCrudRepository} from '../../repositories'; |
12 | 13 | import { |
| 14 | + StringKeyOf, |
13 | 15 | findByForeignKeys, |
14 | 16 | flattenTargetsOfOneToManyRelation, |
15 | | - StringKeyOf, |
16 | 17 | } from '../relation.helpers'; |
17 | 18 | import {Getter, HasManyDefinition, InclusionResolver} from '../relation.types'; |
18 | 19 | import {resolveHasManyThroughMetadata} from './has-many-through.helpers'; |
@@ -111,7 +112,9 @@ export function createHasManyThroughInclusionResolver< |
111 | 112 | ); |
112 | 113 |
|
113 | 114 | const scope = |
114 | | - typeof inclusion === 'string' ? {} : (inclusion.scope as Filter<Target>); |
| 115 | + typeof inclusion === 'string' |
| 116 | + ? {} |
| 117 | + : (inclusion.scope as Filter<Target> | undefined); |
115 | 118 |
|
116 | 119 | // whether the polymorphism is configured |
117 | 120 | const targetDiscriminator: keyof (Through & ThroughRelations) | undefined = |
@@ -197,22 +200,81 @@ export function createHasManyThroughInclusionResolver< |
197 | 200 | const targetRepo = await getTargetRepoDict[relationMeta.target().name](); |
198 | 201 | const result = []; |
199 | 202 |
|
| 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 | + |
200 | 265 | // convert from through entities to the target entities |
201 | 266 | for (const entityList of throughResult) { |
202 | 267 | 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]))); |
216 | 278 | } else { |
217 | 279 | // no entities found, add undefined to results |
218 | 280 | result.push(entityList); |
|
0 commit comments