Skip to content
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

Closed
wants to merge 2 commits into from
Closed

handle invalid inputs for destroyAll #1597

wants to merge 2 commits into from

Conversation

b-admike
Copy link
Contributor

@b-admike b-admike commented Jun 21, 2018

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

  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style
    guide

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');
Copy link
Contributor

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');
Copy link
Contributor

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.

@b-admike b-admike changed the title [WIP] handle bad filter(s) for destroyAll handle invalid inputs for destroyAll Sep 6, 2018
});

it('should delete all for empty object', function(done) {
Person.destroyAll({}, function(err, info) {
Copy link
Contributor

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.

Copy link
Member

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?

Copy link
Contributor Author

@b-admike b-admike Sep 7, 2018

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.

Copy link
Member

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!

Copy link
Contributor

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).

Copy link
Contributor Author

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.

Copy link
Member

@bajtos bajtos left a 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'});

});

it('should delete all for empty object', function(done) {
Person.destroyAll({}, function(err, info) {
Copy link
Member

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?

});

after(function deleteFixtures(done) {
Person.destroyAll(done);
Copy link
Member

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.

{name: 'John'},
{name: 'Jane'},
{name: 'Mary'},
], done);
Copy link
Member

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.

should.exist(err);
err.should.match(/Filter object detected/);
Person.find({fields: 'name'}, function(err, users) {
should.not.exist(err);
Copy link
Member

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);


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);
Copy link
Member

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);


it('should normalize filter with undefined property', function(done) {
Person.destroyAll({name: undefined}, function(err, info) {
should.not.exist(err);
Copy link
Member

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);

Person.destroyAll({name: undefined}, function(err, info) {
should.not.exist(err);
Person.find({fields: 'name'}, function(err, users) {
should.not.exist(err);
Copy link
Member

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);

should.not.exist(err);
Person.find({fields: 'name'}, function(err, users) {
should.not.exist(err);
users.should.containDeep([]);
Copy link
Member

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.

Copy link
Contributor Author

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 :-)

should.not.exist(err);
Person.find({fields: 'name'}, function(err, users) {
should.not.exist(err);
users.should.containDeep([]);
Copy link
Member

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.

should.not.exist(err);
Person.find({fields: 'name'}, function(err, users) {
should.not.exist(err);
users.should.containDeep([]);
Copy link
Member

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.

@@ -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');
Copy link
Contributor

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.

});

it('should delete all for empty object', function(done) {
Person.destroyAll({}, function(err, info) {
Copy link
Contributor

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).

@bajtos
Copy link
Member

bajtos commented Sep 11, 2018

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 loopback's dependencies specified in its package.json.

When we release a new semver-major version of juggler (4.0), we need to decide what to do with [email protected].

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 loopback dependencies to use [email protected], then we need to publish such change as [email protected]. Can we release [email protected] when our intention is to release loopback-next as the 4.0 version of LoopBack?

@virkt25
Copy link
Contributor

virkt25 commented Sep 12, 2018

LoopBack users cannot choose the juggler version they are using, juggler version is controlled by loopback's dependencies specified in its package.json.

That makes sense. I was thinking of juggler as an individual module. I think @b-admike has a fix which won't be breaking.

@stale
Copy link

stale bot commented Jul 11, 2019

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.

@stale stale bot added the stale label Jul 11, 2019
@dhmlau
Copy link
Member

dhmlau commented Jul 15, 2019

@b-admike, are you still interested in completing this PR? Thanks.

@stale stale bot removed the stale label Jul 15, 2019
@stale
Copy link

stale bot commented Sep 13, 2019

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.

@stale stale bot added the stale label Sep 13, 2019
@stale
Copy link

stale bot commented Sep 28, 2019

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 CODEOWNERS file at the top-level of this repository.

@stale stale bot closed this Sep 28, 2019
@rmg rmg deleted the fix/destroyall branch March 18, 2021 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PersistedModel.destroyAll has very dangerous behaviour if the filter is not passed in correctly.
5 participants