Skip to content

Commit

Permalink
feat(repository): add function findByForeignKeys
Browse files Browse the repository at this point in the history
Implemented initial version of the new helper function findByForeignKeys that finds model instances that contain any of the provided foreign key values.

Co-authored-by: Agnes Lin <agneslin.lin@ibm.com>
Co-authored-by: Miroslav Bajtoš <mbajtoss@gmail.com>
  • Loading branch information
3 people committed Jul 31, 2019
1 parent da7afef commit d37714a
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 5 deletions.
29 changes: 26 additions & 3 deletions packages/repository-tests/src/crud/create-retrieve.suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AnyObject,
Entity,
EntityCrudRepository,
findByForeignKeys,
model,
property,
} from '@loopback/repository';
Expand Down Expand Up @@ -42,6 +43,9 @@ export function createRetrieveSuite(
@property({type: 'string', required: true})
name: string;

@property()
categoryId?: number;

constructor(data?: Partial<Product>) {
super(data);
}
Expand All @@ -58,22 +62,41 @@ export function createRetrieveSuite(
}),
);

beforeEach(async () => {
await repo.deleteAll();
});

it('retrieves a newly created model with id set by the database', async () => {
const created = await repo.create({name: 'Pencil'});
expect(created.toObject()).to.have.properties('id', 'name');
const created = await repo.create({name: 'Pencil', categoryId: 1});
expect(created.toObject()).to.have.properties('id', 'name', 'categoryId');
expect(created.id).to.be.ok();

const found = await repo.findById(created.id);
expect(toJSON(created)).to.deepEqual(toJSON(found));
});

it('retrieves a newly created model when id was transformed via JSON', async () => {
const created = await repo.create({name: 'Pen'});
const created = await repo.create({name: 'Pen', categoryId: 1});
expect(created.id).to.be.ok();

const id = (toJSON(created) as AnyObject).id;
const found = await repo.findById(id);
expect(toJSON(created)).to.deepEqual(toJSON(found));
});

it('retrieves an instance of a model from its foreign key value', async () => {
const pens = await repo.create({name: 'Pens', categoryId: 1});
const pencils = await repo.create({name: 'Pencils', categoryId: 2});
const products = await findByForeignKeys(repo, 'categoryId', [1]);
expect(products).deepEqual([pens]);
expect(products).to.not.containDeep(pencils);
});

it('retrieves instances of a model from their foreign key value', async () => {
const pens = await repo.create({name: 'Pens', categoryId: 1});
const pencils = await repo.create({name: 'Pencils', categoryId: 2});
const products = await findByForeignKeys(repo, 'categoryId', [1, 2]);
expect(products).deepEqual([pens, pencils]);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 {DefaultCrudRepository, findByForeignKeys, juggler} from '../../..';
import {model, property} from '../../../decorators';
import {Entity} from '../../../model';

describe('findByForeignKeys', () => {
let productRepo: ProductRepository;

before(() => {
productRepo = new ProductRepository(testdb);
});

beforeEach(async () => {
await productRepo.deleteAll();
});

it('returns an empty array when no instances have the foreign key value', async () => {
await productRepo.create({id: 1, name: 'product', categoryId: 1});
const products = await findByForeignKeys(productRepo, 'categoryId', [2]);
expect(products).to.be.empty();
});

it('returns all instances that have the foreign key value', async () => {
const pens = await productRepo.create({name: 'pens', categoryId: 1});
const pencils = await productRepo.create({name: 'pencils', categoryId: 1});
const products = await findByForeignKeys(productRepo, 'categoryId', [1]);
expect(products).to.deepEqual([pens, pencils]);
});

it('does not include instances with different foreign key values', async () => {
const pens = await productRepo.create({name: 'pens', categoryId: 1});
const pencils = await productRepo.create({name: 'pencils', categoryId: 2});
const products = await findByForeignKeys(productRepo, 'categoryId', [1]);
expect(products).to.deepEqual([pens]);
expect(products).to.not.containDeep(pencils);
});

it('returns all instances that have any of multiple foreign key values', async () => {
const pens = await productRepo.create({name: 'pens', categoryId: 1});
const pencils = await productRepo.create({name: 'pencils', categoryId: 2});
const paper = await productRepo.create({name: 'paper', categoryId: 3});
const products = await findByForeignKeys(productRepo, 'categoryId', [1, 3]);
expect(products).to.deepEqual([pens, paper]);
expect(products).to.not.containDeep(pencils);
});

it('throws error if scope is passed in and is non-empty', async () => {
let errorMessage;
try {
await findByForeignKeys(productRepo, 'categoryId', [1], {
limit: 1,
});
} catch (error) {
errorMessage = error.message;
}
expect(errorMessage).to.eql('scope is not supported');
});

/******************* HELPERS *******************/

@model()
class Product extends Entity {
@property({id: true})
id: number;
@property()
name: string;
@property()
categoryId: number;
}

class ProductRepository extends DefaultCrudRepository<
Product,
typeof Product.prototype.id
> {
constructor(dataSource: juggler.DataSource) {
super(Product, dataSource);
}
}

const testdb: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
});
});
5 changes: 3 additions & 2 deletions packages/repository/src/relations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './relation.types';
export * from './relation.decorator';
export * from './belongs-to';
export * from './has-many';
export * from './has-one';
export * from './relation.decorator';
export * from './relation.helpers';
export * from './relation.types';
44 changes: 44 additions & 0 deletions packages/repository/src/relations/relation.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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 _ from 'lodash';
import {Entity, EntityCrudRepository, Filter, Options, Where} from '..';

/**
* Finds model instances that contain any of the provided foreign key values.
*
* @param targetRepository - The target repository where the model instances are found
* @param fkName - Name of the foreign key
* @param fkValues - Array of the values of the foreign keys to be included
* @param scope - Additional scope constraints (not currently supported)
* @param options - Options for the operations
*/
export async function findByForeignKeys<
Target extends Entity,
TargetID,
TargetRelations extends object,
ForeignKey
>(
targetRepository: EntityCrudRepository<Target, TargetID, TargetRelations>,
fkName: StringKeyOf<Target>,
fkValues: ForeignKey[],
scope?: Filter<Target>,
options?: Options,
): Promise<(Target & TargetRelations)[]> {
// throw error if scope is defined and non-empty
// see https://github.com/strongloop/loopback-next/issues/3453
if (scope && !_.isEmpty(scope)) {
throw new Error('scope is not supported');
}

const where = ({
[fkName]: fkValues.length === 1 ? fkValues[0] : {inq: fkValues},
} as unknown) as Where<Target>;
const targetFilter = {where};

return targetRepository.find(targetFilter, options);
}

export type StringKeyOf<T> = Extract<keyof T, string>;

0 comments on commit d37714a

Please sign in to comment.