Skip to content

Commit

Permalink
Merge pull request #16 from Avocarrot/feat/fieldValidations
Browse files Browse the repository at this point in the history
feat(custom-validation) Adds code to apply custom validations on any …
  • Loading branch information
giorgosera committed Nov 30, 2015
2 parents d73ef9e + a01ca04 commit 98c02e2
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 25 deletions.
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -160,7 +160,7 @@ var schema = {
};
```

**Define schemas with Array types**
### Define schemas with Array types

```javascript
// Defines a 'friends' property with Array type
Expand All @@ -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.<errorName> ```

- ```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

Expand Down
12 changes: 12 additions & 0 deletions lib/errors/CustomValidationError.js
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions lib/errors/TypeValidationError.js
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 0 additions & 13 deletions lib/errors/ValidationError.js

This file was deleted.

3 changes: 2 additions & 1 deletion lib/errors/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
NotFoundError: require('./NotFoundError'),
ValidationError: require('./ValidationError'),
TypeValidationError: require('./TypeValidationError'),
CustomValidationError: require('./CustomValidationError'),
AlreadyExistsError: require('./AlreadyExistsError')
};
29 changes: 24 additions & 5 deletions lib/schema.js
Original file line number Diff line number Diff line change
@@ -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');


Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -77,25 +85,36 @@ 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
if (_.isUndefined(value)) {
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);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stormer",
"version": "0.7.0",
"version": "0.8.0",
"description": "The flexible Node.js ORM",
"main": "index.js",
"directories": {
Expand Down
81 changes: 80 additions & 1 deletion test/schema.spec.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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({
Expand Down Expand Up @@ -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() {

Expand All @@ -306,7 +369,10 @@ describe('Schema Tests', function() {
fieldA: 'String',
fieldB: {
type: 'Number',
default: 100
default: 100,
validate: function(value) {
return value < 1000
}
}
}
}
Expand All @@ -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();
});
Expand All @@ -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();
});
Expand All @@ -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'}]
Expand Down

0 comments on commit 98c02e2

Please sign in to comment.