Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,6 +9,7 @@ import {
constrainDataObjects,
constrainFilter,
constrainWhere,
constrainWhereOr,
Entity,
Filter,
FilterBuilder,
Expand Down Expand Up @@ -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'});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {
createTargetConstraint,
createThroughConstraint,
createThroughFkConstraint,
HasManyThroughResolvedDefinition,
resolveHasManyThroughMetadata,
} from '../../../../relations/has-many/has-many-through.helpers';
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -324,4 +353,7 @@ describe('HasManyThroughHelpers', () => {
function createCategoryProductLink(properties: Partial<CategoryProductLink>) {
return new CategoryProductLink(properties);
}
function createProduct(properties: Partial<Product>) {
return new Product(properties);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
*
Expand All @@ -50,26 +52,32 @@ export type HasManyThroughResolvedDefinition = HasManyDefinition & {
categoryId: 2,
productId: 8,
}, {
id: 2,
id: 1,
categoryId: 2,
productId: 9,
}
]);

>>> {id: {inq: [9, 8]}}
* ```
*/
export function createTargetConstraint<
Target extends Entity,
Through extends Entity
>(
relationMeta: HasManyThroughResolvedDefinition,
throughInstances: Through[],
throughInstances: Through | Through[],
): DataObject<Target> {
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},
Expand All @@ -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
Expand All @@ -98,6 +106,8 @@ export function createTargetConstraint<
* },
* };
* createThroughConstraint(resolvedMetadata, 1);
*
* >>> {categoryId: 1}
* ```
*/
export function createThroughConstraint<Through extends Entity, ForeignKeyType>(
Expand All @@ -109,6 +119,45 @@ export function createThroughConstraint<Through extends Entity, ForeignKeyType>(
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'});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional query field reminds me sth(sorry don't remember where is the original chat :p) :

We want to distinguish the query on through model(CategoryProductLink) and target model(Product), how do we achieve that?

For example, {name: 'a product'} seems for target model, and createThroughConstraint applies for through model, and that's why name is ignored here. But:

  • where is the place that processes the target model's filter?
  • how to specify the through model's filter?

(I started from the helper function, might missing the code/docs if they are created other places.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jannyHou I think these helpers are just for handling the relation metadata. And filters are handled in the hasManyThrough repository. Here is an example in the original design: find method. They won't be included in this PR. But I'd love to discuss it:

So far from those two community PRs(#2359 , #4438 ), I only see the target model is capable of applying filters. Not sure if we need it for through models ( I don't see the reason to allow filtering through models tho). Do we need it?

*
* >>> {productId: 1}
*
* createThroughConstraint(resolvedMetadata, {id: {inq:[3,4]}});
*
* >>> {productId: {inq:[3,4]}}
*/
export function createThroughFkConstraint<Target, Through extends Entity>(
relationMeta: HasManyThroughResolvedDefinition,
targetInstance: Target,
): DataObject<Through> {
const targetKey = relationMeta.keyTo as StringKeyOf<Target>;
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.
Expand Down
17 changes: 17 additions & 0 deletions packages/repository/src/repositories/constraint-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ export function constrainWhere<T extends object>(
const builder = new WhereBuilder<T>(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<T extends object>(
originalWhere: Where<T> | undefined,
constraint: Where<T>[],
): Where<T> {
const where = cloneDeep(originalWhere);
const builder = new WhereBuilder<T>(where);
return builder.or(constraint).build();
}
/**
* A utility function which takes a model instance data and enforces constraint(s)
* on it
Expand Down