From b99c96b30157bc8d908765cc3f2dc4399bb7f6a0 Mon Sep 17 00:00:00 2001 From: Agnes Lin Date: Fri, 23 Aug 2019 14:23:19 -0400 Subject: [PATCH] feat(repository): implement inclusionResolver for hasMany Co-authored-by: Nora Co-authored-by: Miroslav --- docs/site/HasMany-relation.md | 92 +++++++ ....inclusion-resolver.relation.acceptance.ts | 17 +- .../belongs-to.relation.acceptance.ts | 6 +- ...-inclusion-resolver.relation.acceptance.ts | 239 ++++++++++++++++++ .../repositories/customer.repository.ts | 2 +- .../src/crud/relations/helpers.ts | 17 +- ...ts-of-one-to-many-relation-helpers.unit.ts | 37 +++ .../relations-helpers-fixtures.ts | 2 +- .../has-many/has-many-repository.factory.ts | 29 ++- .../relations/has-many/has-many.helpers.ts | 7 +- .../has-many/has-many.inclusion-resolver.ts | 80 ++++++ .../src/relations/has-many/index.ts | 1 + .../src/relations/relation.helpers.ts | 33 +++ 13 files changed, 543 insertions(+), 19 deletions(-) create mode 100644 packages/repository-tests/src/crud/relations/acceptance/has-many-inclusion-resolver.relation.acceptance.ts create mode 100644 packages/repository/src/__tests__/unit/repositories/relations-helpers/flatten-targets-of-one-to-many-relation-helpers.unit.ts create mode 100644 packages/repository/src/relations/has-many/has-many.inclusion-resolver.ts diff --git a/docs/site/HasMany-relation.md b/docs/site/HasMany-relation.md index dcf1353eb6c5..3da5c5b91c30 100644 --- a/docs/site/HasMany-relation.md +++ b/docs/site/HasMany-relation.md @@ -314,3 +314,95 @@ certain properties from the JSON/OpenAPI spec schema built for the `requestBody` payload. See its [GitHub issue](https://github.com/strongloop/loopback-next/issues/1179) to follow the discussion. " %} + +## Querying related models + +LoopBack 4 has the concept of an `inclusion resolver` in relations, which helps +to query data through an `include` filter. An inclusion resolver is a function +that can fetch target models for the given list of source model instances. +LoopBack 4 creates a different inclusion resolver for each relation type. + +Use the relation between `Customer` and `Order` we show above, a `Customer` has +many `Order`s. + +After setting up the relation in the repository class, the inclusion resolver +allows users to retrieve all customers along with their related orders through +the following code: + +```ts +customerRepo.find({include: [{relation: 'orders'}]}); +``` + +### Enable/disable the inclusion resolvers: + +- Base repository classes have a public property `inclusionResolvers`, which + maintains a map containing inclusion resolvers for each relation. +- The `inclusionResolver` of a certain relation is built when the source + repository class calls the `createHasManyRepositoryFactoryFor` function in the + constructor with the relation name. +- Call `registerInclusionResolver` to add the resolver of that relation to the + `inclusionResolvers` map. (As we realized in LB3, not all relations are + allowed to be traversed. Users can decide to which resolvers can be added.) + +The following code snippet shows how to register the inclusion resolver for the +has-many relation 'orders': + +```ts +export class CustomerRepository extends DefaultCrudRepository { + products: HasManyRepositoryFactory; + + constructor( + dataSource: juggler.DataSource, + orderRepositoryGetter: Getter, + ) { + super(Customer, dataSource); + + // we already have this line to create a HasManyRepository factory + this.orders = this.createHasManyRepositoryFactoryFor( + 'orders', + orderRepositoryGetter, + ); + + // add this line to register inclusion resolver + this.registerInclusion('orders', this.orders.inclusionResolver); + } +} +``` + +- We can simply include the relation in queries via `find()`, `findOne()`, and + `findById()` methods. Example: + + ```ts + customerRepository.find({include: [{relation: 'orders'}]}); + ``` + + which returns: + + ```ts + [ + { + id: 1, + name: 'Thor', + orders: [ + {name: 'Mjolnir', customerId: 1}, + {name: 'Rocket Raccoon', customerId: 1}, + ], + }, + { + id: 2, + name: 'Captain', + orders: [{name: 'Shield', customerId: 2}], + }, + ]; + ``` + +- You can delete a relation from `inclusionResolvers` to disable the inclusion + for a certain relation. e.g + `customerRepository.inclusionResolvers.delete('orders')` + +{% include note.html content=" +Inclusion with custom scope: +Besides specifying the relation name to include, it's also possible to specify additional scope constraints. +However, this feature is not supported yet. Check our GitHub issue for more information: +[Include related models with a custom scope](https://github.com/strongloop/loopback-next/issues/3453). +" %} diff --git a/packages/repository-tests/src/crud/relations/acceptance/belongs-to.inclusion-resolver.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/belongs-to.inclusion-resolver.relation.acceptance.ts index c2a7ed8a7d37..0cf219877e17 100644 --- a/packages/repository-tests/src/crud/relations/acceptance/belongs-to.inclusion-resolver.relation.acceptance.ts +++ b/packages/repository-tests/src/crud/relations/acceptance/belongs-to.inclusion-resolver.relation.acceptance.ts @@ -13,7 +13,6 @@ import { } from '../../..'; import { deleteAllModelsInDefaultDataSource, - MixedIdType, withCrudCtx, } from '../../../helpers.repository-tests'; import { @@ -39,7 +38,6 @@ export function belongsToInclusionResolverAcceptance( before(deleteAllModelsInDefaultDataSource); let customerRepo: CustomerRepository; let orderRepo: OrderRepository; - let existingCustomerId: MixedIdType; before( withCrudCtx(async function setupRepository(ctx: CrudTestContext) { @@ -62,9 +60,10 @@ export function belongsToInclusionResolverAcceptance( }); it('throws an error if it tries to query nonexists relation names', async () => { + const customer = await customerRepo.create({name: 'customer'}); await orderRepo.create({ - description: 'shiba', - customerId: existingCustomerId, + description: 'an order', + customerId: customer.id, }); await expect( orderRepo.find({include: [{relation: 'shipment'}]}), @@ -166,9 +165,10 @@ export function belongsToInclusionResolverAcceptance( }); // scope for inclusion is not supported yet it('throws error if the inclusion query contains a non-empty scope', async () => { + const customer = await customerRepo.create({name: 'customer'}); await orderRepo.create({ - description: 'shiba', - customerId: existingCustomerId, + description: 'an order', + customerId: customer.id, }); await expect( orderRepo.find({ @@ -178,9 +178,10 @@ export function belongsToInclusionResolverAcceptance( }); it('throws error if the target repository does not have the registered resolver', async () => { + const customer = await customerRepo.create({name: 'customer'}); await orderRepo.create({ - description: 'shiba', - customerId: existingCustomerId, + description: 'an order', + customerId: customer.id, }); // unregister the resolver orderRepo.inclusionResolvers.delete('customer'); diff --git a/packages/repository-tests/src/crud/relations/acceptance/belongs-to.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/belongs-to.relation.acceptance.ts index b65ef2b4a472..e9690125ef56 100644 --- a/packages/repository-tests/src/crud/relations/acceptance/belongs-to.relation.acceptance.ts +++ b/packages/repository-tests/src/crud/relations/acceptance/belongs-to.relation.acceptance.ts @@ -98,11 +98,13 @@ export function belongsToRelationAcceptance( name: 'Order McForder', }); const order = await orderRepo.create({ - customerId: deletedCustomer.id, // does not exist - description: 'Order of a fictional customer', + customerId: deletedCustomer.id, + description: 'custotmer will be deleted', }); await customerRepo.deleteAll(); + await orderRepo.deleteAll(); + await expect(findCustomerOfOrder(order.id)).to.be.rejectedWith( EntityNotFoundError, ); diff --git a/packages/repository-tests/src/crud/relations/acceptance/has-many-inclusion-resolver.relation.acceptance.ts b/packages/repository-tests/src/crud/relations/acceptance/has-many-inclusion-resolver.relation.acceptance.ts new file mode 100644 index 000000000000..06f6914e5802 --- /dev/null +++ b/packages/repository-tests/src/crud/relations/acceptance/has-many-inclusion-resolver.relation.acceptance.ts @@ -0,0 +1,239 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect, skipIf, toJSON} from '@loopback/testlab'; +import {Suite} from 'mocha'; +import { + CrudFeatures, + CrudRepositoryCtor, + CrudTestContext, + DataSourceOptions, +} from '../../..'; +import { + deleteAllModelsInDefaultDataSource, + withCrudCtx, +} from '../../../helpers.repository-tests'; +import { + Customer, + CustomerRepository, + Order, + OrderRepository, +} from '../fixtures/models'; +import {givenBoundCrudRepositories} from '../helpers'; + +export function hasManyInclusionResolverAcceptance( + dataSourceOptions: DataSourceOptions, + repositoryClass: CrudRepositoryCtor, + features: CrudFeatures, +) { + skipIf<[(this: Suite) => void], void>( + !features.supportsInclusionResolvers, + describe, + 'HasMany inclusion resolvers - acceptance', + suite, + ); + function suite() { + before(deleteAllModelsInDefaultDataSource); + let customerRepo: CustomerRepository; + let orderRepo: OrderRepository; + + before( + withCrudCtx(async function setupRepository(ctx: CrudTestContext) { + // this helper should create the inclusion resolvers and also + // register inclusion resolvers for us + ({customerRepo, orderRepo} = givenBoundCrudRepositories( + ctx.dataSource, + repositoryClass, + features, + )); + expect(customerRepo.orders.inclusionResolver).to.be.Function(); + + await ctx.dataSource.automigrate([Customer.name, Order.name]); + }), + ); + + beforeEach(async () => { + await customerRepo.deleteAll(); + await orderRepo.deleteAll(); + }); + + it('throws an error if tries to query nonexists relation names', async () => { + const customer = await customerRepo.create({name: 'customer'}); + await orderRepo.create({ + description: 'an order', + customerId: customer.id, + }); + await expect( + customerRepo.find({include: [{relation: 'managers'}]}), + ).to.be.rejectedWith( + `Invalid "filter.include" entries: {"relation":"managers"}`, + ); + }); + + it('returns single model instance including single related instance', async () => { + const thor = await customerRepo.create({name: 'Thor'}); + const thorOrder = await orderRepo.create({ + customerId: thor.id, + description: "Thor's Mjolnir", + }); + const result = await customerRepo.find({ + include: [{relation: 'orders'}], + }); + + expect(toJSON(result)).to.deepEqual([ + toJSON({ + ...thor, + parentId: features.emptyValue, + orders: [ + { + ...thorOrder, + isShipped: features.emptyValue, + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: features.emptyValue, + }, + ], + }), + ]); + }); + + it('returns multiple model instances including related instances', async () => { + const thor = await customerRepo.create({name: 'Thor'}); + const odin = await customerRepo.create({name: 'Odin'}); + const thorOrderMjolnir = await orderRepo.create({ + customerId: thor.id, + description: 'Mjolnir', + }); + const thorOrderPizza = await orderRepo.create({ + customerId: thor.id, + description: 'Pizza', + }); + const odinOrderCoffee = await orderRepo.create({ + customerId: odin.id, + description: 'Coffee', + }); + + const result = await customerRepo.find({ + include: [{relation: 'orders'}], + }); + + const expected = [ + { + ...thor, + orders: [ + { + ...thorOrderMjolnir, + isShipped: features.emptyValue, + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: features.emptyValue, + }, + { + ...thorOrderPizza, + isShipped: features.emptyValue, + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: features.emptyValue, + }, + ], + parentId: features.emptyValue, + }, + { + ...odin, + parentId: features.emptyValue, + orders: [ + { + ...odinOrderCoffee, + isShipped: features.emptyValue, + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: features.emptyValue, + }, + ], + }, + ]; + expect(toJSON(result)).to.deepEqual(toJSON(expected)); + }); + + it('returns a specified instance including its related model instances', async () => { + const thor = await customerRepo.create({name: 'Thor'}); + const odin = await customerRepo.create({name: 'Odin'}); + await orderRepo.create({ + customerId: thor.id, + description: 'Mjolnir', + }); + await orderRepo.create({ + customerId: thor.id, + description: 'Pizza', + }); + const odinOrder = await orderRepo.create({ + customerId: odin.id, + description: 'Coffee', + }); + + const result = await customerRepo.findById(odin.id, { + include: [{relation: 'orders'}], + }); + const expected = { + ...odin, + parentId: features.emptyValue, + orders: [ + { + ...odinOrder, + isShipped: features.emptyValue, + // eslint-disable-next-line @typescript-eslint/camelcase + shipment_id: features.emptyValue, + }, + ], + }; + expect(toJSON(result)).to.deepEqual(toJSON(expected)); + }); + + it('throws when navigational properties are present when updating model instance', async () => { + const created = await customerRepo.create({name: 'customer'}); + const customerId = created.id; + + await orderRepo.create({ + description: 'pizza', + customerId, + }); + + const found = await customerRepo.findById(customerId, { + include: [{relation: 'orders'}], + }); + expect(found.orders).to.have.lengthOf(1); + + found.name = 'updated name'; + await expect(customerRepo.save(found)).to.be.rejectedWith( + 'The `Customer` instance is not valid. Details: `orders` is not defined in the model (value: undefined).', + ); + }); + // scope for inclusion is not supported yet + it('throws error if the inclusion query contains a non-empty scope', async () => { + const customer = await customerRepo.create({name: 'customer'}); + await orderRepo.create({ + description: 'an order', + customerId: customer.id, + }); + await expect( + customerRepo.find({ + include: [{relation: 'orders', scope: {limit: 1}}], + }), + ).to.be.rejectedWith(`scope is not supported`); + }); + + it('throws error if the target repository does not have the registered resolver', async () => { + const customer = await customerRepo.create({name: 'customer'}); + await orderRepo.create({ + description: 'an order', + customerId: customer.id, + }); + // unregister the resolver + customerRepo.inclusionResolvers.delete('orders'); + + await expect( + customerRepo.find({include: [{relation: 'orders'}]}), + ).to.be.rejectedWith( + `Invalid "filter.include" entries: {"relation":"orders"}`, + ); + }); + } +} diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts index 606d34a75420..9de711d82756 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/customer.repository.ts @@ -49,8 +49,8 @@ export function createCustomerRepo(repoClass: CrudRepositoryCtor) { addressRepositoryGetter: Getter, ) { super(Customer, db); - // create a has-many relation from this public method const ordersMeta = this.entityClass.definition.relations['orders']; + // create a has-many relation through this public method this.orders = createHasManyRepositoryFactory( ordersMeta as HasManyDefinition, orderRepositoryGetter, diff --git a/packages/repository-tests/src/crud/relations/helpers.ts b/packages/repository-tests/src/crud/relations/helpers.ts index 44e93388e728..77c65eb9940e 100644 --- a/packages/repository-tests/src/crud/relations/helpers.ts +++ b/packages/repository-tests/src/crud/relations/helpers.ts @@ -45,6 +45,16 @@ export function givenBoundCrudRepositories( async () => addressRepo, ); + // register the inclusionResolvers here for customerRepo + customerRepo.inclusionResolvers.set( + 'orders', + customerRepo.orders.inclusionResolver, + ); + customerRepo.inclusionResolvers.set( + 'customers', + customerRepo.customers.inclusionResolver, + ); + const orderRepoClass = createOrderRepo(repositoryClass); const orderRepo: OrderRepository = new orderRepoClass( db, @@ -69,5 +79,10 @@ export function givenBoundCrudRepositories( async () => customerRepo, ); - return {customerRepo, orderRepo, shipmentRepo, addressRepo}; + return { + customerRepo, + orderRepo, + shipmentRepo, + addressRepo, + }; } diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/flatten-targets-of-one-to-many-relation-helpers.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/flatten-targets-of-one-to-many-relation-helpers.unit.ts new file mode 100644 index 000000000000..7030c810faab --- /dev/null +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/flatten-targets-of-one-to-many-relation-helpers.unit.ts @@ -0,0 +1,37 @@ +// Copyright IBM Corp. 2019. 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 {expect} from '@loopback/testlab'; +import {flattenTargetsOfOneToManyRelation} from '../../../..'; +import {createProduct} from './relations-helpers-fixtures'; + +describe('flattenTargetsOfOneToManyRelation', () => { + describe('gets the result of using reduceAsArray strategy for hasMany relation', () => { + it('gets the result of passing in a single sourceId', () => { + const pen = createProduct({name: 'pen', categoryId: 1}); + const pencil = createProduct({name: 'pencil', categoryId: 1}); + createProduct({name: 'eraser', categoryId: 2}); + + const result = flattenTargetsOfOneToManyRelation( + [1], + [pen, pencil], + 'categoryId', + ); + expect(result).to.eql([[pen, pencil]]); + }); + it('gets the result of passing in multiple sourceIds', () => { + const pen = createProduct({name: 'pen', categoryId: 1}); + const pencil = createProduct({name: 'pencil', categoryId: 1}); + const eraser = createProduct({name: 'eraser', categoryId: 2}); + // use [2, 1] here to show the order of sourceIds matters + const result = flattenTargetsOfOneToManyRelation( + [2, 1], + [pen, pencil, eraser], + 'categoryId', + ); + expect(result).to.deepEqual([[eraser], [pen, pencil]]); + }); + }); +}); diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts index c25d7010234c..5e1ce8b617c5 100644 --- a/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts @@ -124,7 +124,7 @@ export class Category extends Entity { } } interface CategoryRelations { - products?: Product[]; + products?: ProductWithRelations; } type CategoryWithRelations = Category & CategoryRelations; diff --git a/packages/repository/src/relations/has-many/has-many-repository.factory.ts b/packages/repository/src/relations/has-many/has-many-repository.factory.ts index 1e1c6c1717f6..e95972b8cec1 100644 --- a/packages/repository/src/relations/has-many/has-many-repository.factory.ts +++ b/packages/repository/src/relations/has-many/has-many-repository.factory.ts @@ -7,8 +7,9 @@ import * as debugFactory from 'debug'; import {DataObject} from '../../common-types'; import {Entity} from '../../model'; import {EntityCrudRepository} from '../../repositories/repository'; -import {Getter, HasManyDefinition} from '../relation.types'; +import {Getter, HasManyDefinition, InclusionResolver} from '../relation.types'; import {resolveHasManyMetadata} from './has-many.helpers'; +import {createHasManyInclusionResolver} from './has-many.inclusion-resolver'; import { DefaultHasManyRepository, HasManyRepository, @@ -16,9 +17,20 @@ import { const debug = debugFactory('loopback:repository:has-many-repository-factory'); -export type HasManyRepositoryFactory = ( - fkValue: ForeignKeyType, -) => HasManyRepository; +export interface HasManyRepositoryFactory< + Target extends Entity, + ForeignKeyType +> { + /** + * Invoke the function to obtain HasManyRepository. + */ + (fkValue: ForeignKeyType): HasManyRepository; + + /** + * Use `resolver` property to obtain an InclusionResolver for this relation. + */ + inclusionResolver: InclusionResolver; +} /** * Enforces a constraint on a repository based on a relationship contract @@ -43,7 +55,9 @@ export function createHasManyRepositoryFactory< ): HasManyRepositoryFactory { const meta = resolveHasManyMetadata(relationMetadata); debug('Resolved HasMany relation metadata: %o', meta); - return function(fkValue: ForeignKeyType) { + const result: HasManyRepositoryFactory = function( + fkValue: ForeignKeyType, + ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const constraint: any = {[meta.keyTo]: fkValue}; return new DefaultHasManyRepository< @@ -52,4 +66,9 @@ export function createHasManyRepositoryFactory< EntityCrudRepository >(targetRepositoryGetter, constraint as DataObject); }; + result.inclusionResolver = createHasManyInclusionResolver( + meta, + targetRepositoryGetter, + ); + return result; } diff --git a/packages/repository/src/relations/has-many/has-many.helpers.ts b/packages/repository/src/relations/has-many/has-many.helpers.ts index 702f44721e43..25adeda5ff6d 100644 --- a/packages/repository/src/relations/has-many/has-many.helpers.ts +++ b/packages/repository/src/relations/has-many/has-many.helpers.ts @@ -7,7 +7,7 @@ import * as debugFactory from 'debug'; import {camelCase} from 'lodash'; import {InvalidRelationError} from '../../errors'; import {isTypeResolver} from '../../type-resolver'; -import {HasManyDefinition} from '../relation.types'; +import {HasManyDefinition, RelationType} from '../relation.types'; const debug = debugFactory('loopback:repository:has-many-helpers'); @@ -30,6 +30,11 @@ export type HasManyResolvedDefinition = HasManyDefinition & { export function resolveHasManyMetadata( relationMeta: HasManyDefinition, ): HasManyResolvedDefinition { + if ((relationMeta.type as RelationType) !== RelationType.hasMany) { + const reason = 'relation type must be HasMany'; + throw new InvalidRelationError(reason, relationMeta); + } + if (!isTypeResolver(relationMeta.target)) { const reason = 'target must be a type resolver'; throw new InvalidRelationError(reason, relationMeta); diff --git a/packages/repository/src/relations/has-many/has-many.inclusion-resolver.ts b/packages/repository/src/relations/has-many/has-many.inclusion-resolver.ts new file mode 100644 index 000000000000..c3dc9e0608a9 --- /dev/null +++ b/packages/repository/src/relations/has-many/has-many.inclusion-resolver.ts @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2019. 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 * as debugFactory from 'debug'; +import {AnyObject, Options} from '../../common-types'; +import {Entity} from '../../model'; +import {Filter, Inclusion} from '../../query'; +import {EntityCrudRepository} from '../../repositories/repository'; +import { + findByForeignKeys, + flattenTargetsOfOneToManyRelation, + StringKeyOf, +} from '../relation.helpers'; +import {Getter, HasManyDefinition, InclusionResolver} from '../relation.types'; +import {resolveHasManyMetadata} from './has-many.helpers'; + +const debug = debugFactory('loopback:repository:has-many-inclusion-resolver'); + +/** + * Creates InclusionResolver for HasMany 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 metadata of the hasMany relation + * @param getTargetRepo - target repository i.e where related instances are + */ +export function createHasManyInclusionResolver< + Target extends Entity, + TargetID, + TargetRelations extends object +>( + meta: HasManyDefinition, + getTargetRepo: Getter< + EntityCrudRepository + >, +): InclusionResolver { + const relationMeta = resolveHasManyMetadata(meta); + + return async function fetchHasManyModels( + entities: Entity[], + inclusion: Inclusion, + options?: Options, + ): Promise<((Target & TargetRelations)[] | undefined)[]> { + if (!entities.length) return []; + + debug('Fetching target models for entities:', entities); + debug('Relation metadata:', relationMeta); + + const sourceKey = relationMeta.keyFrom; + const sourceIds = entities.map(e => (e as AnyObject)[sourceKey]); + const targetKey = relationMeta.keyTo as StringKeyOf; + + debug('Parameters:', {sourceKey, sourceIds, targetKey}); + debug('sourceId types', sourceIds.map(i => typeof i)); + + const targetRepo = await getTargetRepo(); + const targetsFound = await findByForeignKeys( + targetRepo, + targetKey, + sourceIds, + inclusion.scope as Filter, + options, + ); + + debug('Targets found:', targetsFound); + + const result = flattenTargetsOfOneToManyRelation( + sourceIds, + targetsFound, + targetKey, + ); + + debug('fetchHasManyModels result', result); + return result; + }; +} diff --git a/packages/repository/src/relations/has-many/index.ts b/packages/repository/src/relations/has-many/index.ts index 0025021d819a..7780e8b4c018 100644 --- a/packages/repository/src/relations/has-many/index.ts +++ b/packages/repository/src/relations/has-many/index.ts @@ -6,3 +6,4 @@ export * from './has-many.decorator'; export * from './has-many.repository'; export * from './has-many-repository.factory'; +export * from './has-many.inclusion-resolver'; diff --git a/packages/repository/src/relations/relation.helpers.ts b/packages/repository/src/relations/relation.helpers.ts index 10063998e7db..1231da86f9e2 100644 --- a/packages/repository/src/relations/relation.helpers.ts +++ b/packages/repository/src/relations/relation.helpers.ts @@ -168,6 +168,39 @@ export function flattenTargetsOfOneToOneRelation< return flattenMapByKeys(sourceIds, lookup); } +/** + * Returns an array of instances. The order of arrays is based on + * as a result of one to many relation. The order of arrays is based on + * the order of sourceIds + * + * @param sourceIds - One value or array of values of the target key + * @param targetEntities - target entities that satisfy targetKey's value (ids). + * @param targetKey - name of the target key + * + * @return + */ +export function flattenTargetsOfOneToManyRelation( + sourceIds: unknown[], + targetEntities: Target[], + targetKey: StringKeyOf, +): (Target[] | undefined)[] { + debug('flattenTargetsOfOneToManyRelation'); + debug('sourceIds', sourceIds); + debug('sourceId types', sourceIds.map(i => typeof i)); + debug('targetEntities', targetEntities); + debug('targetKey', targetKey); + + const lookup = buildLookupMap( + targetEntities, + targetKey, + reduceAsArray, + ); + + debug('lookup map', lookup); + + return flattenMapByKeys(sourceIds, lookup); +} + /** * Returns an array of instances from the target map. The order of arrays is based on * the order of sourceIds