Skip to content
Closed
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
60 changes: 59 additions & 1 deletion lib/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
78 changes: 78 additions & 0 deletions test/validations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

});
});
});