From 22cc726cc282a3c2e33d45d8bc75ec5ef00011de Mon Sep 17 00:00:00 2001 From: Oleksandr Podoprygora Date: Fri, 6 Mar 2015 21:32:30 +0200 Subject: [PATCH 1/3] Validation only occurs at root level when there are nested Models #136 --- lib/validations.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lib/validations.js b/lib/validations.js index 086349c11..c11860c3c 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -453,6 +453,34 @@ Validatable.prototype.isValid = function (callback, data) { }); }); + var nestedModelsToValidate = Object.keys(inst.constructor.definition.properties) + .filter(function (attr) { + if (data[attr] === undefined) { + return false; + } + return inst[attr].isValid === Validatable.prototype.isValid; + }); + nestedModelsToValidate.forEach(function (attr) { + var nestedInst = inst[attr]; + var nestedData = data[attr]; + var nestedCallback = function () {}; + var nestedValid = nestedInst.isValid(function (valid) { + nestedCallback(!valid); + }, nestedData); + if (nestedValid === undefined) { + async = true; + wait += 1; + nestedCallback = function (fail) { + if (fail) + inst.errors.addNested(attr, nestedInst.errors); + done(fail); + }; + } else if (!nestedValid) { + inst.errors.addNested(attr, nestedInst.errors); + valid = false; + } + }); + if (!async) { validationsDone.call(inst, function () { if (valid) cleanErrors(inst); @@ -666,6 +694,20 @@ Errors.prototype.add = function (field, message, code) { this.codes[field].push(code); }; +Errors.prototype.addNested = function (parentField, nestedErrors) { + var self = this; + var fields = Object.keys(nestedErrors); + fields.forEach(function (field) { + var errorsCount = nestedErrors[field].length; + for (var i = 0; i < errorsCount; i++) { + var parentAttr = parentField + "." + field; + var message = nestedErrors[field][i]; + var code = nestedErrors.codes[field][i]; + self.add(parentAttr, message, code); + } + }); +} + function ErrorCodes(messages) { var c = this; Object.keys(messages).forEach(function (field) { From b9d7b2c8e1a80afa573df6ac56c7d4d7a433158c Mon Sep 17 00:00:00 2001 From: Oleksandr Podoprygora Date: Sat, 7 Mar 2015 04:37:09 +0200 Subject: [PATCH 2/3] nested arrays validation --- lib/validations.js | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/validations.js b/lib/validations.js index c11860c3c..ffa1ea5a0 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -454,29 +454,48 @@ Validatable.prototype.isValid = function (callback, data) { }); var nestedModelsToValidate = Object.keys(inst.constructor.definition.properties) - .filter(function (attr) { - if (data[attr] === undefined) { - return false; + .filter(function (attr) { return data[attr] !== undefined; }) + .map(function (attr) { + var result = []; + var nestedInst = inst[attr]; + var nestedData = data[attr]; + if (Array.isArray(nestedInst) && nestedInst.itemType.prototype.isValid === Validatable.prototype.isValid) { + for (var i = 0; i < nestedInst.length; i++) { + result.push({ + attr: attr + "." + i, + inst: nestedInst[i], + data: nestedData[i] + }); + } + } else if (nestedInst.isValid === Validatable.prototype.isValid) { + result.push({ + attr: attr, + inst: nestedInst, + data: nestedData + }); } - return inst[attr].isValid === Validatable.prototype.isValid; - }); - nestedModelsToValidate.forEach(function (attr) { - var nestedInst = inst[attr]; - var nestedData = data[attr]; + return result; + }) + .reduce(function (result, models) { + Array.prototype.push.apply(result, models); + return result; + }, []); + + nestedModelsToValidate.forEach(function (model) { var nestedCallback = function () {}; - var nestedValid = nestedInst.isValid(function (valid) { + var nestedValid = model.inst.isValid(function (valid) { nestedCallback(!valid); - }, nestedData); + }, model.data); if (nestedValid === undefined) { async = true; wait += 1; nestedCallback = function (fail) { if (fail) - inst.errors.addNested(attr, nestedInst.errors); + inst.errors.addNested(model.attr, model.inst.errors); done(fail); }; } else if (!nestedValid) { - inst.errors.addNested(attr, nestedInst.errors); + inst.errors.addNested(model.attr, model.inst.errors); valid = false; } }); From dbe87a2226395655c5706f2c071789b3a502d61f Mon Sep 17 00:00:00 2001 From: Oleksandr Podoprygora Date: Wed, 18 Mar 2015 03:08:49 +0200 Subject: [PATCH 3/3] fix: nested validations does not work for models without any validations fix: using inst[attr] instead of data[attr] adding tests --- lib/validations.js | 57 ++++++++++++++--------------- test/validations.test.js | 78 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 30 deletions(-) diff --git a/lib/validations.js b/lib/validations.js index ffa1ea5a0..ee037595a 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -411,8 +411,33 @@ Validatable.prototype.isValid = function (callback, data) { var valid = true, inst = this, wait = 0, async = false; var validations = this.constructor.validations; + var nestedModelsToValidate = Object.keys(inst.constructor.definition.properties) + .filter(function (attr) { return inst[attr] !== null && typeof(inst[attr]) === 'object' ; }) + .map(function (attr) { + var result = []; + var nestedInst = inst[attr]; + if (Array.isArray(nestedInst) && nestedInst.itemType.prototype.isValid === Validatable.prototype.isValid) { + for (var i = 0; i < nestedInst.length; i++) { + result.push({ + attr: attr + "." + i, + inst: nestedInst[i] + }); + } + } else if (nestedInst.isValid === Validatable.prototype.isValid) { + result.push({ + attr: attr, + inst: nestedInst + }); + } + return result; + }) + .reduce(function (result, models) { + Array.prototype.push.apply(result, models); + return result; + }, []); + // exit with success when no errors - if (typeof validations !== 'object') { + if (typeof validations !== 'object' && !nestedModelsToValidate.length) { cleanErrors(this); if (callback) { this.trigger('validate', function (validationsDone) { @@ -453,39 +478,11 @@ Validatable.prototype.isValid = function (callback, data) { }); }); - var nestedModelsToValidate = Object.keys(inst.constructor.definition.properties) - .filter(function (attr) { return data[attr] !== undefined; }) - .map(function (attr) { - var result = []; - var nestedInst = inst[attr]; - var nestedData = data[attr]; - if (Array.isArray(nestedInst) && nestedInst.itemType.prototype.isValid === Validatable.prototype.isValid) { - for (var i = 0; i < nestedInst.length; i++) { - result.push({ - attr: attr + "." + i, - inst: nestedInst[i], - data: nestedData[i] - }); - } - } else if (nestedInst.isValid === Validatable.prototype.isValid) { - result.push({ - attr: attr, - inst: nestedInst, - data: nestedData - }); - } - return result; - }) - .reduce(function (result, models) { - Array.prototype.push.apply(result, models); - return result; - }, []); - nestedModelsToValidate.forEach(function (model) { var nestedCallback = function () {}; var nestedValid = model.inst.isValid(function (valid) { nestedCallback(!valid); - }, model.data); + }); if (nestedValid === undefined) { async = true; wait += 1; diff --git a/test/validations.test.js b/test/validations.test.js index 9df68d465..d967a92cf 100644 --- a/test/validations.test.js +++ b/test/validations.test.js @@ -535,4 +535,82 @@ describe('validations', function () { return err.message.replace(/^.*Details: /, ''); } }); + + describe('nested models', function() { + var Parent; + describe('nested property', function() { + before(function (done) { + Parent = db.define('Parent', { + user: User + }); + done(); + }); + + it('should skip undefined values', function(done) { + var parent = new Parent(); + parent.isValid().should.be.true; + done(); + }); + it('should skip null values', function(done) { + var parent = new Parent({user: null}); + parent.isValid().should.be.true; + done(); + }); + it('validate property value', function(done) { + User.validatesPresenceOf('name'); + var parent = new Parent({user: {}}); + parent.isValid().should.not.be.true; + parent.errors['user.name'].should.eql(['can\'t be blank']); + parent.user.name = 1; + parent.isValid().should.true; + done(); + }); + it('property value has async validations', function (done) { + function customValidator(err, done) { + var self = this; + process.nextTick(function () { + if (self.name === 'bad') err(); + done(); + }); + }; + User.validateAsync('name', customValidator, {message: 'Bad name'}); + var parent = new Parent({user: {name: 'bad'}}); + should(parent.isValid(function(valid) { + valid.should.not.be.true; + parent.user.name = 1; + should(parent.isValid(function(valid) { + valid.should.be.true; + done(); + })).be.undefined; + })).be.undefined; + }); + }); + describe('nested array', function() { + before(function (done) { + Parent = db.define('Parent', { + users: [User] + }); + done(); + }); + + it('should work for null values', function(done) { + var parent = new Parent({users: []}); + parent.users.push(null); + parent.isValid().should.be.true; + done(); + }); + + it('validate value', function(done) { + User.validatesPresenceOf('name'); + var parent = new Parent({users: []}); + parent.users.push({}); + parent.isValid().should.not.be.true; + parent.errors['users.0.name'].should.eql(['can\'t be blank']); + parent.users[0].name = 1; + parent.isValid().should.be.true; + done(); + }); + + }); + }); });