-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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): rejects CRUD operations for navigational properties #4148
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -187,9 +187,142 @@ export function hasManyInclusionResolverAcceptance( | |
expect(toJSON(result)).to.deepEqual(toJSON(expected)); | ||
}); | ||
|
||
it('throws when navigational properties are present when updating model instance', async () => { | ||
const created = await customerRepo.create({name: 'customer'}); | ||
const customerId = created.id; | ||
skipIf( | ||
features.hasRevisionToken, | ||
it, | ||
'returns inclusions after running save() operation', | ||
async () => { | ||
// this shows save() works well with func ensurePersistable and ObjectId | ||
// the test skips for Cloudant as it needs the _rev property for replacement. | ||
// see replace-by-id.suite.ts | ||
const thor = await customerRepo.create({name: 'Thor'}); | ||
const odin = await customerRepo.create({name: 'Odin'}); | ||
|
||
const thorOrder = await orderRepo.create({ | ||
customerId: thor.id, | ||
description: 'Pizza', | ||
}); | ||
|
||
const pizza = await orderRepo.findById(thorOrder.id); | ||
pizza.customerId = odin.id; | ||
|
||
await orderRepo.save(pizza); | ||
const odinPizza = await orderRepo.findById(thorOrder.id); | ||
|
||
const result = await customerRepo.findById(odin.id, { | ||
include: [{relation: 'orders'}], | ||
}); | ||
const expected = { | ||
...odin, | ||
parentId: features.emptyValue, | ||
orders: [ | ||
{ | ||
...odinPizza, | ||
isShipped: features.emptyValue, | ||
// eslint-disable-next-line @typescript-eslint/camelcase | ||
shipment_id: features.emptyValue, | ||
}, | ||
], | ||
}; | ||
expect(toJSON(result)).to.containEql(toJSON(expected)); | ||
}, | ||
); | ||
|
||
skipIf( | ||
features.hasRevisionToken, | ||
it, | ||
'returns inclusions after running replaceById() operation', | ||
async () => { | ||
// this shows replaceById() works well with func ensurePersistable and ObjectId | ||
// the test skips for Cloudant as it needs the _rev property for replacement. | ||
// see replace-by-id.suite.ts | ||
const thor = await customerRepo.create({name: 'Thor'}); | ||
const odin = await customerRepo.create({name: 'Odin'}); | ||
|
||
const thorOrder = await orderRepo.create({ | ||
customerId: thor.id, | ||
description: 'Pizza', | ||
}); | ||
|
||
const pizza = await orderRepo.findById(thorOrder.id.toString()); | ||
pizza.customerId = odin.id; | ||
// FIXME: [mongo] if pizza obj is converted to JSON obj, it would get an error | ||
// because it tries to convert ObjectId to string type. | ||
// should test with JSON obj once it's fixed. | ||
|
||
await orderRepo.replaceById(pizza.id, pizza); | ||
const odinPizza = await orderRepo.findById(thorOrder.id); | ||
|
||
const result = await customerRepo.find({ | ||
include: [{relation: 'orders'}], | ||
}); | ||
const expected = [ | ||
{ | ||
...thor, | ||
parentId: features.emptyValue, | ||
}, | ||
{ | ||
...odin, | ||
parentId: features.emptyValue, | ||
orders: [ | ||
{ | ||
...odinPizza, | ||
isShipped: features.emptyValue, | ||
// eslint-disable-next-line @typescript-eslint/camelcase | ||
shipment_id: features.emptyValue, | ||
}, | ||
], | ||
}, | ||
]; | ||
expect(toJSON(result)).to.deepEqual(toJSON(expected)); | ||
}, | ||
); | ||
|
||
it('returns inclusions after running updateById() operation', async () => { | ||
const thor = await customerRepo.create({name: 'Thor'}); | ||
const odin = await customerRepo.create({name: 'Odin'}); | ||
|
||
const thorOrder = await orderRepo.create({ | ||
customerId: thor.id, | ||
description: 'Pizza', | ||
}); | ||
|
||
const pizza = await orderRepo.findById(thorOrder.id.toString()); | ||
pizza.customerId = odin.id; | ||
const reheatedPizza = toJSON(pizza); | ||
|
||
await orderRepo.updateById(pizza.id, reheatedPizza); | ||
const odinPizza = await orderRepo.findById(thorOrder.id); | ||
|
||
const result = await customerRepo.find({ | ||
include: [{relation: 'orders'}], | ||
}); | ||
const expected = [ | ||
{ | ||
...thor, | ||
parentId: features.emptyValue, | ||
}, | ||
{ | ||
...odin, | ||
parentId: features.emptyValue, | ||
orders: [ | ||
{ | ||
...odinPizza, | ||
isShipped: features.emptyValue, | ||
// eslint-disable-next-line @typescript-eslint/camelcase | ||
shipment_id: features.emptyValue, | ||
}, | ||
], | ||
}, | ||
]; | ||
expect(toJSON(result)).to.deepEqual(toJSON(expected)); | ||
}); | ||
|
||
it('throws when navigational properties are present when updating model instance with save()', async () => { | ||
// save() calls replaceById so the result will be the same for replaceById | ||
const customer = await customerRepo.create({name: 'customer'}); | ||
const anotherCus = await customerRepo.create({name: 'another customer'}); | ||
const customerId = customer.id; | ||
|
||
await orderRepo.create({ | ||
description: 'pizza', | ||
|
@@ -201,11 +334,19 @@ export function hasManyInclusionResolverAcceptance( | |
}); | ||
expect(found.orders).to.have.lengthOf(1); | ||
|
||
const wrongOrder = await orderRepo.create({ | ||
description: 'wrong order', | ||
customerId: anotherCus.id, | ||
}); | ||
|
||
found.name = 'updated name'; | ||
found.orders.push(wrongOrder); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it a requirement to modify |
||
|
||
await expect(customerRepo.save(found)).to.be.rejectedWith( | ||
'The `Customer` instance is not valid. Details: `orders` is not defined in the model (value: undefined).', | ||
/Navigational properties are not allowed.*"orders"/, | ||
); | ||
}); | ||
|
||
// scope for inclusion is not supported yet | ||
it('throws error if the inclusion query contains a non-empty scope', async () => { | ||
const customer = await customerRepo.create({name: 'customer'}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please provide more details on why are these new test cases skipped? IIUC, you are running these tests only for CouchDB/Cloudant and skipping them for all other databases (memory, PostgreSQL, MongoDB). The code in the test looks legit to me, I find it suspicious that it does not work on so many database types.
Ideally, I'd like to find a way how to enable these tests for all databases we support.
If that's not possible or if it requires too much effort, then I would like you to add code comments near
!features.hasRevisionToken
line to explain why each test is skipped.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test should run on all databases except
Cloudant
cause it needs the_rev
property. I will add comments to clarify.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@agnes512 thank you for replying in time, I am running these 2 tests and I think I understand why they fail for Cloudant and mongodb, fixing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos The test case here skips Cloudant connector because the Customer model in fixtures doesn't have
_rev
property defined.skipIf(features.hasRevisionToken, testcase){//tests}
means whenfeatures.hasRevisionToken
is true(which only applies for the cloudant connector), the test will be skipped.I am not very familiar with the shared test case, so agreed with Agnes's solution,
if you want to enable same tests for cloudant, I can add them in a separate PR (within same story).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos The test fails for mongodb because the id type is
object
not string, therefore fails at the check in https://github.com/strongloop/loopback-datasource-juggler/blob/master/lib/dao.js#L2621, hmm...any suggestion on how to fix it?Update, just talked to Agnes, she points me to the fix code commented in https://github.com/strongloop/loopback-next/blob/49ecf1b86f058576404f28c88d4650651fc31419/packages/repository/src/repositories/legacy-juggler-bridge.ts#L580-L585 :)
Do you agree on the solution? If it is acceptable I will recover this code. (Then I believe all tests will pass)