From 9274d230ecb89029ffa4de7254308d0df86d4756 Mon Sep 17 00:00:00 2001 From: George Eracleous Date: Mon, 30 Nov 2015 17:50:16 +0200 Subject: [PATCH 1/2] feat(custom-validation) Adds code to apply custom validations on any property --- README.md | 26 +++++++++-- lib/errors/ValidationError.js | 13 ------ lib/errors/index.js | 3 +- lib/schema.js | 29 ++++++++++--- package.json | 2 +- test/schema.spec.js | 81 ++++++++++++++++++++++++++++++++++- 6 files changed, 129 insertions(+), 25 deletions(-) delete mode 100644 lib/errors/ValidationError.js diff --git a/README.md b/README.md index cda16d1..b4305e2 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ store.get('users', { ## Schemas -**Define a primary key** +### Define a primary key Any field can be designated as the primary key. Only one field can be designated as the primary key. @@ -145,7 +145,7 @@ var schema = { }; ``` -**Nested schemas a.k.a object types** +### Nested schemas a.k.a object types ```javascript // Defines an 'address'' property with nested schema @@ -160,7 +160,7 @@ var schema = { }; ``` -**Define schemas with Array types** +### Define schemas with Array types ```javascript // Defines a 'friends' property with Array type @@ -173,11 +173,29 @@ var schema = { }; ``` +### Custom property validations + +You can define a ```validate(value)``` function on each property. The ```value``` argument passed can be used to check the validity of the value and return either a truthy or falsy value. If a falsy value is returned then a ```CustomValidationError``` is thrown. + +```javascript +// Defines a 'age' property with custom validation +var schema = { + age: { + type: 'Number', + validate: function(value) { + return value > 0; + } + } +}; +``` + ## Errors You can import the errors using ``` require('stormer').errors. ``` -- ```ValidationError```: This error indicates that an operation failed because object didn't conform with the model's schema +- ```TypeValidationError```: This error indicates that an operation failed because a schema property didn't conform with the designated type + +- ```CustomValidationError```: This error indicates that an operation failed because a schema property failed a custom validation - ```NotFoundError```: This error indicates that the object was not found in the store diff --git a/lib/errors/ValidationError.js b/lib/errors/ValidationError.js deleted file mode 100644 index 1eca35e..0000000 --- a/lib/errors/ValidationError.js +++ /dev/null @@ -1,13 +0,0 @@ -var util = require('util'); - -var ValidationError = function (message, property, correctType) { - Error.captureStackTrace(this, this); - this.message = message; - this.property = property; - this.correctType = correctType; -}; - -util.inherits(ValidationError, Error); -ValidationError.prototype.name = 'ValidationError'; - -module.exports = ValidationError; \ No newline at end of file diff --git a/lib/errors/index.js b/lib/errors/index.js index 14e88ee..5eaf424 100644 --- a/lib/errors/index.js +++ b/lib/errors/index.js @@ -1,5 +1,6 @@ module.exports = { NotFoundError: require('./NotFoundError'), - ValidationError: require('./ValidationError'), + TypeValidationError: require('./TypeValidationError'), + CustomValidationError: require('./CustomValidationError'), AlreadyExistsError: require('./AlreadyExistsError') }; \ No newline at end of file diff --git a/lib/schema.js b/lib/schema.js index fd12696..7242fc4 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1,6 +1,7 @@ var Promise = require('bluebird'); var util = require('util'); -var ValidationError = require('./errors/ValidationError'); +var TypeValidationError = require('./errors/TypeValidationError'); +var CustomValidationError = require('./errors/CustomValidationError'); var _ = require('./utils'); @@ -43,7 +44,14 @@ var normalizeSchema = function(schema, prefix) { } else { schema[key] = normalizePropertySchema(key, value); } + + // Attach key path schema[key].path = util.format('%s.%s', prefix, key); + + // Check validator + if (schema[key].hasOwnProperty('validate') && typeof schema[key].validate !== 'function') { + throw new Error(util.format('Validator for property %s should be function', schema[key].path)); + } }); return schema; }; @@ -77,7 +85,7 @@ Schema.prototype.create = function(obj, schema){ var propertySchema = schema[key]; var value = obj.hasOwnProperty(key) ? obj[key] : propertySchema.default; if (_.isUndefined(value) && (propertySchema.required === true || propertySchema.primaryKey === true)) { - return reject(new ValidationError(util.format('Property %s is required', propertySchema.path))); + return reject(new TypeValidationError(util.format('Property %s is required', propertySchema.path))); } // Skip if this is a not defined property @@ -85,17 +93,28 @@ Schema.prototype.create = function(obj, schema){ return; }; + // Check if value has valid type if (Object.prototype.toString.call(value) !== util.format("[object %s]", propertySchema.type)) { - return reject(new ValidationError(util.format('Property %s should be of type %s', propertySchema.path, propertySchema.type), key, propertySchema.type)); + return reject(new TypeValidationError(util.format('Property %s should be of type %s', propertySchema.path, propertySchema.type), key, propertySchema.type)); + } + + // Apply custom validators (if any) + if (propertySchema.hasOwnProperty('validate')) { + if (!propertySchema.validate(value)) { + return reject(new CustomValidationError(util.format('Property %s failed custom validation', propertySchema.path), key)); + } } if (propertySchema.type === 'Array') { return Promise.map(value, function(v, i) { return propertySchema.of.create({subSchema: v}).then(function(instance) { return instance.subSchema; - }).catch(ValidationError, function(err) { + }).catch(TypeValidationError, function(err) { + var invalidPropertyName = util.format('%s[%s].%s', propertySchema.path, i, err.property); + return reject(new TypeValidationError(util.format('Property %s should be of type %s', invalidPropertyName, err.correctType))); + }).catch(CustomValidationError, function(err) { var invalidPropertyName = util.format('%s[%s].%s', propertySchema.path, i, err.property); - return reject(new ValidationError(util.format('Property %s should be of type %s', invalidPropertyName, err.correctType))); + return reject(new CustomValidationError(util.format('Property %s failed custom validation', invalidPropertyName))); }); }).then(function(values) { _.setValueAtPath(instance, key, values); diff --git a/package.json b/package.json index 10f8701..dee4092 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stormer", - "version": "0.7.0", + "version": "0.8.0", "description": "The flexible Node.js ORM", "main": "index.js", "directories": { diff --git a/test/schema.spec.js b/test/schema.spec.js index cd93102..187a734 100644 --- a/test/schema.spec.js +++ b/test/schema.spec.js @@ -1,5 +1,7 @@ var chai = require('chai'); var Schema = require('../lib/schema'); +var TypeValidationError = require('../lib/errors').TypeValidationError; +var CustomValidationError = require('../lib/errors').CustomValidationError; chai.should(); describe('Schema Tests', function() { @@ -20,6 +22,17 @@ describe('Schema Tests', function() { }).should.throw('Type InvalidType is not supported'); }); + it('throw an error if property has invalid validator', function() { + (function () { + new Schema({ + propertyWithInvalidValidator: { + type: 'Number', + validate: 'this should be a function' + } + }); + }).should.throw('Validator for property .propertyWithInvalidValidator should be function'); + }); + it('throw an error if more than two fields are designated as primary keys', function() { (function () { new Schema({ @@ -286,6 +299,56 @@ describe('Schema Tests', function() { }); }); + describe('work with Number properties and', function() { + + before(function() { + var schemaDef = { + simpleNumField: 'Number', + requiredNumField: { + type: 'Number', + required: true, + validate: function(value) { + return value <= 5; + } + }, + numFieldWithDefault: { + type: 'Number', + default: 1.01 + } + }; + this.schema = new Schema(schemaDef); + }); + + it('return an error if an number property has the wrong type', function(done) { + this.schema.create({ + requiredNumField: 'this should be a number' + }).catch(function(err) { + err.should.be.an.instanceOf(TypeValidationError); + err.message.should.equal('Property .requiredNumField should be of type Number'); + done(); + }); + }); + + it('set the defaults for properties of type number', function(done) { + this.schema.create({ + requiredNumField: 5 + }).then(function(instance) { + instance.should.have.deep.property('numFieldWithDefault', 1.01); + done(); + }).catch(done); + }); + + it('return validation error if number field failed to pass the custom validation', function(done) { + this.schema.create({ + requiredNumField: 6 + }).catch(function(err) { + err.should.be.an.instanceOf(CustomValidationError); + err.message.should.equal('Property .requiredNumField failed custom validation'); + done(); + }); + }); + + }); describe('work with Array properties and', function() { @@ -306,7 +369,10 @@ describe('Schema Tests', function() { fieldA: 'String', fieldB: { type: 'Number', - default: 100 + default: 100, + validate: function(value) { + return value < 1000 + } } } } @@ -318,6 +384,7 @@ describe('Schema Tests', function() { this.schema.create({ ofStrings: 'this should be an array' }).catch(function(err) { + err.should.be.an.instanceOf(TypeValidationError); err.message.should.equal('Property .ofStrings should be of type Array'); done(); }); @@ -327,6 +394,7 @@ describe('Schema Tests', function() { this.schema.create({ ofStrings: ['1', 2, '3'] // The array should have items of type String }).catch(function(err) { + err.should.be.an.instanceOf(TypeValidationError); err.message.should.equal('Property .ofStrings[1].subSchema should be of type String'); done(); }); @@ -336,11 +404,22 @@ describe('Schema Tests', function() { this.schema.create({ ofObjects: [{fieldA: 1234}, {fieldA: '1234'}] // The array should have items of type Object }).catch(function(err) { + err.should.be.an.instanceOf(TypeValidationError); err.message.should.equal('Property .ofObjects[0].fieldA should be of type String'); done(); }); }); + it('return an error if an array of objects has items that failed custom validation', function(done) { + this.schema.create({ + ofObjects: [{fieldB: 1234}] // The value of fieldB should be lower than 1000 + }).catch(function(err) { + err.should.be.an.instanceOf(CustomValidationError); + err.message.should.equal('Property .ofObjects[0].fieldB failed custom validation'); + done(); + }); + }); + it('create objects with properties of type array', function(done) { this.schema.create({ ofObjects: [{fieldA: '1234'}, {fieldA: '1234'}] From a01ca04cf7270c231f35a500e7b0f9f24404cc82 Mon Sep 17 00:00:00 2001 From: George Eracleous Date: Mon, 30 Nov 2015 18:02:57 +0200 Subject: [PATCH 2/2] fix(errors) Commits needed files --- lib/errors/CustomValidationError.js | 12 ++++++++++++ lib/errors/TypeValidationError.js | 13 +++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 lib/errors/CustomValidationError.js create mode 100644 lib/errors/TypeValidationError.js diff --git a/lib/errors/CustomValidationError.js b/lib/errors/CustomValidationError.js new file mode 100644 index 0000000..64e9a00 --- /dev/null +++ b/lib/errors/CustomValidationError.js @@ -0,0 +1,12 @@ +var util = require('util'); + +var CustomValidationError = function (message, property) { + Error.captureStackTrace(this, this); + this.message = message; + this.property = property; +}; + +util.inherits(CustomValidationError, Error); +CustomValidationError.prototype.name = 'CustomValidationError'; + +module.exports = CustomValidationError; \ No newline at end of file diff --git a/lib/errors/TypeValidationError.js b/lib/errors/TypeValidationError.js new file mode 100644 index 0000000..58350b9 --- /dev/null +++ b/lib/errors/TypeValidationError.js @@ -0,0 +1,13 @@ +var util = require('util'); + +var TypeValidationError = function (message, property, correctType) { + Error.captureStackTrace(this, this); + this.message = message; + this.property = property; + this.correctType = correctType; +}; + +util.inherits(TypeValidationError, Error); +TypeValidationError.prototype.name = 'TypeValidationError'; + +module.exports = TypeValidationError; \ No newline at end of file