-
Notifications
You must be signed in to change notification settings - Fork 362
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
handle invalid inputs for destroyAll #1597
Conversation
7fb58ea
to
ba16c48
Compare
lib/dao.js
Outdated
options = options || {}; | ||
|
||
assert(typeof where === 'object', 'The where argument must be an object'); | ||
// assert(typeof where === 'object', 'The where argument must be an object'); |
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.
I believe you will still need checks for the type of the value of where, since you can now pass through null values and such.
lib/dao.js
Outdated
if (typeof where === 'object' && Object.keys(where).includes('where')) { | ||
/** Option 1 | ||
* throw new Error('This method does not accept filter objects. Please pass | ||
in a where object'); |
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.
I like this option best, personally.
ba16c48
to
42e2f1c
Compare
test/manipulation.test.js
Outdated
}); | ||
|
||
it('should delete all for empty object', function(done) { | ||
Person.destroyAll({}, function(err, info) { |
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.
I think this is the dangerous behaviour we want to eliminate. If a user wants to destroyAll, then it should be called as such (with undefined
or nothin) but if an object is present then it should be a filter and not an empty object IMO.
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.
Wouldn't such change be backwards incompatible and require a semver-major release?
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.
Yes. This is a good discussion point. I agree with changing the usage to use undefined
value for the where
object (which is also the case when it's omitted in API Explorer since it's optional) to delete all records. That means changing our default assignment to undefined
since in dao.destroyAll
we default to an empty object. Then, we need to make sure that the connector specific methods also adhere to this behaviour. I also want to implement rejection of empty objects and to advise users to use undefined
instead. Yes that would mean a semver-major release, but perhaps we can mitigate it by introducing a flag for this new behaviour as well? Please keep in mind that this is also what the users complained about in the original issue.
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.
Please note that detecting {}
can be tricky.
Should we treat {userId: undefined}
the same way as {}
? Or does the first example mean "match all records that have no userId
set"? With SQL connectors, this can be probably implemented as a NULL check, but I don't know what's the level of support in noSQL connectors for such query.
perhaps we can mitigate it by introducing a flag for this new behaviour as well?
Yeah, we will have to.
Please keep in mind that this is also what the users complained about in the original issue.
As I understand the discussion in the original issue, the biggest pain point is the situation when people accidentally wrap the condition inside where
, which is an extension of the situation when they make a typo in a property name.
The case of where={}
is an edge case that was brought up later, as we started to discuss possible fixes.
I would personally focus on the first part (i.e. fix connectors to stop ignoring unknown properties) and leave changes in handling of the empty where
object for later.
Baby steps FTW!
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.
Should we treat {userId: undefined} the same way as {}?
I don't think that userId: undefined
is the same as an empty object. I think it makes more sense for that to be treated as match all records that have no userId set
while an empty object should not be allowed. If the intent is to delete all object then the user should call destroyAll()
with no arguments.
Any reason we can't have a breaking change with this module? I wouldn't consider this a breaking change considering I think this was a design oversight that we are fixing but to adhere to semver we can do a major release (at least that's my opinion).
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.
I agree with making fixes in small increments. I'll remove this test case for now (thoughts?). We could definitely follow up with issues regarding how we would like to handle empty objects. I agree that {userId: undefined}
shouldn't be the same as {}
but I think we should either reject it or yes treat it as match all records that have no userId set
.
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.
Let's make the new test code more clean please. (I do understand you took inspiration from existing tests, I don't blame you for the poor style we have in this codebase.)
Besides the comments below, please add a test showing what happens when a user makes a typo and uses a property name names
instead of name
in the query (see loopbackio/loopback-connector#132 (comment)):
Person.destroyAll({names: 'John'});
test/manipulation.test.js
Outdated
}); | ||
|
||
it('should delete all for empty object', function(done) { | ||
Person.destroyAll({}, function(err, info) { |
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.
Wouldn't such change be backwards incompatible and require a semver-major release?
test/manipulation.test.js
Outdated
}); | ||
|
||
after(function deleteFixtures(done) { | ||
Person.destroyAll(done); |
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.
Cleaning the database after the tests is an antipattern, see https://loopback.io/doc/en/lb4/Testing-your-application.html#clean-the-database-before-each-test
Start with a clean database before each test. This may seem counter-intuitive: why not reset the database after the test has finished? When a test fails and the database is cleaned after the test has finished, then it’s difficult to observe what was stored in the database and why the test failed. When the database is cleaned in the beginning, then any failing test will leave the database in the state that caused the test to fail.
test/manipulation.test.js
Outdated
{name: 'John'}, | ||
{name: 'Jane'}, | ||
{name: 'Mary'}, | ||
], done); |
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.
Sharing test data with so many tests is not ideal. When reading the test code and the assertions, it's difficult to understand where the "magic string" names are coming from.
A better solution is to create a helper function for creating an arbitrary list of person instances, and then call that helper from each test.
Also note that new tests can use Promise-based flow control to keep the code simpler.
function givenNamedPeople(namesList) {
return Promise.all(
namesList.map(name => Person.create({name})
);
}
it('should not delete using condition from filter', function() {
return givenNamedPeople(['John', 'Jane'])
.then(() =>
Person.destroyAll({where: {name: 'John'}}).should.be.rejectedWith(/Filter object detected/))
.then(() => Person.find({fields: 'name'})
.then(users => {
users.should.containDeep([
// ...
]);
);
});
Feel free to keep using callbacks if you prefer, in which case you should use something like async.map
instead of Promise.all
.
test/manipulation.test.js
Outdated
should.exist(err); | ||
err.should.match(/Filter object detected/); | ||
Person.find({fields: 'name'}, function(err, users) { | ||
should.not.exist(err); |
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.
This is an anti-pattern I have been fighting for years.
When Person.find
fails, this assertions says something along the lines "Expected {Error} to not exist" and shows a stack trace pointing to this very line, throwing the original stack trace away.
A better solution is to pass err
to test callback. That way the test fails with the original error triggered by Person.find
and provides all information from the original error (message, stack trace, error code, etc.)
if (err) return done(err);
test/manipulation.test.js
Outdated
|
||
it('should not delete all for bad and condition', function(done) { | ||
Person.destroyAll({and: [{name: 'Jane'}, {where: {name: 'John'}}]}, function(err, info) { | ||
should.not.exist(err); |
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.
if (err) return done(err);
test/manipulation.test.js
Outdated
|
||
it('should normalize filter with undefined property', function(done) { | ||
Person.destroyAll({name: undefined}, function(err, info) { | ||
should.not.exist(err); |
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.
if (err) return done(err);
test/manipulation.test.js
Outdated
Person.destroyAll({name: undefined}, function(err, info) { | ||
should.not.exist(err); | ||
Person.find({fields: 'name'}, function(err, users) { | ||
should.not.exist(err); |
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.
if (err) return done(err);
test/manipulation.test.js
Outdated
should.not.exist(err); | ||
Person.find({fields: 'name'}, function(err, users) { | ||
should.not.exist(err); | ||
users.should.containDeep([]); |
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.
Isn't this assertion always true as long as users
is any array? I think you need to call users.should.deepEqual
instead.
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.
Yes! I found that out after debugging and was looking for an alternative. Thank you :-)
test/manipulation.test.js
Outdated
should.not.exist(err); | ||
Person.find({fields: 'name'}, function(err, users) { | ||
should.not.exist(err); | ||
users.should.containDeep([]); |
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.
Isn't this assertion always true as long as users
is any array? I think you need to call users.should.deepEqual
instead.
test/manipulation.test.js
Outdated
should.not.exist(err); | ||
Person.find({fields: 'name'}, function(err, users) { | ||
should.not.exist(err); | ||
users.should.containDeep([]); |
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.
Isn't this assertion always true as long as users
is any array? I think you need to call users.should.deepEqual
instead.
test/manipulation.test.js
Outdated
@@ -1686,7 +1700,73 @@ describe('manipulation', function() { | |||
}); | |||
|
|||
// TODO: implement destroy with filtered set | |||
it('should destroy filtered set of records'); | |||
it.skip('should destroy filtered set of records'); |
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.
Reminder: This should be implemented / removed to cleanup the code.
test/manipulation.test.js
Outdated
}); | ||
|
||
it('should delete all for empty object', function(done) { | ||
Person.destroyAll({}, function(err, info) { |
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.
Should we treat {userId: undefined} the same way as {}?
I don't think that userId: undefined
is the same as an empty object. I think it makes more sense for that to be treated as match all records that have no userId set
while an empty object should not be allowed. If the intent is to delete all object then the user should call destroyAll()
with no arguments.
Any reason we can't have a breaking change with this module? I wouldn't consider this a breaking change considering I think this was a design oversight that we are fixing but to adhere to semver we can do a major release (at least that's my opinion).
LoopBack users cannot choose the juggler version they are using, juggler version is controlled by When we release a new semver-major version of juggler (4.0), we need to decide what to do with Is it going to keep using juggler 3.x and not receiving this fix? To me, that sounds as not very useful to LB 3.x users. If we upgrade |
That makes sense. I was thinking of |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
@b-admike, are you still interested in completing this PR? Thanks. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners, listed in the |
Description
Fixes strongloop/loopback#3094. At the moment, there is just a test to reproduce the issue (which I wasn't able to locally, so will gather more feedback).
Checklist
guide