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'] });
This commit is contained in:
Miroslav Bajtoš 2014-05-14 20:38:40 +02:00
parent 1db35cc926
commit 2a74bdc4de
2 changed files with 55 additions and 0 deletions

View File

@ -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.<String>} 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();

View File

@ -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 () {