Skip to content

Commit 1fdae63

Browse files
jannyHouJanny
authored andcommitted
feat: add crud relation methods
1 parent c076eba commit 1fdae63

File tree

5 files changed

+185
-28
lines changed

5 files changed

+185
-28
lines changed

packages/repository/src/repositories/constraint-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export function constrainDataObject<T extends Entity>(
5555
): DataObject<T> {
5656
const constrainedData = cloneDeep(originalData);
5757
for (const c in constraint) {
58+
if (constrainedData.hasOwnProperty(c))
59+
throw new Error(`Property "${c}" cannot be changed!`);
5860
constrainedData[c] = constraint[c];
5961
}
6062
return constrainedData;

packages/repository/src/repositories/relation.repository.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {EntityCrudRepository} from './repository';
7-
import {constrainDataObject, constrainFilter} from './constraint-utils';
8-
import {AnyObject, Options} from '../common-types';
7+
import {
8+
constrainDataObject,
9+
constrainFilter,
10+
constrainWhere,
11+
} from './constraint-utils';
12+
import {DataObject, AnyObject, Options} from '../common-types';
913
import {Entity} from '../model';
10-
import {Filter} from '../query';
14+
import {Filter, Where} from '../query';
1115

1216
/**
1317
* CRUD operations for a target repository of a HasMany relation
@@ -26,7 +30,26 @@ export interface HasManyEntityCrudRepository<T extends Entity> {
2630
* @param options Options for the operation
2731
* @returns A promise which resolves with the found target instance(s)
2832
*/
29-
find(filter?: Filter | undefined, options?: Options): Promise<T[]>;
33+
find(filter?: Filter, options?: Options): Promise<T[]>;
34+
/**
35+
* Delete multiple target model instances
36+
* @param where Instances within the where scope are deleted
37+
* @param options
38+
* @returns A promise which resolves the deleted target model instances
39+
*/
40+
delete(where?: Where, options?: Options): Promise<number>;
41+
/**
42+
* Patch multiple target model instances
43+
* @param dataObject The fields and their new values to patch
44+
* @param where Instances within the where scope are patched
45+
* @param options
46+
* @returns A promise which resolves the patched target model instances
47+
*/
48+
patch(
49+
dataObject: DataObject<T>,
50+
where?: Where,
51+
options?: Options,
52+
): Promise<number>;
3053
}
3154

3255
export class DefaultHasManyEntityCrudRepository<
@@ -51,10 +74,29 @@ export class DefaultHasManyEntityCrudRepository<
5174
);
5275
}
5376

54-
async find(filter?: Filter | undefined, options?: Options): Promise<T[]> {
77+
async find(filter?: Filter, options?: Options): Promise<T[]> {
5578
return await this.targetRepository.find(
5679
constrainFilter(filter, this.constraint),
5780
options,
5881
);
5982
}
83+
84+
async delete(where?: Where, options?: Options): Promise<number> {
85+
return await this.targetRepository.deleteAll(
86+
constrainWhere(where, this.constraint),
87+
options,
88+
);
89+
}
90+
91+
async patch(
92+
dataObject: Partial<T>,
93+
where?: Where,
94+
options?: Options,
95+
): Promise<number> {
96+
return await this.targetRepository.updateAll(
97+
constrainDataObject(dataObject, this.constraint),
98+
constrainWhere(where, this.constraint),
99+
options,
100+
);
101+
}
60102
}

packages/repository/test/acceptance/has-many.relation.acceptance.ts

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import {
1313
hasManyRepositoryFactory,
1414
HasManyDefinition,
1515
RelationType,
16+
HasManyEntityCrudRepository,
1617
} from '../..';
1718
import {expect} from '@loopback/testlab';
19+
import * as _ from 'lodash';
1820

1921
describe('HasMany relation', () => {
2022
// Given a Customer and Order models - see definitions at the bottom
@@ -24,32 +26,27 @@ describe('HasMany relation', () => {
2426
let existingCustomerId: number;
2527
//FIXME: this should be inferred from relational decorators
2628
let customerHasManyOrdersRelationMeta: HasManyDefinition;
29+
let customerOrders: HasManyEntityCrudRepository<Order>;
2730

2831
beforeEach(async () => {
2932
existingCustomerId = (await givenPersistedCustomerInstance()).id;
3033
customerHasManyOrdersRelationMeta = givenHasManyRelationMetadata();
34+
// Ideally, we would like to write
35+
// customerRepo.orders.create(customerId, orderData);
36+
// or customerRepo.orders({id: customerId}).*
37+
// The initial "involved" implementation is below
38+
39+
//FIXME: should be automagically instantiated via DI or other means
40+
customerOrders = hasManyRepositoryFactory(
41+
existingCustomerId,
42+
customerHasManyOrdersRelationMeta,
43+
orderRepo,
44+
);
3145
});
3246

3347
it('can create an instance of the related model', async () => {
34-
// A controller method - CustomerOrdersController.create()
35-
// customerRepo and orderRepo would be injected via constructor arguments
36-
async function create(customerId: number, orderData: Partial<Order>) {
37-
// Ideally, we would like to write
38-
// customerRepo.orders.create(customerId, orderData);
39-
// or customerRepo.orders({id: customerId}).*
40-
// The initial "involved" implementation is below
41-
42-
//FIXME: should be automagically instantiated via DI or other means
43-
const customerOrders = hasManyRepositoryFactory(
44-
customerId,
45-
customerHasManyOrdersRelationMeta,
46-
orderRepo,
47-
);
48-
return await customerOrders.create(orderData);
49-
}
50-
5148
const description = 'an order desc';
52-
const order = await create(existingCustomerId, {description});
49+
const order = await customerOrders.create({description});
5350

5451
expect(order.toObject()).to.containDeep({
5552
customerId: existingCustomerId,
@@ -59,6 +56,59 @@ describe('HasMany relation', () => {
5956
expect(persisted.toObject()).to.deepEqual(order.toObject());
6057
});
6158

59+
it('can patch many instances', async () => {
60+
await givenCustomerOrder({description: 'order 1', isDelivered: false});
61+
await givenCustomerOrder({description: 'order 2', isDelivered: false});
62+
const patchObject = {isDelivered: true};
63+
const arePatched = await customerOrders.patch(patchObject);
64+
expect(arePatched).to.equal(2);
65+
const patchedData = _.map(await customerOrders.find(), d =>
66+
_.pick(d, ['customerId', 'description', 'isDelivered']),
67+
);
68+
expect(patchedData).to.eql([
69+
{
70+
customerId: existingCustomerId,
71+
description: 'order 1',
72+
isDelivered: true,
73+
},
74+
{
75+
customerId: existingCustomerId,
76+
description: 'order 2',
77+
isDelivered: true,
78+
},
79+
]);
80+
});
81+
82+
it('throws error when query tries to change the foreignKey', async () => {
83+
await expect(
84+
customerOrders.patch({customerId: existingCustomerId + 1}),
85+
).to.be.rejectedWith(/Property "customerId" cannot be changed!/);
86+
});
87+
88+
it('can delete many instances', async () => {
89+
await givenCustomerOrder({description: 'order 1'});
90+
await givenCustomerOrder({description: 'order 2'});
91+
const deletedOrders = await customerOrders.delete();
92+
expect(deletedOrders).to.equal(2);
93+
const relatedOrders = await customerOrders.find();
94+
expect(relatedOrders).to.be.empty();
95+
});
96+
97+
it("does not delete instances that don't belong to the constrained instance", async () => {
98+
const newOrder = {
99+
customerId: existingCustomerId + 1,
100+
description: 'another order',
101+
};
102+
await orderRepo.create(newOrder);
103+
await customerOrders.delete();
104+
const orders = await orderRepo.find();
105+
expect(orders).to.have.length(1);
106+
expect(_.pick(orders[0], ['customerId', 'description'])).to.eql(newOrder);
107+
});
108+
109+
async function givenCustomerOrder(dataObject: Partial<Order>) {
110+
await customerOrders.create(dataObject);
111+
}
62112
// This should be enforced by the database to avoid race conditions
63113
it.skip('reject create request when the customer does not exist');
64114

@@ -92,6 +142,12 @@ describe('HasMany relation', () => {
92142
})
93143
description: string;
94144

145+
@property({
146+
type: 'boolean',
147+
required: false,
148+
})
149+
isDelivered: boolean;
150+
95151
@property({
96152
type: 'number',
97153
required: true,

packages/repository/test/unit/repositories/constraint-utils.unit.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,24 @@ describe('constraint utility functions', () => {
7676
});
7777

7878
context('constrainDataObject', () => {
79-
it('constrain a single data object', () => {
79+
it('constrains a single data object', () => {
80+
const input = new Order({description: 'order 1'});
81+
const constraint: Partial<Order> = {id: 2};
82+
expect(constrainDataObject(input, constraint)).to.containDeep({
83+
description: 'order 1',
84+
id: 2,
85+
});
86+
});
87+
88+
it('throws error when the query changes field in constrain', () => {
8089
const input = new Order({id: 1, description: 'order 1'});
8190
const constraint: Partial<Order> = {id: 2};
82-
const result = constrainDataObject(input, constraint);
83-
expect(result).to.containDeep(Object.assign({}, input, constraint));
91+
expect(() => {
92+
constrainDataObject(input, constraint);
93+
}).to.throwError(/Property "id" cannot be changed!/);
8494
});
8595

86-
it('constrain array of data objects', () => {
96+
it('constrains array of data objects', () => {
8797
const input = [
8898
new Order({id: 1, description: 'order 1'}),
8999
new Order({id: 2, description: 'order 2'}),

packages/repository/test/unit/repositories/relation.repository.unit.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {sinon} from '@loopback/testlab';
6+
import {sinon, expect} from '@loopback/testlab';
77
import {
88
EntityCrudRepository,
99
HasManyEntityCrudRepository,
@@ -13,6 +13,9 @@ import {
1313
Entity,
1414
AnyObject,
1515
Filter,
16+
Options,
17+
DataObject,
18+
Where,
1619
} from '../../..';
1720

1821
describe('relation repository', () => {
@@ -34,12 +37,26 @@ describe('relation repository', () => {
3437
targetModelData: Partial<T>,
3538
options?: AnyObject | undefined,
3639
): Promise<T> {
40+
/* istanbul ignore next */
3741
throw new Error('Method not implemented.');
3842
}
3943
find(
4044
filter?: Filter | undefined,
4145
options?: AnyObject | undefined,
4246
): Promise<T[]> {
47+
/* istanbul ignore next */
48+
throw new Error('Method not implemented.');
49+
}
50+
async delete(where?: Where, options?: Options): Promise<number> {
51+
/* istanbul ignore next */
52+
throw new Error('Method not implemented.');
53+
}
54+
async patch(
55+
dataObject: DataObject<T>,
56+
where?: Where,
57+
options?: Options,
58+
): Promise<number> {
59+
/* istanbul ignore next */
4360
throw new Error('Method not implemented.');
4461
}
4562
}
@@ -61,6 +78,36 @@ describe('relation repository', () => {
6178
const findStub = repo.find as sinon.SinonStub;
6279
sinon.assert.calledWithMatch(findStub, {where: {id: 3, name: 'Jane'}});
6380
});
81+
82+
context('patch', async () => {
83+
it('can patch related model instance', async () => {
84+
const constraint: Partial<Customer> = {name: 'Jane'};
85+
const HasManyCrudInstance = givenDefaultHasManyCrudInstance(constraint);
86+
await HasManyCrudInstance.patch({country: 'US'}, {id: 3});
87+
const patchStub = repo.updateAll as sinon.SinonStub;
88+
sinon.assert.calledWith(
89+
patchStub,
90+
{country: 'US', name: 'Jane'},
91+
{id: 3, name: 'Jane'},
92+
);
93+
});
94+
95+
it('cannot override the constrain data', async () => {
96+
const constraint: Partial<Customer> = {name: 'Jane'};
97+
const HasManyCrudInstance = givenDefaultHasManyCrudInstance(constraint);
98+
await expect(
99+
HasManyCrudInstance.patch({name: 'Joe'}),
100+
).to.be.rejectedWith(/Property "name" cannot be changed!/);
101+
});
102+
});
103+
104+
it('can delete related model instance', async () => {
105+
const constraint: Partial<Customer> = {name: 'Jane'};
106+
const HasManyCrudInstance = givenDefaultHasManyCrudInstance(constraint);
107+
await HasManyCrudInstance.delete({id: 3});
108+
const deleteStub = repo.deleteAll as sinon.SinonStub;
109+
sinon.assert.calledWith(deleteStub, {id: 3, name: 'Jane'});
110+
});
64111
});
65112

66113
/*------------- HELPERS ---------------*/

0 commit comments

Comments
 (0)