Merge branch 'release/1.5.0' into production

This commit is contained in:
Miroslav Bajtoš 2014-05-15 09:36:20 +02:00
commit 800202516c
4 changed files with 192 additions and 52 deletions

View File

@ -138,7 +138,14 @@ function GeoPoint(data) {
assert(data.lat <= 90, 'lat must be <= 90'); assert(data.lat <= 90, 'lat must be <= 90');
assert(data.lat >= -90, 'lat must be >= -90'); assert(data.lat >= -90, 'lat must be >= -90');
/**
* @property {Number} lat The latitude point in degrees. Range: -90 to 90.
*/
this.lat = data.lat; this.lat = data.lat;
/**
* @property {Number} lng The longitude point in degrees. Range: -90 to 90.
*/
this.lng = data.lng; this.lng = data.lng;
} }

View File

@ -11,10 +11,10 @@ exports.Validatable = Validatable;
* This class provides methods that add validation cababilities to models. * This class provides methods that add validation cababilities to models.
* Each of this validations run when `obj.isValid()` method called. * Each of this validations run when `obj.isValid()` method called.
* *
* Each configurator can accept n params (n-1 field names and one config). Config * Each configurator can accept *n* params (*n*-1 field names and one config). Config
* is {Object} depends on specific validation, but all of them has one common part: * is {Object} depends on specific validation, but all of them have a
* `message` member. It can be just string, when only one situation possible, * `message` member property. It can be just string, when only one situation possible,
* e.g. `Post.validatesPresenceOf('title', { message: 'can not be blank' });` * For example: `Post.validatesPresenceOf('title', { message: 'can not be blank' });`
* *
* In more complicated cases it can be {Hash} of messages (for each case): * In more complicated cases it can be {Hash} of messages (for each case):
* `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});` * `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});`
@ -24,24 +24,31 @@ function Validatable() {
} }
/** /**
* Validate presence. This validation fails when validated field is blank. * Validate presence of one or more specified properties.
* * Requires a model to include a property to be considered valid; fails when validated field is blank.
* Default error message "can't be blank"
* *
* For example, validate presence of title * For example, validate presence of title
* ``` * ```
* Post.validatesPresenceOf('title'); * Post.validatesPresenceOf('title');
* ``` * ```
*Example with custom message * Validate that model has first, last, and age properties:
* ```
* User.validatesPresenceOf('first', 'last', 'age');
* ```
* Example with custom message
* ``` * ```
* Post.validatesPresenceOf('title', {message: 'Cannot be blank'}); * Post.validatesPresenceOf('title', {message: 'Cannot be blank'});
* ``` * ```
* *
* @param {String} propertyName One or more property names.
* @options {Object} errMsg Optional custom error message. Default is "can't be blank"
* @property {String} message Error message to use instead of default.
*/ */
Validatable.validatesPresenceOf = getConfigurator('presence'); Validatable.validatesPresenceOf = getConfigurator('presence');
/** /**
* Validate length. Three kinds of validations: min, max, is. * Validate length. Require a property length to be within a specified range.
* Three kinds of validations: min, max, is.
* *
* Default error messages: * Default error messages:
* *
@ -61,11 +68,17 @@ Validatable.validatesPresenceOf = getConfigurator('presence');
* User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}}); * User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}});
* User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}}); * User.validatesLengthOf('state', {is: 2, message: {is: 'is not valid state name'}});
* ``` * ```
* @param {String} propertyName Property name to validate.
* @options {Object} Options
* @property {Number} is Value that property must equal to validate.
* @property {Number} min Value that property must be less than to be valid.
* @property {Number} max Value that property must be less than to be valid.
* @property {Object} message Optional Object with string properties for custom error message for each validation: is, min, or max
*/ */
Validatable.validatesLengthOf = getConfigurator('length'); Validatable.validatesLengthOf = getConfigurator('length');
/** /**
* Validate numericality. * Validate numericality. Requires a value for property to be either an integer or number.
* *
* Example * Example
* ``` * ```
@ -73,19 +86,17 @@ Validatable.validatesLengthOf = getConfigurator('length');
* User.validatesNumericalityOf('age', {int: true, message: { int: '...' }}); * User.validatesNumericalityOf('age', {int: true, message: { int: '...' }});
* ``` * ```
* *
* Default error messages: * @param {String} propertyName Property name to validate.
* * @options {Object} Options
* @property {Boolean} int If true, then property must be an integer to be valid.
* @property {Object} message Optional object with string properties for 'int' for integer validation. Default error messages:
* - number: is not a number * - number: is not a number
* - int: is not an integer * - int: is not an integer
*
* @sync
* @nocode
* @see helper/validateNumericality
*/ */
Validatable.validatesNumericalityOf = getConfigurator('numericality'); Validatable.validatesNumericalityOf = getConfigurator('numericality');
/** /**
* Validate inclusion in set * Validate inclusion in set. Require a value for property to be in the specified array.
* *
* Example: * Example:
* ``` * ```
@ -95,33 +106,35 @@ Validatable.validatesNumericalityOf = getConfigurator('numericality');
* }); * });
* ``` * ```
* *
* Default error message: is not included in the list * @param {String} propertyName Property name to validate.
* * @options {Object} Options
* @sync * @property {Array} in Array Property must match one of the values in the array to be valid.
* @nocode * @property {String} message Optional error message if property is not valid. Default error message: "is not included in the list".
* @see helper/validateInclusion
*/ */
Validatable.validatesInclusionOf = getConfigurator('inclusion'); Validatable.validatesInclusionOf = getConfigurator('inclusion');
/** /**
* Validate exclusion * Validate exclusion. Require a property value not be in the specified array.
* *
* Example: `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});` * Example: `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});`
* *
* Default error message: is reserved * @param {String} propertyName Property name to validate.
* * @options {Object} Options
* @nocode * @property {Array} in Array Property must match one of the values in the array to be valid.
* @see helper/validateExclusion * @property {String} message Optional error message if property is not valid. Default error message: "is reserved".
*/ */
Validatable.validatesExclusionOf = getConfigurator('exclusion'); Validatable.validatesExclusionOf = getConfigurator('exclusion');
/** /**
* Validate format * Validate format. Require a model to include a property that matches the given format.
* *
* Default error message: is invalid * Require a model to include a property that matches the given format. Example:
* `User.validatesFormat('name', {with: /\w+/});`
* *
* @nocode * @param {String} propertyName Property name to validate.
* @see helper/validateFormat * @options {Object} Options
* @property {RegExp} with Regular expression to validate format.
* @property {String} message Optional error message if property is not valid. Default error message: " is invalid".
*/ */
Validatable.validatesFormatOf = getConfigurator('format'); Validatable.validatesFormatOf = getConfigurator('format');
@ -178,19 +191,32 @@ Validatable.validate = getConfigurator('custom');
Validatable.validateAsync = getConfigurator('custom', {async: true}); Validatable.validateAsync = getConfigurator('custom', {async: true});
/** /**
* Validate uniqueness * Validate uniqueness. Ensure the value for property is unique in the collection of models.
* Not available for all connectors. Currently supported with these connectors:
* - In Memory
* - Oracle
* - MongoDB
* *
* Default error message: is not unique * ```
* // The login must be unique across all User instances.
* User.validatesUniquenessOf('login');
* *
* @async * // Assuming SiteUser.belongsTo(Site)
* @nocode * // The login must be unique within each Site.
* @see helper/validateUniqueness * 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}); Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true});
// implementation of validators // implementation of validators
/** /*!
* Presence validator * Presence validator
*/ */
function validatePresence(attr, conf, err) { function validatePresence(attr, conf, err) {
@ -199,7 +225,7 @@ function validatePresence(attr, conf, err) {
} }
} }
/** /*!
* Length validator * Length validator
*/ */
function validateLength(attr, conf, err) { function validateLength(attr, conf, err) {
@ -217,7 +243,7 @@ function validateLength(attr, conf, err) {
} }
} }
/** /*!
* Numericality validator * Numericality validator
*/ */
function validateNumericality(attr, conf, err) { function validateNumericality(attr, conf, err) {
@ -231,7 +257,7 @@ function validateNumericality(attr, conf, err) {
} }
} }
/** /*!
* Inclusion validator * Inclusion validator
*/ */
function validateInclusion(attr, conf, err) { function validateInclusion(attr, conf, err) {
@ -242,7 +268,7 @@ function validateInclusion(attr, conf, err) {
} }
} }
/** /*!
* Exclusion validator * Exclusion validator
*/ */
function validateExclusion(attr, conf, err) { function validateExclusion(attr, conf, err) {
@ -253,7 +279,7 @@ function validateExclusion(attr, conf, err) {
} }
} }
/** /*!
* Format validator * Format validator
*/ */
function validateFormat(attr, conf, err) { function validateFormat(attr, conf, err) {
@ -268,19 +294,28 @@ function validateFormat(attr, conf, err) {
} }
} }
/** /*!
* Custom validator * Custom validator
*/ */
function validateCustom(attr, conf, err, done) { function validateCustom(attr, conf, err, done) {
conf.customValidator.call(this, err, done); conf.customValidator.call(this, err, done);
} }
/** /*!
* Uniqueness validator * Uniqueness validator
*/ */
function validateUniqueness(attr, conf, err, done) { function validateUniqueness(attr, conf, err, done) {
var cond = {where: {}}; var cond = {where: {}};
cond.where[attr] = this[attr]; 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) { this.constructor.find(cond, function (error, found) {
if (error) { if (error) {
return err(); return err();
@ -312,16 +347,14 @@ function getConfigurator(name, opts) {
} }
/** /**
* This method performs validation, triggers validation hooks. * This method performs validation and triggers validation hooks.
* Before validation `obj.errors` collection cleaned. * Before validation the `obj.errors` collection is cleaned.
* Each validation can add errors to `obj.errors` collection. * Each validation can add errors to `obj.errors` collection.
* If collection is not blank, validation failed. * If collection is not blank, validation failed.
* *
* @warning This method can be called as sync only when no async validation * NOTE: This method can be called as synchronous only when no asynchronous validation is
* configured. It's strongly recommended to run all validations as asyncronous. * configured. It's strongly recommended to run all validations as asyncronous.
* *
* Returns true if no async validation configured and all passed
*
* Example: ExpressJS controller: render user if valid, show flash otherwise * Example: ExpressJS controller: render user if valid, show flash otherwise
* ``` * ```
* user.isValid(function (valid) { * user.isValid(function (valid) {
@ -329,7 +362,21 @@ function getConfigurator(name, opts) {
* else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users'); * else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users');
* }); * });
* ``` * ```
* Another example:
* ```
* user.isValid(function (valid) {
* if (!valid) {
* console.log(user.errors);
* // => hash of errors
* // => {
* // => username: [errmessage, errmessage, ...],
* // => email: ...
* // => }
* }
* });
* ```
* @param {Function} callback called with (valid) * @param {Function} callback called with (valid)
* @returns {Boolean} True if no asynchronouse validation is configured and all properties pass validation.
*/ */
Validatable.prototype.isValid = function (callback, data) { Validatable.prototype.isValid = function (callback, data) {
var valid = true, inst = this, wait = 0, async = false; var valid = true, inst = this, wait = 0, async = false;
@ -521,7 +568,7 @@ function nullCheck(attr, conf, err) {
return false; return false;
} }
/** /*!
* Return true when v is undefined, blank array, null or empty string * Return true when v is undefined, blank array, null or empty string
* otherwise returns false * otherwise returns false
* *
@ -586,6 +633,56 @@ function ErrorCodes(messages) {
}); });
} }
/**
* ValidationError is raised when the application attempts to save an invalid model instance.
* Example:
* ```
* {
* "name": "ValidationError",
* "status": 422,
* "message": "The Model instance is not valid. \
* See `details` property of the error object for more info.",
* "statusCode": 422,
* "details": {
* "context": "user",
* "codes": {
* "password": [
* "presence"
* ],
* "email": [
* "uniqueness"
* ]
* },
* "messages": {
* "password": [
* "can't be blank"
* ],
* "email": [
* "Email already exists"
* ]
* }
* },
* }
* ```
* You might run into situations where you need to raise a validation error yourself, for example in a "before" hook or a
* custom model method.
* ```
* MyModel.prototype.preflight = function(changes, callback) {
* // Update properties, do not save to db
* for (var key in changes) {
* model[key] = changes[key];
* }
*
* if (model.isValid()) {
* return callback(null, { success: true });
* }
*
* // This line shows how to create a ValidationError
* err = new ValidationError(model);
* callback(err);
* }
* ```
*/
function ValidationError(obj) { function ValidationError(obj) {
if (!(this instanceof ValidationError)) return new ValidationError(obj); if (!(this instanceof ValidationError)) return new ValidationError(obj);

View File

@ -1,6 +1,6 @@
{ {
"name": "loopback-datasource-juggler", "name": "loopback-datasource-juggler",
"version": "1.4.0", "version": "1.5.0",
"description": "LoopBack DataSoure Juggler", "description": "LoopBack DataSoure Juggler",
"keywords": [ "keywords": [
"StrongLoop", "StrongLoop",

View File

@ -1,5 +1,6 @@
// This test written in mocha+should.js // This test written in mocha+should.js
var should = require('./init.js'); var should = require('./init.js');
var async = require('async');
var j = require('../'), db, User; var j = require('../'), db, User;
var ValidationError = j.ValidationError; var ValidationError = j.ValidationError;
@ -172,6 +173,41 @@ describe('validations', function () {
})).should.not.be.ok; })).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 () { describe('format', function () {