Add validation from schema.js

closes #1401
- added data/validation/index.js
- added generic validation for length
- added generic validation for nullable
- added validations object to schema.js for custom validation
- removed pyramid of doom from api/db.js
This commit is contained in:
Sebastian Gierlinger 2014-02-19 18:32:23 +01:00
parent f273e17e08
commit ac7f4f05c4
11 changed files with 122 additions and 106 deletions

View File

@ -6,7 +6,7 @@ var dataExport = require('../data/export'),
when = require('when'),
nodefn = require('when/node/function'),
_ = require('lodash'),
schema = require('../data/schema').tables,
validation = require('../data/validation'),
config = require('../config'),
api = {},
@ -69,8 +69,7 @@ db = {
return nodefn.call(fs.readFile, options.importfile.path);
}).then(function (fileContents) {
var importData,
error = '',
tableKeys = _.keys(schema);
error = '';
// Parse the json data
try {
@ -83,28 +82,13 @@ db = {
return when.reject(new Error("Import data does not specify version"));
}
_.each(tableKeys, function (constkey) {
_.each(importData.data[constkey], function (elem) {
var prop;
for (prop in elem) {
if (elem.hasOwnProperty(prop)) {
if (schema[constkey].hasOwnProperty(prop)) {
if (!_.isNull(elem[prop])) {
if (elem[prop].length > schema[constkey][prop].maxlength) {
error += error !== "" ? "<br>" : "";
error += "Property '" + prop + "' exceeds maximum length of " + schema[constkey][prop].maxlength + " (element:" + constkey + " / id:" + elem.id + ")";
}
} else {
if (!schema[constkey][prop].nullable) {
error += error !== "" ? "<br>" : "";
error += "Property '" + prop + "' is not nullable (element:" + constkey + " / id:" + elem.id + ")";
}
}
} else {
error += error !== "" ? "<br>" : "";
error += "Property '" + prop + "' is not allowed (element:" + constkey + " / id:" + elem.id + ")";
}
}
_.each(_.keys(importData.data), function (tableName) {
_.each(importData.data[tableName], function (importValues) {
try {
validation.validateSchema(tableName, importValues);
} catch (err) {
error += error !== "" ? "<br>" : "";
error += err.message;
}
});
});

View File

@ -1,14 +1,14 @@
var db = {
posts: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
title: {type: 'string', maxlength: 150, nullable: false},
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
markdown: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true},
html: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true},
image: {type: 'text', maxlength: 2000, nullable: true},
featured: {type: 'bool', nullable: false, defaultTo: false},
page: {type: 'bool', nullable: false, defaultTo: false},
page: {type: 'bool', nullable: false, defaultTo: false, validations: {'isIn': ['true', 'false']}},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'draft'},
language: {type: 'string', maxlength: 6, nullable: false, defaultTo: 'en_US'},
meta_title: {type: 'string', maxlength: 150, nullable: true},
@ -23,15 +23,15 @@ var db = {
},
users: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
name: {type: 'string', maxlength: 150, nullable: false, unique: true},
slug: {type: 'string', maxlength: 150, nullable: false},
password: {type: 'string', maxlength: 60, nullable: false},
email: {type: 'string', maxlength: 254, nullable: false, unique: true},
email: {type: 'string', maxlength: 254, nullable: false, unique: true, validations: {'isEmail': true}},
image: {type: 'text', maxlength: 2000, nullable: true},
cover: {type: 'text', maxlength: 2000, nullable: true},
bio: {type: 'string', maxlength: 200, nullable: true},
website: {type: 'text', maxlength: 2000, nullable: true},
website: {type: 'text', maxlength: 2000, nullable: true, validations: {'isUrl': true}},
location: {type: 'text', maxlength: 65535, nullable: true},
accessibility: {type: 'text', maxlength: 65535, nullable: true},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'active'},
@ -46,7 +46,7 @@ var db = {
},
roles: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
name: {type: 'string', maxlength: 150, nullable: false},
description: {type: 'string', maxlength: 200, nullable: true},
created_at: {type: 'dateTime', nullable: false},
@ -61,7 +61,7 @@ var db = {
},
permissions: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
name: {type: 'string', maxlength: 150, nullable: false},
object_type: {type: 'string', maxlength: 150, nullable: false},
action_type: {type: 'string', maxlength: 150, nullable: false},
@ -88,10 +88,10 @@ var db = {
},
settings: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
key: {type: 'string', maxlength: 150, nullable: false, unique: true},
value: {type: 'text', maxlength: 65535, nullable: true},
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core'},
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core', validations: {'isIn': ['core', 'blog', 'theme', 'app', 'plugin']}},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'integer', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
@ -99,7 +99,7 @@ var db = {
},
tags: {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false},
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
name: {type: 'string', maxlength: 150, nullable: false},
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
description: {type: 'string', maxlength: 200, nullable: true},

View File

@ -0,0 +1,89 @@
var schema = require('../schema').tables,
_ = require('lodash'),
validator = require('validator'),
when = require('when'),
validateSchema,
validateSettings,
validate;
// Validation validation against schema attributes
// values are checked against the validation objects
// form schema.js
validateSchema = function (tableName, model) {
var columns = _.keys(schema[tableName]);
_.each(columns, function (columnKey) {
// check nullable
if (model.hasOwnProperty(columnKey) && schema[tableName][columnKey].hasOwnProperty('nullable')
&& schema[tableName][columnKey].nullable !== true) {
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] cannot be blank.').notNull();
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] cannot be blank.').notEmpty();
}
// TODO: check if mandatory values should be enforced
if (model[columnKey]) {
// check length
if (schema[tableName][columnKey].hasOwnProperty('maxlength')) {
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] exceeds maximum length of %2 characters.').len(0, schema[tableName][columnKey].maxlength);
}
//check validations objects
if (schema[tableName][columnKey].hasOwnProperty('validations')) {
validate(model[columnKey], columnKey, schema[tableName][columnKey].validations);
}
//check type
if (schema[tableName][columnKey].hasOwnProperty('type')) {
if (schema[tableName][columnKey].type === 'integer') {
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] is no valid integer.' + model[columnKey]).isInt();
}
}
}
});
};
// Validation for settings
// settings are checked against the validation objects
// form default-settings.json
validateSettings = function (defaultSettings, model) {
var values = model.toJSON(),
matchingDefault = defaultSettings[values.key];
if (matchingDefault && matchingDefault.validations) {
validate(values.value, values.key, matchingDefault.validations);
}
};
// Validate using the validation module.
// Each validation's key is a name and its value is an array of options
// Use true (boolean) if options aren't applicable
//
// eg:
// validations: { isUrl: true, len: [20, 40] }
//
// will validate that a values's length is a URL between 20 and 40 chars,
// available validators: https://github.com/chriso/node-validator#list-of-validation-methods
validate = function (value, key, validations) {
_.each(validations, function (validationOptions, validationName) {
var validation = validator.check(value, 'Validation [' + validationName + '] of field [' + key + '] failed.');
if (validationOptions === true) {
validationOptions = null;
}
if (typeof validationOptions !== 'array') {
validationOptions = [validationOptions];
}
// equivalent of validation.isSomething(option1, option2)
validation[validationName].apply(validation, validationOptions);
}, this);
};
module.exports = {
validateSchema: validateSchema,
validateSettings: validateSettings
};

View File

@ -4,10 +4,10 @@ var Bookshelf = require('bookshelf'),
_ = require('lodash'),
uuid = require('node-uuid'),
config = require('../config'),
Validator = require('validator').Validator,
unidecode = require('unidecode'),
sanitize = require('validator').sanitize,
schema = require('../data/schema'),
validation = require('../data/validation'),
ghostBookshelf;
@ -15,7 +15,6 @@ var Bookshelf = require('bookshelf'),
ghostBookshelf = Bookshelf.ghost = Bookshelf.initialize(config().database);
ghostBookshelf.client = config().database.client;
ghostBookshelf.validator = new Validator();
// The Base Model which other Ghost objects will inherit from,
// including some convenience functions as static properties on the model.
@ -45,7 +44,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
},
validate: function () {
return true;
validation.validateSchema(this.tableName, this.toJSON());
},
creating: function () {

View File

@ -1,6 +1,7 @@
var ghostBookshelf = require('./base'),
User = require('./user').User,
Role = require('./role').Role,
validation = require('../data/validation'),
Permission,
Permissions;
@ -9,11 +10,6 @@ Permission = ghostBookshelf.Model.extend({
tableName: 'permissions',
validate: function () {
// TODO: validate object_type, action_type and object_id
ghostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty();
},
roles: function () {
return this.belongsToMany(Role);
},

View File

@ -9,6 +9,7 @@ var _ = require('lodash'),
Tag = require('./tag').Tag,
Tags = require('./tag').Tags,
ghostBookshelf = require('./base'),
validation = require('../data/validation'),
Post,
Posts,
@ -37,12 +38,7 @@ Post = ghostBookshelf.Model.extend({
},
validate: function () {
ghostBookshelf.validator.check(this.get('title'), "Post title cannot be blank").notEmpty();
ghostBookshelf.validator.check(this.get('title'), 'Post title maximum length is 150 characters.').len(0, 150);
ghostBookshelf.validator.check(this.get('slug'), "Post title cannot be blank").notEmpty();
ghostBookshelf.validator.check(this.get('slug'), 'Post title maximum length is 150 characters.').len(0, 150);
return true;
validation.validateSchema(this.tableName, this.toJSON());
},
saving: function (newPage, attr, options) {

View File

@ -9,11 +9,6 @@ Role = ghostBookshelf.Model.extend({
tableName: 'roles',
validate: function () {
ghostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty();
ghostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty();
},
users: function () {
return this.belongsToMany(User);
},

View File

@ -1,10 +1,10 @@
var Settings,
ghostBookshelf = require('./base'),
validator = ghostBookshelf.validator,
uuid = require('node-uuid'),
_ = require('lodash'),
errors = require('../errorHandling'),
when = require('when'),
validation = require('../data/validation'),
defaultSettings;
@ -41,37 +41,9 @@ Settings = ghostBookshelf.Model.extend({
};
},
// Validate default settings using the validator module.
// Each validation's key is a name and its value is an array of options
// Use true (boolean) if options aren't applicable
//
// eg:
// validations: { isUrl: true, len: [20, 40] }
//
// will validate that a setting's length is a URL between 20 and 40 chars,
// available validators: https://github.com/chriso/node-validator#list-of-validation-methods
validate: function () {
validator.check(this.get('key'), "Setting key cannot be blank").notEmpty();
validator.check(this.get('type'), "Setting type cannot be blank").notEmpty();
var matchingDefault = defaultSettings[this.get('key')];
if (matchingDefault && matchingDefault.validations) {
_.each(matchingDefault.validations, function (validationOptions, validationName) {
var validation = validator.check(this.get('value'));
if (validationOptions === true) {
validationOptions = null;
}
if (typeof validationOptions !== 'array') {
validationOptions = [validationOptions];
}
// equivalent of validation.isSomething(option1, option2)
validation[validationName].apply(validation, validationOptions);
}, this);
}
validation.validateSchema(this.tableName, this.toJSON());
validation.validateSettings(defaultSettings, this);
},

View File

@ -8,11 +8,6 @@ Tag = ghostBookshelf.Model.extend({
tableName: 'tags',
validate: function () {
return true;
},
saving: function () {
var self = this;
ghostBookshelf.Model.prototype.saving.apply(this, arguments);

View File

@ -10,6 +10,7 @@ var _ = require('lodash'),
Permission = require('./permission').Permission,
http = require('http'),
crypto = require('crypto'),
validator = require('validator'),
tokenSecurity = {},
User,
@ -17,11 +18,10 @@ var _ = require('lodash'),
function validatePasswordLength(password) {
try {
ghostBookshelf.validator.check(password, "Your password must be at least 8 characters long.").len(8);
validator.check(password, "Your password must be at least 8 characters long.").len(8);
} catch (error) {
return when.reject(error);
}
return when.resolve();
}
@ -37,16 +37,6 @@ User = ghostBookshelf.Model.extend({
tableName: 'users',
validate: function () {
ghostBookshelf.validator.check(this.get('email'), "Please enter a valid email address. That one looks a bit dodgy.").isEmail();
ghostBookshelf.validator.check(this.get('bio'), "We're not writing a novel here! I'm afraid your bio has to stay under 200 characters.").len(0, 200);
if (this.get('website') && this.get('website').length > 0) {
ghostBookshelf.validator.check(this.get('website'), "Looks like your website is not actually a website. Try again?").isUrl();
}
ghostBookshelf.validator.check(this.get('location'), 'This seems a little too long! Please try and keep your location under 150 characters.').len(0, 150);
return true;
},
saving: function () {
var self = this;
// disabling sanitization until we can implement a better version

View File

@ -246,7 +246,7 @@ describe("Import", function () {
}).then(function () {
(1).should.eql(0, 'Data import should not resolve promise.');
}, function (error) {
error.should.eql('Error importing data: Post title maximum length is 150 characters.');
error.should.eql('Error importing data: Value in [posts.title] exceeds maximum length of 150 characters.');
when.all([
knex("users").select(),
@ -292,7 +292,7 @@ describe("Import", function () {
}).then(function () {
(1).should.eql(0, 'Data import should not resolve promise.');
}, function (error) {
error.should.eql('Error importing data: Setting key cannot be blank');
error.should.eql('Error importing data: Value in [settings.key] cannot be blank.');
when.all([
knex("users").select(),
@ -433,7 +433,7 @@ describe("Import", function () {
}).then(function () {
(1).should.eql(0, 'Data import should not resolve promise.');
}, function (error) {
error.should.eql('Error importing data: Post title maximum length is 150 characters.');
error.should.eql('Error importing data: Value in [posts.title] exceeds maximum length of 150 characters.');
when.all([
knex("users").select(),
@ -479,7 +479,7 @@ describe("Import", function () {
}).then(function () {
(1).should.eql(0, 'Data import should not resolve promise.');
}, function (error) {
error.should.eql('Error importing data: Setting key cannot be blank');
error.should.eql('Error importing data: Value in [settings.key] cannot be blank.');
when.all([
knex("users").select(),