diff --git a/lib/geo.js b/lib/geo.js index 30d6931d..1dac1abd 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -138,7 +138,14 @@ function GeoPoint(data) { 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; + +/** + * @property {Number} lng The longitude point in degrees. Range: -90 to 90. +*/ this.lng = data.lng; } diff --git a/lib/validations.js b/lib/validations.js index ab2c85ba..200bf6ba 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -11,10 +11,10 @@ exports.Validatable = Validatable; * This class provides methods that add validation cababilities to models. * Each of this validations run when `obj.isValid()` method called. * - * 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: - * `message` member. It can be just string, when only one situation possible, - * e.g. `Post.validatesPresenceOf('title', { message: 'can not be blank' });` + * Each configurator can accept *n* params (*n*-1 field names and one config). Config + * is {Object} depends on specific validation, but all of them have a + * `message` member property. It can be just string, when only one situation possible, + * For example: `Post.validatesPresenceOf('title', { message: 'can not be blank' });` * * 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'}});` @@ -24,24 +24,31 @@ function Validatable() { } /** - * Validate presence. This validation fails when validated field is blank. - * - * Default error message "can't be 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. * * For example, validate presence of 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'}); * ``` * + * @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'); /** - * 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: * @@ -61,11 +68,17 @@ Validatable.validatesPresenceOf = getConfigurator('presence'); * User.validatesLengthOf('password', {min: 7, message: {min: 'too weak'}}); * 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'); /** - * Validate numericality. + * Validate numericality. Requires a value for property to be either an integer or number. * * Example * ``` @@ -73,19 +86,17 @@ Validatable.validatesLengthOf = getConfigurator('length'); * 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 * - int: is not an integer - * - * @sync - * @nocode - * @see helper/validateNumericality */ Validatable.validatesNumericalityOf = getConfigurator('numericality'); /** - * Validate inclusion in set + * Validate inclusion in set. Require a value for property to be in the specified array. * * Example: * ``` @@ -95,33 +106,35 @@ Validatable.validatesNumericalityOf = getConfigurator('numericality'); * }); * ``` * - * Default error message: is not included in the list - * - * @sync - * @nocode - * @see helper/validateInclusion + * @param {String} propertyName Property name to validate. + * @options {Object} Options + * @property {Array} in Array Property must match one of the values in the array to be valid. + * @property {String} message Optional error message if property is not valid. Default error message: "is not included in the list". */ 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']});` * - * Default error message: is reserved - * - * @nocode - * @see helper/validateExclusion + * @param {String} propertyName Property name to validate. + * @options {Object} Options + * @property {Array} in Array Property must match one of the values in the array to be valid. + * @property {String} message Optional error message if property is not valid. Default error message: "is reserved". */ 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 - * - * @nocode - * @see helper/validateFormat + * Require a model to include a property that matches the given format. Example: + * `User.validatesFormat('name', {with: /\w+/});` + * + * @param {String} propertyName Property name to validate. + * @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'); @@ -178,19 +191,32 @@ Validatable.validate = getConfigurator('custom'); 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 - * @nocode - * @see helper/validateUniqueness + * // 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}); // implementation of validators -/** +/*! * Presence validator */ function validatePresence(attr, conf, err) { @@ -199,7 +225,7 @@ function validatePresence(attr, conf, err) { } } -/** +/*! * Length validator */ function validateLength(attr, conf, err) { @@ -217,7 +243,7 @@ function validateLength(attr, conf, err) { } } -/** +/*! * Numericality validator */ function validateNumericality(attr, conf, err) { @@ -231,7 +257,7 @@ function validateNumericality(attr, conf, err) { } } -/** +/*! * Inclusion validator */ function validateInclusion(attr, conf, err) { @@ -242,7 +268,7 @@ function validateInclusion(attr, conf, err) { } } -/** +/*! * Exclusion validator */ function validateExclusion(attr, conf, err) { @@ -253,7 +279,7 @@ function validateExclusion(attr, conf, err) { } } -/** +/*! * Format validator */ function validateFormat(attr, conf, err) { @@ -268,19 +294,28 @@ function validateFormat(attr, conf, err) { } } -/** +/*! * Custom validator */ function validateCustom(attr, conf, err, done) { conf.customValidator.call(this, err, done); } -/** +/*! * Uniqueness validator */ 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(); @@ -312,16 +347,14 @@ function getConfigurator(name, opts) { } /** - * This method performs validation, triggers validation hooks. - * Before validation `obj.errors` collection cleaned. + * This method performs validation and triggers validation hooks. + * Before validation the `obj.errors` collection is cleaned. * Each validation can add errors to `obj.errors` collection. * 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. * - * Returns true if no async validation configured and all passed - * * Example: ExpressJS controller: render user if valid, show flash otherwise * ``` * 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'); * }); * ``` + * 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) + * @returns {Boolean} True if no asynchronouse validation is configured and all properties pass validation. */ Validatable.prototype.isValid = function (callback, data) { var valid = true, inst = this, wait = 0, async = false; @@ -521,7 +568,7 @@ function nullCheck(attr, conf, err) { return false; } -/** +/*! * Return true when v is undefined, blank array, null or empty string * 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) { if (!(this instanceof ValidationError)) return new ValidationError(obj); diff --git a/package.json b/package.json index b03fcd36..a569f21f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "1.4.0", + "version": "1.5.0", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", 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 () {