var cloneDeep = require('lodash.clonedeep'); var mschema = require('mschema'); var v = require('validator'); /** * Custom types. * To add a custom type, add a key with the type name and a value * in the form {parent: {...}, assertValid: function(d) {...}} * where parent is the base mschema spec and validate is a function * accepting one data point and throwing an array of errors if invalid. * * Error array format is derived from mschema and is in the form * [{property: x, constraint: x, actual: x, expected: x, message: x} ... ] */ var types = { /** * Larger text inputs. Currently just proxies to string type. */ 'text': { parent: {type: 'string'}, // Let parent handle validation assertValid: function(spec, value) {} }, 'video': { parent: {type: 'string'}, // Let parent handle validation assertValid: function(spec, value) {} }, 'enum': { parent: {type: 'string'}, assertValid: function(spec, value) { value = value || ''; if (spec.options.indexOf(value.trim()) === -1) { throw [{ constraint: 'enum', actual: value, expected: JSON.stringify(spec.options), message: 'Invalid value' }]; } } }, 'captioned-image-list': { isArray: true, parent: [{ uri: { type: 'string' }, // TODO Implement URI type caption: { type: 'string' } }], assertValid: function(spec, value) {} }, // Special type for resource meta information 'meta': { parent: 'string', assertValid: function(spec, value) {} }, 'link-list': { isArray: true, parent: [{ uri: { type: 'string' }, text: { type: 'string' } }], assertValid: function(spec, value) {} }, 'date': { parent: 'string', assertValid: function(spec, value) {} }, 'flag': { parent: 'string', assertValid: function(spec, value) {} }, 'foreign-key': { parent: 'enum', assertValid: function(spec, value) {} }, 'media-list': { isArray: true, parent: [], assertValid: function(spec, value) {} }, 'double-captioned-image-list': { isArray: true, parent: [], assertValid: function(spec, value) {} }, 'triple-captioned-image-list': { isArray: true, parent: [], assertValid: function(spec, value) {} }, } /* function checkArrayLength (spec, value) { var message; var actual; if (!value || !value.length) { throw [{ message: 'Not an array', expected: JSON.stringify(this.parent), actual: value }]; } } */ /** * OKSchema! * Meant as a thin wrapper around some existing schema validation * module, mostly to allow for the extension of types. * * NOTE: Currently just assumes spec is valid. If you give a bad spec * strange things may or may not happen */ function OKSchema(spec) { if (!(this instanceof OKSchema)) return new OKSchema(spec); if (!spec) throw new Error('No spec provided to OKSchema'); spec = cloneDeep(spec); var specKeys = Object.keys(spec); // Cache the mschema version of our spec this._mschemaSpec = specKeys.reduce(function(cache, prop) { // If custom type, return its parent spec var type = spec[prop].type; if (types[type]) { cache[prop] = types[type].parent; // Otherwise, it's already in mschema format } else { cache[prop] = spec[prop]; } return cache; }, {}); // Find ID field var idField; specKeys.every(function(prop) { if (prop === 'id' || spec[prop].id) { idField = prop; return false; } else { return true; } }); // Register autoincrement fields // NOTE Does not work for nested fields var autoIncrementFields = specKeys.reduce(function(arr, prop) { var specProp = spec[prop]; if (specProp.autoincrement) { arr.push(prop); } return arr; }, []); Object.defineProperty(this, 'spec', { get: function() { return cloneDeep(spec); }, enumerable: true }); Object.defineProperty(this, 'idField', { value: idField, writable: true, enumerable: true }); Object.defineProperty(this, 'autoIncrementFields',{ get: function() { return cloneDeep(autoIncrementFields); }, enumerable: true }); } OKSchema.prototype.checkDataForMissingArrays = function(data) { data = data || {}; var spec = this.spec; // The qs body-parser module does not have a way to represent // empty lists. If you delete all elements from a list, // check against the spec so we know to replace with an empty list. Object.keys(spec).forEach(function(prop){ var type = spec[prop].type; if (types[type] && types[type].isArray && ! data[prop]) { data[prop] = [] } }) } OKSchema.prototype.assertValid = function(data) { data = data || {}; var spec = this.spec; this.checkDataForMissingArrays(data) // Run through custom validators, they'll throw if invalid Object.keys(data).forEach(function(prop) { var type = spec[prop].type; if (types[type]) { types[type].assertValid(spec[prop], data[prop]); // Also check if it's a number type and try to cast it // otherwise pass and let mschema handle } else if (type === 'number') { try { data[prop] = parseFloat(data[prop]); } catch (err) {} } }); var result = mschema.validate(data, this.toMschema()); if (!result.valid) { throw result.errors; } }; /** * Return our custom spec as an mschema spec */ OKSchema.prototype.toMschema = function() { return this._mschemaSpec; }; module.exports = OKSchema;