diff --git a/lib/validations.js b/lib/validations.js index 017c017e..200bf6ba 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -197,9 +197,19 @@ Validatable.validateAsync = getConfigurator('custom', {async: true}); * - Oracle * - MongoDB * + * ``` + * // The login must be unique across all User instances. + * User.validatesUniquenessOf('login'); + * + * // Assuming SiteUser.belongsTo(Site) + * // The login must be unique within each Site. + * SiteUser.validateUniquenessOf('login', { scopedTo: ['siteId'] }); + * ``` + * @param {String} propertyName Property name to validate. * @options {Object} Options * @property {RegExp} with Regular expression to validate format. + * @property {Array.} scopedTo List of properties defining the scope. * @property {String} message Optional error message if property is not valid. Default error message: "is not unique". */ Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true}); @@ -297,6 +307,15 @@ function validateCustom(attr, conf, err, done) { function validateUniqueness(attr, conf, err, done) { var cond = {where: {}}; cond.where[attr] = this[attr]; + + if (conf && conf.scopedTo) { + conf.scopedTo.forEach(function(k) { + var val = this[k]; + if (val !== undefined) + cond.where[k] = this[k]; + }, this); + } + this.constructor.find(cond, function (error, found) { if (error) { return err(); diff --git a/test/validations.test.js b/test/validations.test.js index 54a50237..f01414aa 100644 --- a/test/validations.test.js +++ b/test/validations.test.js @@ -1,5 +1,6 @@ // This test written in mocha+should.js var should = require('./init.js'); +var async = require('async'); var j = require('../'), db, User; var ValidationError = j.ValidationError; @@ -172,6 +173,41 @@ describe('validations', function () { })).should.not.be.ok; }); + it('should support multi-key constraint', function(done) { + var EMAIL = 'user@xample.com'; + var SiteUser = db.define('SiteUser', { + siteId: String, + email: String + }); + SiteUser.validatesUniquenessOf('email', { scopedTo: ['siteId'] }); + async.waterfall([ + function automigrate(next) { + db.automigrate(next); + }, + function createSite1User(next) { + SiteUser.create( + { siteId: 1, email: EMAIL }, + next); + }, + function createSite2User(user1, next) { + SiteUser.create( + { siteId: 2, email: EMAIL }, + next); + }, + function validateDuplicateUser(user2, next) { + var user3 = new SiteUser({ siteId: 1, email: EMAIL }); + user3.isValid(function(valid) { + valid.should.be.false; + next(); + }); + } + ], function(err) { + if (err && err.name == 'ValidationError') { + console.error('ValidationError:', err.details.messages); + } + done(err); + }); + }); }); describe('format', function () {