diff --git a/lib/validations.js b/lib/validations.js index 086349c11..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,6 +478,25 @@ Validatable.prototype.isValid = function (callback, data) { }); }); + nestedModelsToValidate.forEach(function (model) { + var nestedCallback = function () {}; + var nestedValid = model.inst.isValid(function (valid) { + nestedCallback(!valid); + }); + if (nestedValid === undefined) { + async = true; + wait += 1; + nestedCallback = function (fail) { + if (fail) + inst.errors.addNested(model.attr, model.inst.errors); + done(fail); + }; + } else if (!nestedValid) { + inst.errors.addNested(model.attr, model.inst.errors); + valid = false; + } + }); + if (!async) { validationsDone.call(inst, function () { if (valid) cleanErrors(inst); @@ -666,6 +710,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) { 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(); + }); + + }); + }); });