From 711ff3fd2a253b6f33faa79e95d5e50e3ad69f18 Mon Sep 17 00:00:00 2001 From: John Szwaronek Date: Thu, 8 Dec 2016 14:09:50 -0500 Subject: [PATCH 1/3] Added validateSchema hook to validate JSON objects - Uses ajv valiadtor repo which Best Buy API playground uses - ajv is injected into hook so its not pulled into feathers-hooks-common - This hook differs enough not to have to attribute to BB's hook - Handles sync ajv validation. Async results from custom plugins to ajv. - Throws with an array of err messages in err.errors - Overriding error formatting func could create e.g. { name: msg } type - The above opens possibility to use this for some UI validation. --- src/new.js | 60 ++++++++++++++++++++++++++++- test/validateSchema.test.js | 75 +++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100755 test/validateSchema.test.js diff --git a/src/new.js b/src/new.js index cc4c220e..85f05f51 100755 --- a/src/new.js +++ b/src/new.js @@ -5,7 +5,7 @@ const errors = require('feathers-errors').errors; import { processHooks } from 'feathers-hooks/lib/commons'; -import { checkContext } from './utils'; +import { checkContext, getItems } from './utils'; /** * Mark an item as deleted rather than removing it from the database. @@ -225,3 +225,61 @@ export const isNot = (predicate) => { return result.then(result1 => !result1); }; }; + +/** + * 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); +} 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'" + ]); + } + }); +}); From 8659343b755cf9da0e251ceb3b6e869748c4ab1f Mon Sep 17 00:00:00 2001 From: John Szwaronek Date: Thu, 8 Dec 2016 14:30:04 -0500 Subject: [PATCH 2/3] Fixed Node 4 issue --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 366108a4..d9915283 100755 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "feathers-errors": "^2.4.0" }, "devDependencies": { + "ajv": "4.9.2", "babel-cli": "^6.16.0", "babel-core": "^6.17.0", "babel-plugin-add-module-exports": "^0.2.1", From 16807f6a42d0a382eba673c163cfaad0b682ff3f Mon Sep 17 00:00:00 2001 From: John Szwaronek Date: Mon, 12 Dec 2016 14:40:39 -0500 Subject: [PATCH 3/3] Resolving merge conflict from multiple PRs waiting --- src/new.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/new.js b/src/new.js index 2f346d3b..92a1f093 100755 --- a/src/new.js +++ b/src/new.js @@ -373,6 +373,7 @@ function addNewErrorDflt (errorMessages, ajvError, itemsLen, index) { return (errorMessages || []).concat(message); } +/* * Traverse objects and modifies values in place * * @param {function} converter - conversion function(node).