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 InclusionResolver type and includeRelatedModels #3517

Merged
merged 1 commit into from Aug 19, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -4,9 +4,8 @@
// 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';
import {findByForeignKeys} from '../../../..';
import {ProductRepository, testdb} from './relations-helpers-fixtures';

describe('findByForeignKeys', () => {
let productRepo: ProductRepository;
Expand Down Expand Up @@ -37,6 +36,7 @@ describe('findByForeignKeys', () => {
const products = await findByForeignKeys(productRepo, 'categoryId', [2, 3]);
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});
Expand All @@ -59,6 +59,7 @@ describe('findByForeignKeys', () => {
expect(products).to.deepEqual([pencils]);
expect(products).to.not.containDeep(pens);
});

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});
Expand All @@ -69,15 +70,9 @@ describe('findByForeignKeys', () => {
});

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');
await expect(
findByForeignKeys(productRepo, 'categoryId', [1], {limit: 1}),
).to.be.rejectedWith('scope is not supported');
});

it('does not throw an error if scope is passed in and is undefined or empty', async () => {
Expand All @@ -92,29 +87,4 @@ describe('findByForeignKeys', () => {
products = await findByForeignKeys(productRepo, 'categoryId', 1, {}, {});
expect(products).to.be.empty();
});
/******************* 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',
});
});
@@ -0,0 +1,212 @@
// 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, toJSON} from '@loopback/testlab';
import {includeRelatedModels, InclusionResolver} from '../../../..';
import {
Category,
CategoryRepository,
Product,
ProductRepository,
testdb,
} from './relations-helpers-fixtures';

describe('includeRelatedModels', () => {
let productRepo: ProductRepository;
let categoryRepo: CategoryRepository;

before(() => {
productRepo = new ProductRepository(testdb);
categoryRepo = new CategoryRepository(testdb, async () => productRepo);
});

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

it("defines a repository's inclusionResolvers property", () => {
expect(categoryRepo.inclusionResolvers).to.not.be.undefined();
expect(productRepo.inclusionResolvers).to.not.be.undefined();
});

it('returns source model if no filter is passed in', async () => {
const category = await categoryRepo.create({name: 'category 1'});
await categoryRepo.create({name: 'category 2'});
const result = await includeRelatedModels(categoryRepo, [category]);
expect(result).to.eql([category]);
});

it('throws error if the target repository does not have the registered resolver', async () => {
const category = await categoryRepo.create({name: 'category 1'});
await expect(
includeRelatedModels(categoryRepo, [category], [{relation: 'products'}]),
).to.be.rejectedWith(
/Invalid "filter.include" entries: {"relation":"products"}/,
);
});

it('returns an empty array if target model of the source entity does not have any matched instances', async () => {
const category = await categoryRepo.create({name: 'category'});

categoryRepo.inclusionResolvers.set('products', hasManyResolver);

const categories = await includeRelatedModels(
categoryRepo,
[category],
[{relation: 'products'}],
);

expect(categories[0].products).to.be.empty();
});

it('includes related model for one instance - belongsTo', async () => {
const category = await categoryRepo.create({name: 'category'});
const product = await productRepo.create({
name: 'product',
categoryId: category.id,
});

productRepo.inclusionResolvers.set('category', belongsToResolver);

const productWithCategories = await includeRelatedModels(
productRepo,
[product],
[{relation: 'category'}],
);

expect(productWithCategories[0].toJSON()).to.deepEqual({
...product.toJSON(),
category: category.toJSON(),
});
});

it('includes related model for more than one instance - belongsTo', async () => {
const categoryOne = await categoryRepo.create({name: 'category 1'});
const productOne = await productRepo.create({
name: 'product 1',
categoryId: categoryOne.id,
});

const categoryTwo = await categoryRepo.create({name: 'category 2'});
const productTwo = await productRepo.create({
name: 'product 2',
categoryId: categoryTwo.id,
});

const productThree = await productRepo.create({
name: 'product 3',
categoryId: categoryTwo.id,
});

productRepo.inclusionResolvers.set('category', belongsToResolver);

const productWithCategories = await includeRelatedModels(
productRepo,
[productOne, productTwo, productThree],
[{relation: 'category'}],
);

expect(toJSON(productWithCategories)).to.deepEqual([
{...productOne.toJSON(), category: categoryOne.toJSON()},
{...productTwo.toJSON(), category: categoryTwo.toJSON()},
{...productThree.toJSON(), category: categoryTwo.toJSON()},
]);
});

it('includes related models for one instance - hasMany', async () => {
const category = await categoryRepo.create({name: 'category'});
const productOne = await productRepo.create({
name: 'product 1',
categoryId: category.id,
});

const productTwo = await productRepo.create({
name: 'product 2',
categoryId: category.id,
});

categoryRepo.inclusionResolvers.set('products', hasManyResolver);

const categoryWithProducts = await includeRelatedModels(
categoryRepo,
[category],
[{relation: 'products'}],
);

expect(toJSON(categoryWithProducts)).to.deepEqual([
{
...category.toJSON(),
products: [productOne.toJSON(), productTwo.toJSON()],
},
]);
});

it('includes related models for more than one instance - hasMany', async () => {
const categoryOne = await categoryRepo.create({name: 'category 1'});
const productOne = await productRepo.create({
name: 'product 1',
categoryId: categoryOne.id,
});

const categoryTwo = await categoryRepo.create({name: 'category 2'});
const productTwo = await productRepo.create({
name: 'product 2',
categoryId: categoryTwo.id,
});

const categoryThree = await categoryRepo.create({name: 'category 3'});
const productThree = await productRepo.create({
name: 'product 3',
categoryId: categoryTwo.id,
});

categoryRepo.inclusionResolvers.set('products', hasManyResolver);

const categoryWithProducts = await includeRelatedModels(
categoryRepo,
[categoryOne, categoryTwo, categoryThree],
[{relation: 'products'}],
);

expect(toJSON(categoryWithProducts)).to.deepEqual([
{...categoryOne.toJSON(), products: [productOne.toJSON()]},
{
...categoryTwo.toJSON(),
products: [productTwo.toJSON(), productThree.toJSON()],
},
{...categoryThree.toJSON(), products: []},
]);
});

// stubbed resolvers

const belongsToResolver: InclusionResolver<
Product,
Category
> = async entities => {
const categories = [];

for (const product of entities) {
const category = await categoryRepo.findById(product.categoryId);
categories.push(category);
}

return categories;
};

const hasManyResolver: InclusionResolver<
Category,
Product
> = async entities => {
const products = [];

for (const category of entities) {
const product = await categoryRepo.products(category.id).find();
products.push(product);
}
return products;
};
});
@@ -0,0 +1,87 @@
// 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 {
belongsTo,
BelongsToAccessor,
DefaultCrudRepository,
Entity,
Getter,
hasMany,
HasManyRepositoryFactory,
juggler,
model,
property,
} from '../../../..';

@model()
export class Product extends Entity {
@property({id: true})
id: number;
@property()
name: string;
@belongsTo(() => Category)
categoryId: number;
}

export class ProductRepository extends DefaultCrudRepository<
Product,
typeof Product.prototype.id
> {
public readonly category: BelongsToAccessor<
Category,
typeof Product.prototype.id
>;
constructor(
dataSource: juggler.DataSource,
categoryRepository?: Getter<CategoryRepository>,
) {
super(Product, dataSource);
if (categoryRepository)
this.category = this.createBelongsToAccessorFor(
'category',
categoryRepository,
);
}
}

@model()
export class Category extends Entity {
@property({id: true})
id?: number;
@property()
name: string;
@hasMany(() => Product, {keyTo: 'categoryId'})
products?: Product[];
}
interface CategoryRelations {
products?: Product[];
}

export class CategoryRepository extends DefaultCrudRepository<
Category,
typeof Category.prototype.id,
CategoryRelations
> {
public readonly products: HasManyRepositoryFactory<
Product,
typeof Category.prototype.id
>;
constructor(
dataSource: juggler.DataSource,
productRepository: Getter<ProductRepository>,
) {
super(Category, dataSource);
this.products = this.createHasManyRepositoryFactoryFor(
'products',
productRepository,
);
}
}

export const testdb: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
});