Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(repository): add findByForeignKeys function #3473

Merged
merged 1 commit into from
Aug 6, 2019
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
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,
nabdelgadir marked this conversation as resolved.
Show resolved Hide resolved
TargetRelations extends object,
ForeignKey
>(
targetRepository: EntityCrudRepository<Target, TargetID, TargetRelations>,
fkName: StringKeyOf<Target>,
fkValues: ForeignKey[],
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we accept a single value too? For example:

fkValues: ForeignKey[] | ForeignKey,

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to have a type param such as FK extends StringKeyOf<Target> and use it to constrain fk values, such as Target[FK]?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let me reword it to make sure that I understand the type constrains in tsc correctly:

if we do foreignKey extends StringKeyOf<Target>, it would constrain the passed in foreignKey to be a string-like <Target> instance?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm proposing the following:

export async function findByForeignKeys<
  Target extends Entity,
  FK extends StringKeyOf<Target>,
  TargetRelations extends object,
>(
  targetRepository: EntityCrudRepository<Target, unknown, TargetRelations>,
  fkName: FK,
  fkValues: Target[FK][],
  scope?: Filter<Target>,
  options?: Options,
): Promise<(Target & TargetRelations)[]> {
}

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>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it's not necessary to export this type?