Skip to content

Commit c9c39c9

Browse files
Agnes Linnabdelgadirbajtos
committed
feat(repository): add InclusionResolver type and includeRelatedModels helper function
Add InclusionResolver type, includeRelatedModels and isInclusionAllowed helpers. Co-authored-by: Nora <nora.abdelgadir@ibm.com> Co-authored-by: Miroslav Bajtoš <mbajtoss@gmail.com>
1 parent c0f8597 commit c9c39c9

File tree

7 files changed

+427
-38
lines changed

7 files changed

+427
-38
lines changed

packages/repository/src/__tests__/unit/repositories/relation.helpers.unit.ts renamed to packages/repository/src/__tests__/unit/repositories/relations-helpers/find-by-foreign-keys.helpers.unit.ts

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {expect} from '@loopback/testlab';
7-
import {DefaultCrudRepository, findByForeignKeys, juggler} from '../../..';
8-
import {model, property} from '../../../decorators';
9-
import {Entity} from '../../../model';
7+
import {findByForeignKeys} from '../../../..';
8+
import {ProductRepository, testdb} from './relations-helpers-fixtures';
109

1110
describe('findByForeignKeys', () => {
1211
let productRepo: ProductRepository;
@@ -37,6 +36,7 @@ describe('findByForeignKeys', () => {
3736
const products = await findByForeignKeys(productRepo, 'categoryId', [2, 3]);
3837
expect(products).to.be.empty();
3938
});
39+
4040
it('returns all instances that have the foreign key value', async () => {
4141
const pens = await productRepo.create({name: 'pens', categoryId: 1});
4242
const pencils = await productRepo.create({name: 'pencils', categoryId: 1});
@@ -59,6 +59,7 @@ describe('findByForeignKeys', () => {
5959
expect(products).to.deepEqual([pencils]);
6060
expect(products).to.not.containDeep(pens);
6161
});
62+
6263
it('returns all instances that have any of multiple foreign key values', async () => {
6364
const pens = await productRepo.create({name: 'pens', categoryId: 1});
6465
const pencils = await productRepo.create({name: 'pencils', categoryId: 2});
@@ -69,15 +70,9 @@ describe('findByForeignKeys', () => {
6970
});
7071

7172
it('throws error if scope is passed in and is non-empty', async () => {
72-
let errorMessage;
73-
try {
74-
await findByForeignKeys(productRepo, 'categoryId', [1], {
75-
limit: 1,
76-
});
77-
} catch (error) {
78-
errorMessage = error.message;
79-
}
80-
expect(errorMessage).to.eql('scope is not supported');
73+
await expect(
74+
findByForeignKeys(productRepo, 'categoryId', [1], {limit: 1}),
75+
).to.be.rejectedWith('scope is not supported');
8176
});
8277

8378
it('does not throw an error if scope is passed in and is undefined or empty', async () => {
@@ -92,29 +87,4 @@ describe('findByForeignKeys', () => {
9287
products = await findByForeignKeys(productRepo, 'categoryId', 1, {}, {});
9388
expect(products).to.be.empty();
9489
});
95-
/******************* HELPERS *******************/
96-
97-
@model()
98-
class Product extends Entity {
99-
@property({id: true})
100-
id: number;
101-
@property()
102-
name: string;
103-
@property()
104-
categoryId: number;
105-
}
106-
107-
class ProductRepository extends DefaultCrudRepository<
108-
Product,
109-
typeof Product.prototype.id
110-
> {
111-
constructor(dataSource: juggler.DataSource) {
112-
super(Product, dataSource);
113-
}
114-
}
115-
116-
const testdb: juggler.DataSource = new juggler.DataSource({
117-
name: 'db',
118-
connector: 'memory',
119-
});
12090
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright IBM Corp. 2019. All Rights Reserved.
2+
// Node module: @loopback/repository
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {expect, toJSON} from '@loopback/testlab';
7+
import {includeRelatedModels, InclusionResolver} from '../../../..';
8+
import {
9+
Category,
10+
CategoryRepository,
11+
Product,
12+
ProductRepository,
13+
testdb,
14+
} from './relations-helpers-fixtures';
15+
16+
describe('includeRelatedModels', () => {
17+
let productRepo: ProductRepository;
18+
let categoryRepo: CategoryRepository;
19+
20+
before(() => {
21+
productRepo = new ProductRepository(testdb);
22+
categoryRepo = new CategoryRepository(testdb, async () => productRepo);
23+
});
24+
25+
beforeEach(async () => {
26+
await productRepo.deleteAll();
27+
await categoryRepo.deleteAll();
28+
});
29+
30+
it("defines a repository's inclusionResolvers property", () => {
31+
expect(categoryRepo.inclusionResolvers).to.not.be.undefined();
32+
expect(productRepo.inclusionResolvers).to.not.be.undefined();
33+
});
34+
35+
it('returns source model if no filter is passed in', async () => {
36+
const category = await categoryRepo.create({name: 'category 1'});
37+
await categoryRepo.create({name: 'category 2'});
38+
const result = await includeRelatedModels(categoryRepo, [category]);
39+
expect(result).to.eql([category]);
40+
});
41+
42+
it('throws error if the target repository does not have the registered resolver', async () => {
43+
const category = await categoryRepo.create({name: 'category 1'});
44+
await expect(
45+
includeRelatedModels(categoryRepo, [category], [{relation: 'products'}]),
46+
).to.be.rejectedWith(
47+
/Invalid "filter.include" entries: {"relation":"products"}/,
48+
);
49+
});
50+
51+
it('returns an empty array if target model of the source entity does not have any matched instances', async () => {
52+
const category = await categoryRepo.create({name: 'category'});
53+
54+
categoryRepo.inclusionResolvers.set('products', hasManyResolver);
55+
56+
const categories = await includeRelatedModels(
57+
categoryRepo,
58+
[category],
59+
[{relation: 'products'}],
60+
);
61+
62+
expect(categories[0].products).to.be.empty();
63+
});
64+
65+
it('includes related model for one instance - belongsTo', async () => {
66+
const category = await categoryRepo.create({name: 'category'});
67+
const product = await productRepo.create({
68+
name: 'product',
69+
categoryId: category.id,
70+
});
71+
72+
productRepo.inclusionResolvers.set('category', belongsToResolver);
73+
74+
const productWithCategories = await includeRelatedModels(
75+
productRepo,
76+
[product],
77+
[{relation: 'category'}],
78+
);
79+
80+
expect(productWithCategories[0].toJSON()).to.deepEqual({
81+
...product.toJSON(),
82+
category: category.toJSON(),
83+
});
84+
});
85+
86+
it('includes related model for more than one instance - belongsTo', async () => {
87+
const categoryOne = await categoryRepo.create({name: 'category 1'});
88+
const productOne = await productRepo.create({
89+
name: 'product 1',
90+
categoryId: categoryOne.id,
91+
});
92+
93+
const categoryTwo = await categoryRepo.create({name: 'category 2'});
94+
const productTwo = await productRepo.create({
95+
name: 'product 2',
96+
categoryId: categoryTwo.id,
97+
});
98+
99+
const productThree = await productRepo.create({
100+
name: 'product 3',
101+
categoryId: categoryTwo.id,
102+
});
103+
104+
productRepo.inclusionResolvers.set('category', belongsToResolver);
105+
106+
const productWithCategories = await includeRelatedModels(
107+
productRepo,
108+
[productOne, productTwo, productThree],
109+
[{relation: 'category'}],
110+
);
111+
112+
expect(toJSON(productWithCategories)).to.deepEqual([
113+
{...productOne.toJSON(), category: categoryOne.toJSON()},
114+
{...productTwo.toJSON(), category: categoryTwo.toJSON()},
115+
{...productThree.toJSON(), category: categoryTwo.toJSON()},
116+
]);
117+
});
118+
119+
it('includes related models for one instance - hasMany', async () => {
120+
const category = await categoryRepo.create({name: 'category'});
121+
const productOne = await productRepo.create({
122+
name: 'product 1',
123+
categoryId: category.id,
124+
});
125+
126+
const productTwo = await productRepo.create({
127+
name: 'product 2',
128+
categoryId: category.id,
129+
});
130+
131+
categoryRepo.inclusionResolvers.set('products', hasManyResolver);
132+
133+
const categoryWithProducts = await includeRelatedModels(
134+
categoryRepo,
135+
[category],
136+
[{relation: 'products'}],
137+
);
138+
139+
expect(toJSON(categoryWithProducts)).to.deepEqual([
140+
{
141+
...category.toJSON(),
142+
products: [productOne.toJSON(), productTwo.toJSON()],
143+
},
144+
]);
145+
});
146+
147+
it('includes related models for more than one instance - hasMany', async () => {
148+
const categoryOne = await categoryRepo.create({name: 'category 1'});
149+
const productOne = await productRepo.create({
150+
name: 'product 1',
151+
categoryId: categoryOne.id,
152+
});
153+
154+
const categoryTwo = await categoryRepo.create({name: 'category 2'});
155+
const productTwo = await productRepo.create({
156+
name: 'product 2',
157+
categoryId: categoryTwo.id,
158+
});
159+
160+
const categoryThree = await categoryRepo.create({name: 'category 3'});
161+
const productThree = await productRepo.create({
162+
name: 'product 3',
163+
categoryId: categoryTwo.id,
164+
});
165+
166+
categoryRepo.inclusionResolvers.set('products', hasManyResolver);
167+
168+
const categoryWithProducts = await includeRelatedModels(
169+
categoryRepo,
170+
[categoryOne, categoryTwo, categoryThree],
171+
[{relation: 'products'}],
172+
);
173+
174+
expect(toJSON(categoryWithProducts)).to.deepEqual([
175+
{...categoryOne.toJSON(), products: [productOne.toJSON()]},
176+
{
177+
...categoryTwo.toJSON(),
178+
products: [productTwo.toJSON(), productThree.toJSON()],
179+
},
180+
{...categoryThree.toJSON(), products: []},
181+
]);
182+
});
183+
184+
// stubbed resolvers
185+
186+
const belongsToResolver: InclusionResolver<
187+
Product,
188+
Category
189+
> = async entities => {
190+
const categories = [];
191+
192+
for (const product of entities) {
193+
const category = await categoryRepo.findById(product.categoryId);
194+
categories.push(category);
195+
}
196+
197+
return categories;
198+
};
199+
200+
const hasManyResolver: InclusionResolver<
201+
Category,
202+
Product
203+
> = async entities => {
204+
const products = [];
205+
206+
for (const category of entities) {
207+
const product = await categoryRepo.products(category.id).find();
208+
products.push(product);
209+
}
210+
return products;
211+
};
212+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright IBM Corp. 2019. All Rights Reserved.
2+
// Node module: @loopback/repository
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {
7+
belongsTo,
8+
BelongsToAccessor,
9+
DefaultCrudRepository,
10+
Entity,
11+
Getter,
12+
hasMany,
13+
HasManyRepositoryFactory,
14+
juggler,
15+
model,
16+
property,
17+
} from '../../../..';
18+
19+
@model()
20+
export class Product extends Entity {
21+
@property({id: true})
22+
id: number;
23+
@property()
24+
name: string;
25+
@belongsTo(() => Category)
26+
categoryId: number;
27+
}
28+
29+
export class ProductRepository extends DefaultCrudRepository<
30+
Product,
31+
typeof Product.prototype.id
32+
> {
33+
public readonly category: BelongsToAccessor<
34+
Category,
35+
typeof Product.prototype.id
36+
>;
37+
constructor(
38+
dataSource: juggler.DataSource,
39+
categoryRepository?: Getter<CategoryRepository>,
40+
) {
41+
super(Product, dataSource);
42+
if (categoryRepository)
43+
this.category = this.createBelongsToAccessorFor(
44+
'category',
45+
categoryRepository,
46+
);
47+
}
48+
}
49+
50+
@model()
51+
export class Category extends Entity {
52+
@property({id: true})
53+
id?: number;
54+
@property()
55+
name: string;
56+
@hasMany(() => Product, {keyTo: 'categoryId'})
57+
products?: Product[];
58+
}
59+
interface CategoryRelations {
60+
products?: Product[];
61+
}
62+
63+
export class CategoryRepository extends DefaultCrudRepository<
64+
Category,
65+
typeof Category.prototype.id,
66+
CategoryRelations
67+
> {
68+
public readonly products: HasManyRepositoryFactory<
69+
Product,
70+
typeof Category.prototype.id
71+
>;
72+
constructor(
73+
dataSource: juggler.DataSource,
74+
productRepository: Getter<ProductRepository>,
75+
) {
76+
super(Category, dataSource);
77+
this.products = this.createHasManyRepositoryFactoryFor(
78+
'products',
79+
productRepository,
80+
);
81+
}
82+
}
83+
84+
export const testdb: juggler.DataSource = new juggler.DataSource({
85+
name: 'db',
86+
connector: 'memory',
87+
});

0 commit comments

Comments
 (0)