diff --git a/package.json b/package.json index a244189c..42698fc2 100755 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "traverse": "0.6.6" }, "devDependencies": { + "ajv": "4.9.2", "babel-cli": "^6.16.0", "babel-core": "^6.17.0", "babel-plugin-add-module-exports": "^0.2.1", diff --git a/src/new.js b/src/new.js index 5eb284b4..92a1f093 100755 --- a/src/new.js +++ b/src/new.js @@ -316,6 +316,64 @@ export const isNot = (predicate) => { }; /** + * Validate JSON object using ajv (synchronous) + * + * @param {Object} schema - json schema (//github.com/json-schema/json-schema) + * @param {Function} Ajv - import Ajv from 'ajv' + * @param {Object?} options - ajv options + * addNewError optional reducing function (previousErrs, ajvError, itemsLen, index) + * to format err.errors in the thrown error + * default addNewErrorDflt returns an array of error messages. + * @returns {undefined} + * @throws if hook does not match the schema. err.errors contains the error messages. + * + * Tutorial: //code.tutsplus.com/tutorials/validating-data-with-json-schema-part-1--cms-25343 + */ +export const validateSchema = (schema, Ajv, options = { allErrors: true }) => { + const addNewError = options.addNewError || addNewErrorDflt; + delete options.addNewError; + const validate = new Ajv(options).compile(schema); // for fastest execution + + return hook => { + const items = getItems(hook); + const itemsArray = Array.isArray(items) ? items : [items]; + const itemsLen = itemsArray.length; + let errorMessages; + let invalid = false; + + itemsArray.forEach((item, index) => { + if (!validate(item)) { + invalid = true; + + validate.errors.forEach(ajvError => { + errorMessages = addNewError(errorMessages, ajvError, itemsLen, index); + }); + } + }); + + if (invalid) { + throw new errors.BadRequest('Invalid schema', { errors: errorMessages }); + } + }; +}; + +function addNewErrorDflt (errorMessages, ajvError, itemsLen, index) { + const leader = itemsLen === 1 ? '' : `in row ${index + 1} of ${itemsLen}, `; + let message; + + if (ajvError.dataPath) { + message = `'${leader}${ajvError.dataPath.substring(1)}' ${ajvError.message}`; + } else { + message = `${leader}${ajvError.message}`; + if (ajvError.params && ajvError.params.additionalProperty) { + message += `: '${ajvError.params.additionalProperty}'`; + } + } + + return (errorMessages || []).concat(message); +} + +/* * Traverse objects and modifies values in place * * @param {function} converter - conversion function(node). diff --git a/test/validateSchema.test.js b/test/validateSchema.test.js new file mode 100755 index 00000000..ba5fefc5 --- /dev/null +++ b/test/validateSchema.test.js @@ -0,0 +1,75 @@ +if (!global._babelPolyfill) { require('babel-polyfill'); } + +import { assert } from 'chai'; +import { validateSchema } from '../src/new'; +import Ajv from 'ajv'; + +describe('validateSchema', () => { + let hookBefore; + let hookBeforeArray; + let schema; + + beforeEach(() => { + hookBefore = { + type: 'before', + method: 'create', + params: { provider: 'rest' }, + data: { first: 'John', last: 'Doe' } + }; + hookBeforeArray = { + type: 'before', + method: 'create', + params: { provider: 'rest' }, + data: [ + { first: 'John', last: 'Doe' }, + { first: 'Jane', last: 'Doe' }, + { first: 'Joe', last: 'Doe' } + ] + }; + schema = { + 'properties': { + 'first': { 'type': 'string' }, + 'last': { 'type': 'string' } + }, + 'required': ['first', 'last'] + }; + }); + + it('works with valid single item', () => { + validateSchema(schema, Ajv)(hookBefore); + }); + + it('works with array of valid items', () => { + validateSchema(schema, Ajv)(hookBeforeArray); + }); + + it('fails with in valid single item', () => { + hookBefore.data = { first: 1 }; + + try { + validateSchema(schema, Ajv)(hookBefore); + assert.fail(true, false, 'test succeeds unexpectedly'); + } catch (err) { + assert.deepEqual(err.errors, [ + '\'first\' should be string', + 'should have required property \'last\'' + ]); + } + }); + + it('fails with array of invalid items', () => { + hookBeforeArray.data[0] = { first: 1 }; + delete hookBeforeArray.data[2].last; + + try { + validateSchema(schema, Ajv)(hookBeforeArray); + assert.fail(true, false, 'test succeeds unexpectedly'); + } catch (err) { + assert.deepEqual(err.errors, [ + "'in row 1 of 3, first' should be string", + "in row 1 of 3, should have required property 'last'", + "in row 3 of 3, should have required property 'last'" + ]); + } + }); +});