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

[SEMVER-MAJOR] Strict mode cleanup #1084

Merged
merged 1 commit into from
Sep 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions 3.0-RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,55 @@ bulk `updateOrCreate`, you could use `async.each` or `Promise.all` to
repeatedly call `updateOfCreate` for each instance.

See [related code change](https://github.com/strongloop/loopback-datasource-juggler/pull/889) here.

## Removing strict: validate and strict: throw
Given a user model definition as follow:
```js
ds.define('User', {
name: String, required: false,
age: Number, required: false
})

var johndoe = new User({ name: 'John doe', age: 15, gender: 'm'});
```
- #### strict: false
behavior will remain the same and unknown properties will be saved along with other properties
```js
//johndoe would be created as
{
id: 1,
name: 'John doe',
age: 15,
geneder: 'm'
}
```

- #### strict: `'validate'` and strict: `'throw'`
are removed in favor of `strict: true`, and because non-empty string
resolves as true, they will share the same behavior as `strict: true`

- #### strict: `true`'s behavior change:

**Previously:** (2.x)

`strict:true` would silently removing unknown properties, and User instance will be created with unknown properties discarded

```js
johndoe.isValid(); //true
//johndoe would be created as
{
id: 1,
name: 'John doe',
age: 15
}
```

**New Behavior:** (3.x)

```js
johndoe.isValid(); //false
//johndoe would NOT be created and will result in the following error
//ValidationError: The User instance is not valid. Details: gender is not defined in the model (value: 'm').
```

See [related code change](https://github.com/strongloop/loopback-datasource-juggler/pull/1084) here.
5 changes: 1 addition & 4 deletions lib/dao.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,7 @@ function applyStrictCheck(model, strict, data, inst, cb) {
key = keys[i];
if (props[key]) {
result[key] = data[key];
} else if (strict === 'throw') {
cb(new Error(g.f('Unknown property: %s', key)));
return;
} else if (strict === 'validate') {
} else if (strict) {
inst.__unknownProperties.push(key);
}
}
Expand Down
12 changes: 7 additions & 5 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ ModelBaseClass.prototype._initProperties = function(data, options) {

if (strict === undefined) {
strict = ctor.definition.settings.strict;
} else if (strict === 'throw') {
g.warn('Warning: Model %s, {{strict mode: `throw`}} has been removed, ' +
'please use {{`strict: true`}} instead, which returns' +
'{{`Validation Error`}} for unknown properties,', ctor.modelName);
}

var persistUndefinedAsNull = ctor.definition.settings.persistUndefinedAsNull;
Expand Down Expand Up @@ -141,7 +145,7 @@ ModelBaseClass.prototype._initProperties = function(data, options) {
},
});

if (strict === 'validate') {
if (strict) {
Object.defineProperty(this, '__unknownProperties', {
writable: true,
enumerable: false,
Expand All @@ -155,7 +159,7 @@ ModelBaseClass.prototype._initProperties = function(data, options) {
this.__dataSource = options.dataSource;
this.__strict = strict;
this.__persisted = false;
if (strict === 'validate') {
if (strict) {
this.__unknownProperties = [];
}
}
Expand Down Expand Up @@ -243,9 +247,7 @@ ModelBaseClass.prototype._initProperties = function(data, options) {
this.constructor.modelName, p
));
}
} else if (strict === 'throw') {
throw new Error(g.f('Unknown property: %s', p));
} else if (strict === 'validate') {
} else {
this.__unknownProperties.push(p);
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ Validatable.prototype.isValid = function(callback, data, options) {
var valid = true, inst = this, wait = 0, async = false;
var validations = this.constructor.validations;

var reportDiscardedProperties = this.__strict === 'validate' &&
var reportDiscardedProperties = this.__strict &&
this.__unknownProperties && this.__unknownProperties.length;

// exit with success when no errors
Expand Down
5 changes: 3 additions & 2 deletions test/datatype.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ describe('datatypes', function() {
TestModel = db.define(
'TestModel',
{
name: {type: String, required: false},
desc: {type: String, required: false},
stars: {type: Number, required: false},
},
Expand Down Expand Up @@ -220,12 +221,12 @@ describe('datatypes', function() {

it('should convert property value undefined to null', function(done) {
var EXPECTED = {desc: null, extra: null};
var data = {desc: undefined, extra: undefined};
if (isStrict) {
// SQL-based connectors don't support dynamic properties
delete EXPECTED.extra;
delete data.extra;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it necessary to delete data.extra?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mysql fails with the following otherwise, seems it does not discard undefined properties which leads to

1) mysql imported features datatypes model option persistUndefinedAsNull should convert property value undefined to null:
     ValidationError: The `TestModel` instance is not valid. Details: `extra` is not defined in the model (value: undefined); `haha` is not defined in the model (value: undefined).
      at /Users/davidcheung/node-web/loopback-datasource-juggler/lib/dao.js:327:12
      at ModelConstructor.<anonymous> (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/validations.js:495:11)
      at ModelConstructor.next (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/hooks.js:83:12)
      at ModelConstructor.<anonymous> (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/validations.js:492:23)
      at ModelConstructor.trigger (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/hooks.js:73:12)
      at ModelConstructor.Validatable.isValid (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/validations.js:458:8)
      at /Users/davidcheung/node-web/loopback-datasource-juggler/lib/dao.js:323:9
      at doNotify (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/observer.js:99:49)
      at doNotify (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/observer.js:99:49)
      at Function.ObserverMixin._notifyBaseObservers (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/observer.js:122:5)
      at Function.ObserverMixin.notifyObserversOf (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/observer.js:97:8)
      at Function.ObserverMixin._notifyBaseObservers (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/observer.js:120:15)
      at Function.ObserverMixin.notifyObserversOf (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/observer.js:97:8)
      at Function.DataAccessObject.create (/Users/davidcheung/node-web/loopback-datasource-juggler/lib/dao.js:307:9)
      at Context.<anonymous> (/Users/davidcheung/node-web/loopback-datasource-juggler/test/datatype.test.js:229:17)

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'm not sure what to do with this test case, so the option of { persistUndefinedAsNull: true } disables the discarding of undefined property, while strict mode will return validation error.

in this case deleting data.extra is needed ( and i think is correct behavior? )
while the test will still check for the defined property (desc) which fulfills the original intent of the test

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised the problem was not discovered earlier. Is it really related to the changes made in this pull request? If it isn't, then could you perhaps isolate it in a standalone commit (if not send it as a new PR)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is related to this change, because sql database uses strict:true which silently remove before, but now returns err

Copy link
Member

@bajtos bajtos Sep 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slap in the forehead

Ah, of course! Makes a lot of sense.

}

var data = {desc: undefined, extra: undefined};
TestModel.create(data, function(err, created) {
if (err) return done(err);

Expand Down
39 changes: 0 additions & 39 deletions test/loopback-dl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1917,47 +1917,8 @@ describe('ModelBuilder options.models', function() {
},
});
assert.equal(m1.address.number, undefined, 'm1 should not contain number property in address');
});

it('should use strictEmbeddedModels setting (validate) when applied on modelBuilder', function() {
var builder = new ModelBuilder();
builder.settings.strictEmbeddedModels = 'validate';
var M1 = builder.define('testEmbedded', {
name: 'string',
address: {
street: 'string',
},
});
var m1 = new M1({
name: 'Jim',
address: {
street: 'washington st',
number: 5512,
},
});
assert.equal(m1.address.number, undefined, 'm1 should not contain number property in address');
assert.equal(m1.address.isValid(), false, 'm1 address should not validate with extra property');
var codes = m1.address.errors && m1.address.errors.codes || {};
assert.deepEqual(codes.number, ['unknown-property']);
});

it('should use the strictEmbeddedModels setting (throw) when applied on modelBuilder', function() {
var builder = new ModelBuilder();
builder.settings.strictEmbeddedModels = 'throw';
var M1 = builder.define('testEmbedded', {
name: 'string',
address: {
street: 'string',
},
});
assert.throws(function() {
var m1 = new M1({
name: 'Jim',
address: {
street: 'washington st',
number: 5512,
},
});
});
});
});
84 changes: 63 additions & 21 deletions test/manipulation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -544,41 +544,83 @@ describe('manipulation', function() {
});
});

it('should ignore unknown attributes when strict: true', function(done) {
// Using {foo: 'bar'} only causes dependent test failures due to the
// stripping of object properties when in strict mode (ie. {foo: 'bar'}
// changes to '{}' and breaks other tests
person.updateAttributes({name: 'John', foo: 'bar'},
function(err, p) {
if (err) return done(err);
should.not.exist(p.foo);
Person.findById(p.id, function(e, p) {
if (e) return done(e);
should.not.exist(p.foo);
done();
});
it('should discard undefined values before strict validation',
function(done) {
Person.definition.settings.strict = true;
Person.findById(person.id, function(err, p) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved it under it('should return error on unknown attributes when strict: true',

p.updateAttributes({name: 'John', unknownVar: undefined},
function(err, p) {
// if uknownVar was defined, it would return validationError
if (err) return done(err);
Person.findById(p.id, function(e, p) {
if (e) return done(e);
p.name.should.equal('John');
p.should.not.have.property('unknownVar');
done();
});
});
});
});
});

it('should allow unknown attributes when strict: false',
function(done) {
Person.definition.settings.strict = false;
Person.findById(person.id, function(err, p) {
p.updateAttributes({name: 'John', foo: 'bar'},
function(err, p) {
if (err) return done(err);
p.should.have.property('foo');
done();
});
});
});

it('should throw error on unknown attributes when strict: throw', function(done) {
// Prior to version 3.0 `strict: true` used to silently remove unknown properties,
// now return validationError upon unknown properties
it('should return error on unknown attributes when strict: true',
function(done) {
// Using {foo: 'bar'} only causes dependent test failures due to the
// stripping of object properties when in strict mode (ie. {foo: 'bar'}
// changes to '{}' and breaks other tests
Person.definition.settings.strict = true;
Person.findById(person.id, function(err, p) {
p.updateAttributes({name: 'John', foo: 'bar'},
function(err, p) {
should.exist(err);
err.name.should.equal('ValidationError');
err.message.should.containEql('`foo` is not defined in the model');
p.should.not.have.property('foo');
Person.findById(p.id, function(e, p) {
if (e) return done(e);
p.should.not.have.property('foo');
done();
});
});
});
});

// strict: throw is deprecated, use strict: true instead
// which returns Validation Error for unknown properties
it('should fallback to strict:true when using strict: throw', function(done) {
Person.definition.settings.strict = 'throw';
Person.findById(person.id, function(err, p) {
p.updateAttributes({foo: 'bar'},
function(err, p) {
should.exist(err);
err.name.should.equal('Error');
err.message.should.equal('Unknown property: foo');
should.not.exist(p);
err.name.should.equal('ValidationError');
err.message.should.containEql('`foo` is not defined in the model');
Person.findById(person.id, function(e, p) {
if (e) return done(e);
should.not.exist(p.foo);
p.should.not.have.property('foo');
done();
});
});
});
});

it('should throw error on unknown attributes when strict: throw', function(done) {
// strict: validate is deprecated, use strict: true instead
// behavior remains the same as before, because validate is now default behavior
it('should fallback to strict:true when using strict:validate', function(done) {
Person.definition.settings.strict = 'validate';
Person.findById(person.id, function(err, p) {
p.updateAttributes({foo: 'bar'},
Expand All @@ -588,7 +630,7 @@ describe('manipulation', function() {
err.message.should.containEql('`foo` is not defined in the model');
Person.findById(person.id, function(e, p) {
if (e) return done(e);
should.not.exist(p.foo);
p.should.not.have.property('foo');
done();
});
});
Expand Down