From 4e85e21bbc2ffd1a5a2ccdd4b5bdb2977e47fb01 Mon Sep 17 00:00:00 2001 From: Agnes Lin Date: Fri, 29 May 2020 10:02:19 -0400 Subject: [PATCH] feat(repository): add more helpers for HasManyThrough --- .../repositories/constraint-utils.unit.ts | 20 ++++++- .../resolve-has-many-through-metadata.unit.ts | 34 ++++++++++- .../has-many/has-many-through.helpers.ts | 59 +++++++++++++++++-- .../src/repositories/constraint-utils.ts | 17 ++++++ 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/packages/repository/src/__tests__/unit/repositories/constraint-utils.unit.ts b/packages/repository/src/__tests__/unit/repositories/constraint-utils.unit.ts index 44d48f2f1064..0313d6ea1af2 100644 --- a/packages/repository/src/__tests__/unit/repositories/constraint-utils.unit.ts +++ b/packages/repository/src/__tests__/unit/repositories/constraint-utils.unit.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// Copyright IBM Corp. 2019,2020. All Rights Reserved. // Node module: @loopback/repository // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT @@ -9,6 +9,7 @@ import { constrainDataObjects, constrainFilter, constrainWhere, + constrainWhereOr, Entity, Filter, FilterBuilder, @@ -77,6 +78,23 @@ describe('constraint utility functions', () => { }); }); + context('constrainWhereOr', () => { + const inputWhere: Where<{x: string; y: string; id: string}> = { + x: 'x', + }; + it('enforces a constraint', () => { + const constraint = [{id: '5'}, {y: 'y'}]; + const result = constrainWhereOr(inputWhere, constraint); + expect(result).to.deepEqual({...inputWhere, or: constraint}); + }); + + it('enforces constraint with dup key', () => { + const constraint = [{y: 'z'}, {x: 'z'}]; + const result = constrainWhereOr(inputWhere, constraint); + expect(result).to.deepEqual({...inputWhere, or: constraint}); + }); + }); + context('constrainDataObject', () => { it('constrains a single data object', () => { const input = new Order({description: 'order 1'}); diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts index 5ed82e19ca7a..f59a38703989 100644 --- a/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts @@ -15,6 +15,7 @@ import { import { createTargetConstraint, createThroughConstraint, + createThroughFkConstraint, HasManyThroughResolvedDefinition, resolveHasManyThroughMetadata, } from '../../../../relations/has-many/has-many-through.helpers'; @@ -42,12 +43,40 @@ describe('HasManyThroughHelpers', () => { const resolved = resolvedMetadata as HasManyThroughResolvedDefinition; // single through model - let result = createTargetConstraint(resolved, [through1]); + let result = createTargetConstraint(resolved, through1); expect(result).to.containEql({id: 9}); // multiple through models result = createTargetConstraint(resolved, [through1, through2]); expect(result).to.containEql({id: {inq: [9, 8]}}); }); + + it('can create constraint for searching target models with duplicate keys', () => { + const through1 = createCategoryProductLink({ + id: 1, + categoryId: 2, + productId: 9, + }); + const through2 = createCategoryProductLink({ + id: 2, + categoryId: 3, + productId: 9, + }); + const resolved = resolvedMetadata as HasManyThroughResolvedDefinition; + + const result = createTargetConstraint(resolved, [through1, through2]); + expect(result).to.containEql({id: 9}); + }); + }); + context('createThroughFkConstraint', () => { + it('can create constraint with a given target instance', () => { + const product = createProduct({ + id: 1, + }); + const resolved = resolvedMetadata as HasManyThroughResolvedDefinition; + + const result = createThroughFkConstraint(resolved, product); + expect(result).to.containEql({productId: 1}); + }); }); context('resolveHasManyThroughMetadata', () => { it('throws if the wrong metadata type is used', async () => { @@ -324,4 +353,7 @@ describe('HasManyThroughHelpers', () => { function createCategoryProductLink(properties: Partial) { return new CategoryProductLink(properties); } + function createProduct(properties: Partial) { + return new Product(properties); + } }); diff --git a/packages/repository/src/relations/has-many/has-many-through.helpers.ts b/packages/repository/src/relations/has-many/has-many-through.helpers.ts index fbe3b833ca01..b4a510ec027a 100644 --- a/packages/repository/src/relations/has-many/has-many-through.helpers.ts +++ b/packages/repository/src/relations/has-many/has-many-through.helpers.ts @@ -7,10 +7,12 @@ import debugFactory from 'debug'; import {camelCase} from 'lodash'; import { DataObject, + deduplicate, Entity, HasManyDefinition, InvalidRelationError, isTypeResolver, + StringKeyOf, } from '../..'; import {resolveHasManyMetaHelper} from './has-many.helpers'; @@ -27,7 +29,7 @@ export type HasManyThroughResolvedDefinition = HasManyDefinition & { /** * Creates constraint used to query target - * @param relationMeta - hasManyThrough metadata to resolve + * @param relationMeta - resolved hasManyThrough metadata * @param throughInstances - Instances of through entities used to constrain the target * @internal * @@ -50,11 +52,13 @@ export type HasManyThroughResolvedDefinition = HasManyDefinition & { categoryId: 2, productId: 8, }, { - id: 2, + id: 1, categoryId: 2, productId: 9, } ]); + + >>> {id: {inq: [9, 8]}} * ``` */ export function createTargetConstraint< @@ -62,14 +66,18 @@ export function createTargetConstraint< Through extends Entity >( relationMeta: HasManyThroughResolvedDefinition, - throughInstances: Through[], + throughInstances: Through | Through[], ): DataObject { const targetPrimaryKey = relationMeta.keyTo; const targetFkName = relationMeta.through.keyTo; - const fkValues = throughInstances.map( + if (!Array.isArray(throughInstances)) { + throughInstances = [throughInstances]; + } + let fkValues = throughInstances.map( (throughInstance: Through) => throughInstance[targetFkName as keyof Through], ); + fkValues = deduplicate(fkValues); // eslint-disable-next-line @typescript-eslint/no-explicit-any const constraint: any = { [targetPrimaryKey]: fkValues.length === 1 ? fkValues[0] : {inq: fkValues}, @@ -80,7 +88,7 @@ export function createTargetConstraint< /** * Creates constraint used to query through model * - * @param relationMeta - hasManyThrough metadata to resolve + * @param relationMeta - resolved hasManyThrough metadata * @param fkValue - Value of the foreign key of the source model used to constrain through * @param targetInstance - Instance of target entity used to constrain through * @internal @@ -98,6 +106,8 @@ export function createTargetConstraint< * }, * }; * createThroughConstraint(resolvedMetadata, 1); + * + * >>> {categoryId: 1} * ``` */ export function createThroughConstraint( @@ -109,6 +119,45 @@ export function createThroughConstraint( const constraint: any = {[sourceFkName]: fkValue}; return constraint; } +/** + * Creates constraint used to create the through model + * + * @param relationMeta - resolved hasManyThrough metadata + * @param targetInstance instance of target entity used to constrain through + * @internal + * + * @example + * ```ts + * const resolvedMetadata = { + * // .. other props + * keyFrom: 'id', + * keyTo: 'id', + * through: { + * model: () => CategoryProductLink, + * keyFrom: 'categoryId', + * keyTo: 'productId', + * }, + * }; + * createThroughConstraint(resolvedMetadata, {id: 3, name: 'a product'}); + * + * >>> {productId: 1} + * + * createThroughConstraint(resolvedMetadata, {id: {inq:[3,4]}}); + * + * >>> {productId: {inq:[3,4]}} + */ +export function createThroughFkConstraint( + relationMeta: HasManyThroughResolvedDefinition, + targetInstance: Target, +): DataObject { + const targetKey = relationMeta.keyTo as StringKeyOf; + const targetFkName = relationMeta.through.keyTo; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const constraint: any = { + [targetFkName]: targetInstance[targetKey], + }; + return constraint; +} /** * Resolves given hasMany metadata if target is specified to be a resolver. diff --git a/packages/repository/src/repositories/constraint-utils.ts b/packages/repository/src/repositories/constraint-utils.ts index 5887bdc143da..58eda13b3f4c 100644 --- a/packages/repository/src/repositories/constraint-utils.ts +++ b/packages/repository/src/repositories/constraint-utils.ts @@ -41,6 +41,23 @@ export function constrainWhere( const builder = new WhereBuilder(where); return builder.impose(constraint).build(); } +/** + * A utility function which takes a where filter and enforces constraint(s) + * on it with OR clause + * @param originalWhere - the where filter to apply the constrain(s) to + * @param constraint - the constraint which is to be applied on the filter with + * or clause + * @returns Filter the modified filter with the constraint, otherwise + * the original filter + */ +export function constrainWhereOr( + originalWhere: Where | undefined, + constraint: Where[], +): Where { + const where = cloneDeep(originalWhere); + const builder = new WhereBuilder(where); + return builder.or(constraint).build(); +} /** * A utility function which takes a model instance data and enforces constraint(s) * on it