diff --git a/README.md b/README.md index afe958d..7634c63 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,20 @@ The built in Mongoose message template variables still work as expected. You can ### option.type - optional Set the type of validator type. If this is not defined, Mongoose will set this for you. Read more about this here: [http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate](http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate) +### Extending the error properties (mongoose version >= 3.9.7) + +Any additional members added to the options object will be available in the 'err.properties' field of the mongoose validation error. + +```javascript +var alphaValidator = validate({ + validator: 'isAlphanumeric', + passIfEmpty: true, + message: 'Name should contain alpha-numeric characters only', + httpStatus: 400 + }); +``` +In this example the error object returned by mongoose will have its 'properties' extended with httpStatus should validation fail. More details can be found about this here: [http://thecodebarbarian.com/2014/12/19/mongoose-397](http://thecodebarbarian.com/2014/12/19/mongoose-397) + ## Regular Expressions Mongoose Validator can use the validator.js `matches` method, however, it's worth noting that the regex can be passed in 2 ways - as per the validator.js documentation, firstly they can be passed as a literal: @@ -161,4 +175,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/mongoose-validator.js b/lib/mongoose-validator.js index 50a1cbf..ae048b2 100644 --- a/lib/mongoose-validator.js +++ b/lib/mongoose-validator.js @@ -7,6 +7,7 @@ var validatorjs = require('validator'); var defaultErrorMessages = require('./default-errors'); +var _ = require('underscore'); var errorMessages = {}; @@ -31,8 +32,8 @@ function validate (options) { var args = options.arguments || []; var passIfEmpty = options.passIfEmpty !== undefined ? options.passIfEmpty : false; var message = options.message || errorMessages[name] || defaultErrorMessages[name] || 'Error'; - var type = options.type; var validator = (name instanceof Function) ? name : validatorjs[name]; + var extend = _.omit(options, 'passIfEmpty', 'message', 'validator', 'arguments'); // Coerse args into an array args = !Array.isArray(args) ? [args] : args; @@ -44,7 +45,7 @@ function validate (options) { }); if (validator) { - return { + return _.extend({ validator: function(val, next) { var validatorArgs = [val].concat(args); @@ -54,9 +55,8 @@ function validate (options) { return next(validator.apply(null, validatorArgs)); }, - message: message, - type: type - }; + message: message + }, extend); } throw new Error('Validator `' + name + '` does not exist on validator.js'); diff --git a/package.json b/package.json index bad8327..e313c38 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose-validator", "description": "Validators for mongoose models utilising validator.js", - "version": "1.1.1", + "version": "1.2.0", "author": { "name": "Lee Powell", "email": "lee@leepowell.co.uk" @@ -30,15 +30,20 @@ { "name": "Eric Saboia", "url": "https://github.com/ericsaboia" + }, + { + "name": "Rob Rodriguez", + "url": "https://github.com/rodriguise" } ], "dependencies": { + "underscore": "^1.8.3", "validator": "^3.18.0" }, "devDependencies": { "mongoose": "^4.0.5", "mocha": "^2.2.5", - "should": "^3.3.1" + "should": "^6.0.3" }, "keywords": [ "mongoose", diff --git a/test/test.js b/test/test.js index 7c5a67e..d6ace23 100644 --- a/test/test.js +++ b/test/test.js @@ -31,7 +31,7 @@ extend('isArray', function(val) { // Tests // ------------------------------------------------------------ -describe('Mongoose Validator', function() { +describe('Mongoose Validator:', function() { var doc, schema, Person; before(function(done) { @@ -79,381 +79,354 @@ describe('Mongoose Validator', function() { }); }); - it('Should pass a validator', function(done) { - schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10] })); + describe('General Validation -', function() { + it('Should pass a validator', function(done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10] })); - should.exist(doc); + should.exist(doc); - doc.name = "Jonathan"; + doc.name = "Jonathan"; - doc.save(function(err, person) { - should.not.exist(err); - should.exist(person); - person.should.have.property('name', 'Jonathan'); - return done(); - }); - }); - - it('Should fail a validator', function(done) { - schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10] })); - - should.exist(doc); - - doc.name = "Joe"; - - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - return done(); + doc.save(function(err, person) { + should.not.exist(err); + should.exist(person); + person.should.have.property('name', 'Jonathan'); + return done(); + }); }); - }); - it('Should pass a passIfEmpty validator', function(done) { - schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], passIfEmpty: true })); + it('Should fail a validator', function(done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10] })); - should.exist(doc); + should.exist(doc); - doc.name = undefined; + doc.name = "Joe"; - doc.save(function(err, person) { - should.not.exist(err); - should.exist(person); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + return done(); + }); }); }); - it('Should fail a passIfEmpty validator', function(done) { - schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], passIfEmpty: true })); + describe('passIfEmpty Validation -', function() { + it('Should pass a passIfEmpty validator', function(done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], passIfEmpty: true })); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = undefined; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - return done(); + doc.save(function(err, person) { + should.not.exist(err); + should.exist(person); + return done(); + }); }); - }); - it('Should use custom error message', function(done) { - schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], message: 'Custom error message' })); + it('Should fail a passIfEmpty validator', function(done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], passIfEmpty: true })); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('Custom error message'); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + return done(); + }); }); }); - it('Should replace args on custom error message', function (done) { - schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], message: 'At least {ARGS[0]} and less than {ARGS[1]}' })); + describe('Custom Error Messages -', function() { + it('Should use custom error message', function(done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], message: 'Custom error message' })); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('At least 5 and less than 10'); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + err.errors.name.message.should.equal('Custom error message'); + return done(); + }); }); - }); - it('Should use a custom extend test and pass', function(done) { - schema.path('name').validate(validate({ validator: 'isType', arguments: 'string'})); + it('Should replace custom properties on error message', function (done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], http: 403, message: 'Error {HTTP}: Something bad happened' })); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.not.exist(err); - should.exist(person); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + err.errors.name.message.should.equal('Error 403: Something bad happened'); + return done(); + }); }); - }); - it('Should use a custom extend test and fail', function(done) { - schema.path('name').validate(validate({ validator: 'isType', arguments: 'boolean' })); + it('Should replace args on custom error message', function (done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], message: 'At least {ARGS[0]} and less than {ARGS[1]}' })); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('Not correct type'); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + err.errors.name.message.should.equal('At least 5 and less than 10'); + return done(); + }); }); }); - it('Should use a custom extend test and fail with custom error message', function(done) { - schema.path('name').validate(validate({ validator: 'isType', arguments: 'boolean', message: 'Custom error message' })); + describe('Properties -', function() { + it('Should support the `type` property', function (done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], type: 'custom error type'})); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('Custom error message'); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('kind', 'custom error type'); + err.errors.name.should.have.property('properties'); + err.errors.name.should.have.propertyByPath('properties', 'type').eql('custom error type'); + return done(); + }); }); - }); - it('Should use a custom prototype test and pass', function(done) { - schema.path('name').validate(validate({ validator: 'contains', arguments: 'J' })); + // Support a custom property being added to the error object, only documentation I can find is from: + // http://thecodebarbarian.com/2014/12/19/mongoose-397 + // In this case, `http` is a custom property and is passed across to the resulting error object + it('Should support a custom property', function (done) { + schema.path('name').validate(validate({ validator: 'isLength', arguments: [5, 10], http: 403})); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.not.exist(err); - should.exist(person); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('properties'); + err.errors.name.should.have.propertyByPath('properties', 'http').eql(403); + return done(); + }); }); }); - it('Should use a custom prototype test and fail', function(done) { - schema.path('name').validate(validate({ validator: 'contains', arguments: 'K' })); + describe('Custom validator using `extend` method -', function() { + it('Should use a custom extended validator and pass', function(done) { + schema.path('name').validate(validate({ validator: 'isType', arguments: 'string'})); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - return done(); + doc.save(function(err, person) { + should.not.exist(err); + should.exist(person); + return done(); + }); }); - }); - it('Should use a custom prototype test and fail with custom error message', function(done) { - schema.path('name').validate(validate({ validator: 'contains', arguments: 'K', message: 'Custom error message' })); + it('Should use a custom extended validator and fail', function(done) { + schema.path('name').validate(validate({ validator: 'isType', arguments: 'boolean' })); - should.exist(doc); + should.exist(doc); - doc.name = 'Joe'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('Custom error message'); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + err.errors.name.message.should.equal('Not correct type'); + return done(); + }); }); - }); - // https://github.com/leepowellcouk/mongoose-validator/issues/7#issuecomment-20494299 - it('Should pass on legacy isEmail method', function(done) { - schema.path('name').validate(validate({ validator: 'isEmail' })); + it('Should use a custom extended validator and fail with custom error message', function(done) { + schema.path('name').validate(validate({ validator: 'isType', arguments: 'boolean', message: 'Custom error message' })); - should.exist(doc); + should.exist(doc); - doc.name = 'username+suffix@googlemail.com'; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.not.exist(err); - should.exist(person); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + err.errors.name.message.should.equal('Custom error message'); + return done(); + }); }); }); - it('Should pass custom validator using non-string value', function(done) { - schema.path('interests').validate(validate({ validator: 'isArray' })); + describe('Custom validator on validator.js prototype -', function() { + it('Should use a custom prototype test and pass', function(done) { + schema.path('name').validate(validate({ validator: 'contains', arguments: 'J' })); - should.exist(doc); + should.exist(doc); - doc.interests = ['cycling', 'fishing']; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.not.exist(err); - should.exist(person); - person.should.have.property('interests').and.match(['cycling', 'fishing']); - return done(); + doc.save(function(err, person) { + should.not.exist(err); + should.exist(person); + return done(); + }); }); - }); - - it('Should pass custom validator when a custom function is passed directly', function(done) { - schema.path('age').validate(validate({ - validator: function(val) { - return val > 18; - }, - message: 'Age must be greater than 18' - })); + it('Should use a custom prototype test and fail', function(done) { + schema.path('name').validate(validate({ validator: 'contains', arguments: 'K' })); - should.exist(doc); + should.exist(doc); - doc.age = 20; + doc.name = 'Joe'; - doc.save(function(err, person) { - should.not.exist(err); - should.exist(person); - person.should.have.property('age').and.match(20); - return done(); + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.name.should.have.property('path', 'name'); + return done(); + }); }); - }); - - it('Should fail custom validator when a custom function is passed directly', function(done) { - schema.path('age').validate(validate({ - validator: function(val) { - return val > 18; - }, - message: 'Age must be greater than 18' - })); + it('Should use a custom prototype test and fail with custom error message', function(done) { + schema.path('name').validate(validate({ validator: 'contains', arguments: 'K', message: 'Custom error message' })); - should.exist(doc); + should.exist(doc); - doc.age = 10; + doc.name = 'Joe'; - doc.save(function(err, person) { + doc.save(function(err, person) { should.exist(err); should.not.exist(person); err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); - err.errors.age.should.have.property('path', 'age'); - err.errors.age.message.should.equal('Age must be greater than 18'); + err.errors.name.should.have.property('path', 'name'); + err.errors.name.message.should.equal('Custom error message'); return done(); + }); }); }); - it('Issue #12', function(done) { - schema.path('name') - .validate(validate({ validator: 'notEmpty', message: 'Username should not be empty' })) - .validate(validate({ validator: 'isLength', arguments: [4, 40], message: 'Username should be between 4 and 40 characters' })) - .validate(validate({ validator: 'isAlphanumeric', message: 'Username must only contain letters and digits' })); + describe('Passing functions directly -', function () { + it('Should pass custom validator when a custom function is passed directly', function(done) { + schema.path('age').validate(validate({ + validator: function(val) { + return val > 18; + }, + message: 'Age must be greater than 18' + })); - should.exist(doc); + should.exist(doc); - doc.name = ''; + doc.age = 20; - doc.save(function(err, person) { - should.exist(err); - should.not.exist(person); - err.errors.name.message.should.equal('Username should not be empty'); - return done(); + doc.save(function(err, person) { + should.not.exist(err); + should.exist(person); + person.should.have.property('age').and.match(20); + return done(); + }); }); - }); -}); -describe('Mongoose Validator - Validators in schema declaration', function() { - var doc, schema, User; - - before(function(done) { - var url = 'mongodb://localhost/mongoose_validator_test', - date = Date.now(), - many; - - many = [ - validate({ validator: 'notEmpty', message: 'Username should not be empty' }), - validate({ validator: 'isLength', passIfEmpty: true, arguments: [4, 40], message: 'Username should be between 4 and 40 characters' }), - validate({ validator: 'isAlphanumeric', passIfEmpty: true, message: 'Username must only contain letters and digits' }) - ]; - - mongoose.connect(url); - - schema = new Schema({ - name: { type: String, default: null, validate: many }, - date_created: { type: Date, default: date } + it('Should fail custom validator when a custom function is passed directly', function(done) { + schema.path('age').validate(validate({ + validator: function(val) { + return val > 18; + }, + message: 'Age must be greater than 18' + })); + + should.exist(doc); + + doc.age = 10; + + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.should.be.instanceof(Error).and.have.property('name', 'ValidationError'); + err.errors.age.should.have.property('path', 'age'); + err.errors.age.message.should.equal('Age must be greater than 18'); + return done(); + }); }); - - User = mongoose.model('User', schema, 'test.user'); - - done(); }); - after(function(done) { - mongoose.connection.db.dropDatabase(); - mongoose.disconnect(); - done(); - }); + describe('Miscellaneous -', function() { + // https://github.com/leepowellcouk/mongoose-validator/issues/7#issuecomment-20494299 + it('Should pass on legacy isEmail method', function(done) { + schema.path('name').validate(validate({ validator: 'isEmail' })); - beforeEach(function(done) { - done(); - }); + should.exist(doc); - afterEach(function(done) { - done(); - }); - - it('Should fail on notEmpty', function(done) { - var vals = { - name: '' - }; + doc.name = 'username+suffix@googlemail.com'; - User.create(vals, function(err, user) { - should.exist(err); - should.not.exist(user); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('Username should not be empty'); - return done(); + doc.save(function(err, person) { + should.not.exist(err); + should.exist(person); + return done(); + }); }); - }); - it('Should fail on length', function(done) { - var vals = { - name: 'Joh' - }; + it('Should pass custom validator using non-string value', function(done) { + schema.path('interests').validate(validate({ validator: 'isArray' })); - User.create(vals, function(err, user) { - should.exist(err); - should.not.exist(user); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('Username should be between 4 and 40 characters'); - return done(); - }); - }); + should.exist(doc); - it('Should fail on isAlphanumeric', function(done) { - var vals = { - name: 'Joh&' - }; + doc.interests = ['cycling', 'fishing']; - User.create(vals, function(err, user) { - should.exist(err); - should.not.exist(user); - err.errors.name.should.have.property('path', 'name'); - err.errors.name.message.should.equal('Username must only contain letters and digits'); - return done(); + doc.save(function(err, person) { + should.not.exist(err); + should.exist(person); + person.should.have.property('interests').and.match(['cycling', 'fishing']); + return done(); + }); }); - }); - it('Should pass', function(done) { - var vals = { - name: 'John' - }; + it('Issue #12', function(done) { + schema.path('name') + .validate(validate({ validator: 'notEmpty', message: 'Username should not be empty' })) + .validate(validate({ validator: 'isLength', arguments: [4, 40], message: 'Username should be between 4 and 40 characters' })) + .validate(validate({ validator: 'isAlphanumeric', message: 'Username must only contain letters and digits' })); - User.create(vals, function(err, user) { - should.not.exist(err); - should.exist(user); - user.name.should.equal('John'); - return done(); + should.exist(doc); + + doc.name = ''; + + doc.save(function(err, person) { + should.exist(err); + should.not.exist(person); + err.errors.name.message.should.equal('Username should not be empty'); + return done(); + }); }); }); });