Skip to content

Commit 95ab583

Browse files
author
Agnes Lin
committed
feat(repository): rejects create/update operations for navigational properties
1 parent 77cbd01 commit 95ab583

File tree

7 files changed

+415
-109
lines changed

7 files changed

+415
-109
lines changed

packages/repository-tests/src/crud/relations/acceptance/has-many-inclusion-resolver.relation.acceptance.ts

+145-4
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,142 @@ export function hasManyInclusionResolverAcceptance(
187187
expect(toJSON(result)).to.deepEqual(toJSON(expected));
188188
});
189189

190-
it('throws when navigational properties are present when updating model instance', async () => {
191-
const created = await customerRepo.create({name: 'customer'});
192-
const customerId = created.id;
190+
skipIf(
191+
features.hasRevisionToken,
192+
it,
193+
'returns inclusions after running save() operation',
194+
async () => {
195+
// this shows save() works well with func ensurePersistable and ObjectId
196+
// the test skips for Cloudant as it needs the _rev property for replacement.
197+
// see replace-by-id.suite.ts
198+
const thor = await customerRepo.create({name: 'Thor'});
199+
const odin = await customerRepo.create({name: 'Odin'});
200+
201+
const thorOrder = await orderRepo.create({
202+
customerId: thor.id,
203+
description: 'Pizza',
204+
});
205+
206+
const pizza = await orderRepo.findById(thorOrder.id);
207+
pizza.customerId = odin.id;
208+
209+
await orderRepo.save(pizza);
210+
const odinPizza = await orderRepo.findById(thorOrder.id);
211+
212+
const result = await customerRepo.findById(odin.id, {
213+
include: [{relation: 'orders'}],
214+
});
215+
const expected = {
216+
...odin,
217+
parentId: features.emptyValue,
218+
orders: [
219+
{
220+
...odinPizza,
221+
isShipped: features.emptyValue,
222+
// eslint-disable-next-line @typescript-eslint/camelcase
223+
shipment_id: features.emptyValue,
224+
},
225+
],
226+
};
227+
expect(toJSON(result)).to.containEql(toJSON(expected));
228+
},
229+
);
230+
231+
skipIf(
232+
features.hasRevisionToken,
233+
it,
234+
'returns inclusions after running replaceById() operation',
235+
async () => {
236+
// this shows replaceById() works well with func ensurePersistable and ObjectId
237+
// the test skips for Cloudant as it needs the _rev property for replacement.
238+
// see replace-by-id.suite.ts
239+
const thor = await customerRepo.create({name: 'Thor'});
240+
const odin = await customerRepo.create({name: 'Odin'});
241+
242+
const thorOrder = await orderRepo.create({
243+
customerId: thor.id,
244+
description: 'Pizza',
245+
});
246+
247+
const pizza = await orderRepo.findById(thorOrder.id.toString());
248+
pizza.customerId = odin.id;
249+
// FIXME: [mongo] if pizza obj is converted to JSON obj, it would get an error
250+
// because it tries to convert ObjectId to string type.
251+
// should test with JSON obj once it's fixed.
252+
253+
await orderRepo.replaceById(pizza.id, pizza);
254+
const odinPizza = await orderRepo.findById(thorOrder.id);
255+
256+
const result = await customerRepo.find({
257+
include: [{relation: 'orders'}],
258+
});
259+
const expected = [
260+
{
261+
...thor,
262+
parentId: features.emptyValue,
263+
},
264+
{
265+
...odin,
266+
parentId: features.emptyValue,
267+
orders: [
268+
{
269+
...odinPizza,
270+
isShipped: features.emptyValue,
271+
// eslint-disable-next-line @typescript-eslint/camelcase
272+
shipment_id: features.emptyValue,
273+
},
274+
],
275+
},
276+
];
277+
expect(toJSON(result)).to.deepEqual(toJSON(expected));
278+
},
279+
);
280+
281+
it('returns inclusions after running updateById() operation', async () => {
282+
const thor = await customerRepo.create({name: 'Thor'});
283+
const odin = await customerRepo.create({name: 'Odin'});
284+
285+
const thorOrder = await orderRepo.create({
286+
customerId: thor.id,
287+
description: 'Pizza',
288+
});
289+
290+
const pizza = await orderRepo.findById(thorOrder.id.toString());
291+
pizza.customerId = odin.id;
292+
const reheatedPizza = toJSON(pizza);
293+
294+
await orderRepo.updateById(pizza.id, reheatedPizza);
295+
const odinPizza = await orderRepo.findById(thorOrder.id);
296+
297+
const result = await customerRepo.find({
298+
include: [{relation: 'orders'}],
299+
});
300+
const expected = [
301+
{
302+
...thor,
303+
parentId: features.emptyValue,
304+
},
305+
{
306+
...odin,
307+
parentId: features.emptyValue,
308+
orders: [
309+
{
310+
...odinPizza,
311+
isShipped: features.emptyValue,
312+
// eslint-disable-next-line @typescript-eslint/camelcase
313+
shipment_id: features.emptyValue,
314+
},
315+
],
316+
},
317+
];
318+
expect(toJSON(result)).to.deepEqual(toJSON(expected));
319+
});
320+
321+
it('throws when navigational properties are present when updating model instance with save()', async () => {
322+
// save() calls replaceById so the result will be the same for replaceById
323+
const customer = await customerRepo.create({name: 'customer'});
324+
const anotherCus = await customerRepo.create({name: 'another customer'});
325+
const customerId = customer.id;
193326

194327
await orderRepo.create({
195328
description: 'pizza',
@@ -201,11 +334,19 @@ export function hasManyInclusionResolverAcceptance(
201334
});
202335
expect(found.orders).to.have.lengthOf(1);
203336

337+
const wrongOrder = await orderRepo.create({
338+
description: 'wrong order',
339+
customerId: anotherCus.id,
340+
});
341+
204342
found.name = 'updated name';
343+
found.orders.push(wrongOrder);
344+
205345
await expect(customerRepo.save(found)).to.be.rejectedWith(
206-
'The `Customer` instance is not valid. Details: `orders` is not defined in the model (value: undefined).',
346+
/Navigational properties are not allowed.*"orders"/,
207347
);
208348
});
349+
209350
// scope for inclusion is not supported yet
210351
it('throws error if the inclusion query contains a non-empty scope', async () => {
211352
const customer = await customerRepo.create({name: 'customer'});

packages/repository-tests/src/crud/relations/acceptance/has-many.relation.acceptance.ts

+79-2
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export function hasManyRelationAcceptance(
174174
);
175175
});
176176

177-
it('does not create an array of the related model', async () => {
177+
it('throws when tries to create() an instance with navigational property', async () => {
178178
await expect(
179179
customerRepo.create({
180180
name: 'a customer',
@@ -184,7 +184,84 @@ export function hasManyRelationAcceptance(
184184
},
185185
],
186186
}),
187-
).to.be.rejectedWith(/`orders` is not defined/);
187+
).to.be.rejectedWith(
188+
'Navigational properties are not allowed in model data (model "Customer" property "orders")',
189+
);
190+
});
191+
192+
it('throws when tries to createAll() instancese with navigational properties', async () => {
193+
await expect(
194+
customerRepo.createAll([
195+
{
196+
name: 'a customer',
197+
orders: [{description: 'order 1'}],
198+
},
199+
{
200+
name: 'a customer',
201+
address: {street: '1 Amedee Bonnet'},
202+
},
203+
]),
204+
).to.be.rejectedWith(
205+
'Navigational properties are not allowed in model data (model "Customer" property "orders")',
206+
);
207+
});
208+
209+
it('throws when the instance contains navigational property when operates update()', async () => {
210+
const created = await customerRepo.create({name: 'customer'});
211+
await orderRepo.create({
212+
description: 'pizza',
213+
customerId: created.id,
214+
});
215+
216+
const found = await customerRepo.findById(created.id, {
217+
include: [{relation: 'orders'}],
218+
});
219+
expect(found.orders).to.have.lengthOf(1);
220+
221+
found.name = 'updated name';
222+
await expect(customerRepo.update(found)).to.be.rejectedWith(
223+
/Navigational properties are not allowed.*"orders"/,
224+
);
225+
});
226+
227+
it('throws when the instancees contain navigational property when operates updateAll()', async () => {
228+
await customerRepo.create({name: 'Mario'});
229+
await customerRepo.create({name: 'Luigi'});
230+
231+
await expect(
232+
customerRepo.updateAll({
233+
name: 'Nintendo',
234+
orders: [{description: 'Switch'}],
235+
}),
236+
).to.be.rejectedWith(/Navigational properties are not allowed.*"orders"/);
237+
});
238+
239+
it('throws when the instance contains navigational property when operates updateById()', async () => {
240+
const customer = await customerRepo.create({name: 'Mario'});
241+
242+
await expect(
243+
customerRepo.updateById(customer.id, {
244+
name: 'Luigi',
245+
orders: [{description: 'Nintendo'}],
246+
}),
247+
).to.be.rejectedWith(/Navigational properties are not allowed.*"orders"/);
248+
});
249+
250+
it('throws when the instance contains navigational property when operates delete()', async () => {
251+
const customer = await customerRepo.create({name: 'customer'});
252+
253+
await orderRepo.create({
254+
description: 'pizza',
255+
customerId: customer.id,
256+
});
257+
258+
const found = await customerRepo.findById(customer.id, {
259+
include: [{relation: 'orders'}],
260+
});
261+
262+
await expect(customerRepo.delete(found)).to.be.rejectedWith(
263+
'Navigational properties are not allowed in model data (model "Customer" property "orders")',
264+
);
188265
});
189266

190267
context('when targeting the source model', () => {

packages/repository-tests/src/crud/relations/acceptance/has-one.relation.acceptance.ts

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function hasOneRelationAcceptance(
4949
);
5050

5151
beforeEach(async () => {
52+
await customerRepo.deleteAll();
5253
await addressRepo.deleteAll();
5354
existingCustomerId = (await givenPersistedCustomerInstance()).id;
5455
});

packages/repository-tests/src/crud/relations/helpers.ts

+6
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import {CrudFeatures, CrudRepositoryCtor} from '../..';
88
import {
99
Address,
1010
AddressRepository,
11+
Customer,
1112
CustomerRepository,
1213
Order,
1314
OrderRepository,
15+
Shipment,
1416
ShipmentRepository,
1517
} from './fixtures/models';
1618
import {
@@ -25,6 +27,10 @@ export function givenBoundCrudRepositories(
2527
repositoryClass: CrudRepositoryCtor,
2628
features: CrudFeatures,
2729
) {
30+
Order.definition.properties.id.type = features.idType;
31+
Address.definition.properties.id.type = features.idType;
32+
Customer.definition.properties.id.type = features.idType;
33+
Shipment.definition.properties.id.type = features.idType;
2834
// when running the test suite on MongoDB, we don't really need to setup
2935
// this config for mongo connector to pass the test.
3036
// however real-world applications might have such config for MongoDB

packages/repository-tests/src/crud/replace-by-id.suite.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {Entity, model, property} from '@loopback/repository';
7-
import {AnyObject, EntityCrudRepository} from '@loopback/repository';
6+
import {
7+
AnyObject,
8+
Entity,
9+
EntityCrudRepository,
10+
model,
11+
property,
12+
} from '@loopback/repository';
813
import {expect, toJSON} from '@loopback/testlab';
9-
import {MixedIdType} from '../helpers.repository-tests';
1014
import {
1115
deleteAllModelsInDefaultDataSource,
16+
MixedIdType,
1217
withCrudCtx,
1318
} from '../helpers.repository-tests';
1419
import {
@@ -72,7 +77,6 @@ export function createSuiteForReplaceById(
7277
// This important! Not all databases allow `patchById` to set
7378
// properties to "undefined", `replaceById` must always work.
7479
created.description = undefined;
75-
7680
await repo.replaceById(created.id, created);
7781

7882
const found = await repo.findById(created.id);
@@ -112,7 +116,6 @@ export function createSuiteForReplaceById(
112116
// This important! Not all databases allow `patchById` to set
113117
// properties to "undefined", `replaceById` must always work.
114118
created.description = undefined;
115-
116119
await repo.replaceById(created.id, created);
117120

118121
const found = await repo.findById(created.id);

0 commit comments

Comments
 (0)