From 2a74bdc4dea8d61ae7e03677b8eb3d2f6510a2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 14 May 2014 20:38:40 +0200 Subject: [PATCH] validations: support multi-key unique constraint Modify the "unique" validator to accept additional property names to narrow the space of rows searched for duplicates. Example: Consider `SiteUser` belongsTo `Site` via `siteId` foreign key. Inside every site, the user email must be unique. It is allowed to register the same email with multiple sites. SiteUser.validateUniquenessOf('email', { scopedTo: ['siteId'] }); --- lib/validations.js | 19 +++++++++++++++++++ test/validations.test.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) 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 () {