From 18e50407cedd062f8039a7db0c8a0396c6ff0cb2 Mon Sep 17 00:00:00 2001 From: Daniil Yastremskiy Date: Sun, 23 Apr 2017 01:36:35 +0300 Subject: [PATCH 1/4] Add async schema validation support --- src/services/validate-schema.js | 28 +++++++++++++-------- test/services/validate-schema.test.js | 36 +++++++++++++-------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/services/validate-schema.js b/src/services/validate-schema.js index bd4ef8b6..5fdb0fab 100755 --- a/src/services/validate-schema.js +++ b/src/services/validate-schema.js @@ -12,7 +12,7 @@ export default function (schema, ajvOrAjv, options = { allErrors: true }) { let ajv, Ajv; if (typeof ajvOrAjv.addKeyword !== 'function') { Ajv = ajvOrAjv; - ajv = new Ajv(options); + ajv = new Ajv(options); } else { ajv = ajvOrAjv; } @@ -25,19 +25,27 @@ export default function (schema, ajvOrAjv, options = { allErrors: true }) { let errorMessages = null; let invalid = false; - itemsArray.forEach((item, index) => { - if (!validate(item)) { - invalid = true; + return Promise.all(itemsArray.map((item, index) => { + return Promise.resolve(validate(item)) + // Handler for synchronous validation + .then((isValid) => { + if (!isValid) { + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject({errors: validate.errors}); + } + }) + .catch((err) => { + invalid = true; - validate.errors.forEach(ajvError => { - errorMessages = addNewError(errorMessages, ajvError, itemsLen, index); + err.errors.forEach(ajvError => { + errorMessages = addNewError(errorMessages, ajvError, itemsLen, index); + }); }); + })).then(() => { + if (invalid) { + throw new errors.BadRequest('Invalid schema', { errors: errorMessages }); } }); - - if (invalid) { - throw new errors.BadRequest('Invalid schema', { errors: errorMessages }); - } }; } diff --git a/test/services/validate-schema.test.js b/test/services/validate-schema.test.js index 5cd8af00..d7879cf7 100755 --- a/test/services/validate-schema.test.js +++ b/test/services/validate-schema.test.js @@ -45,30 +45,28 @@ describe('services validateSchema', () => { 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\'' - ]); - } + validateSchema(schema, Ajv)(hookBefore) + .catch((err) => { + assert.fail(true, false, 'test succeeds unexpectedly'); + 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'" - ]); - } + validateSchema(schema, Ajv)(hookBefore) + .catch((err) => { + assert.fail(true, false, 'test succeeds unexpectedly'); + 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 9f47e4d1ba798c21e622a2ff6a507b2181fe99de Mon Sep 17 00:00:00 2001 From: Daniil Yastremskiy Date: Sun, 23 Apr 2017 02:28:30 +0300 Subject: [PATCH 2/4] Improve validateSchema error handling --- src/services/validate-schema.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/validate-schema.js b/src/services/validate-schema.js index 5fdb0fab..df34fc03 100755 --- a/src/services/validate-schema.js +++ b/src/services/validate-schema.js @@ -30,11 +30,13 @@ export default function (schema, ajvOrAjv, options = { allErrors: true }) { // Handler for synchronous validation .then((isValid) => { if (!isValid) { - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject({errors: validate.errors}); + return Promise.reject( + new ajv.constructor.ValidationError(validate.errors)); } }) .catch((err) => { + if (!(err instanceof ajv.constructor.ValidationError)) throw err; + invalid = true; err.errors.forEach(ajvError => { From 8df076218bb5fe4113d3140cd4cc498004ab641f Mon Sep 17 00:00:00 2001 From: Daniil Yastremskiy Date: Tue, 25 Apr 2017 19:52:11 +0300 Subject: [PATCH 3/4] Change async schemas validation Return Promise only if schema contains $async prop --- src/services/validate-schema.js | 49 ++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/services/validate-schema.js b/src/services/validate-schema.js index df34fc03..a0bfaad2 100755 --- a/src/services/validate-schema.js +++ b/src/services/validate-schema.js @@ -25,29 +25,40 @@ export default function (schema, ajvOrAjv, options = { allErrors: true }) { let errorMessages = null; let invalid = false; - return Promise.all(itemsArray.map((item, index) => { - return Promise.resolve(validate(item)) - // Handler for synchronous validation - .then((isValid) => { - if (!isValid) { - return Promise.reject( - new ajv.constructor.ValidationError(validate.errors)); - } - }) - .catch((err) => { - if (!(err instanceof ajv.constructor.ValidationError)) throw err; + if (schema.$async) { + return Promise.all(itemsArray.map((item, index) => { + return validate(item) + .catch(err => { + if (!(err instanceof ajv.constructor.ValidationError)) throw err; - invalid = true; + invalid = true; - err.errors.forEach(ajvError => { - errorMessages = addNewError(errorMessages, ajvError, itemsLen, index); + addErrors(err.errors, index); }); - }); - })).then(() => { - if (invalid) { - throw new errors.BadRequest('Invalid schema', { errors: errorMessages }); + })).then(() => { + if (invalid) { + throw new errors.BadRequest('Invalid schema', { errors: errorMessages }); + } + }); + } + + itemsArray.forEach((item, index) => { + if (!validate(item)) { + invalid = true; + + addErrors(validate.errors, index); } - }); + }); + + if (invalid) { + throw new errors.BadRequest('Invalid schema', { errors: errorMessages }); + } + + function addErrors (errors, index) { + errors.forEach(ajvError => { + errorMessages = addNewError(errorMessages, ajvError, itemsLen, index); + }); + } }; } From cb8e72cf85b1f531f0ae43ddf0829b341aa2aafb Mon Sep 17 00:00:00 2001 From: Daniil Yastremskiy Date: Tue, 25 Apr 2017 19:55:30 +0300 Subject: [PATCH 4/4] Cover async schema validation with tests --- test/services/validate-schema.test.js | 167 +++++++++++++++++++++----- 1 file changed, 138 insertions(+), 29 deletions(-) diff --git a/test/services/validate-schema.test.js b/test/services/validate-schema.test.js index d7879cf7..af3a6337 100755 --- a/test/services/validate-schema.test.js +++ b/test/services/validate-schema.test.js @@ -7,6 +7,8 @@ describe('services validateSchema', () => { let hookBefore; let hookBeforeArray; let schema; + let asyncSchema; + let ajvAsync; beforeEach(() => { hookBefore = { @@ -25,48 +27,155 @@ describe('services validateSchema', () => { { 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); - }); + describe('Sync validation', () => { + beforeEach(() => { + schema = { + 'properties': { + 'first': { 'type': 'string' }, + 'last': { 'type': 'string' } + }, + 'required': ['first', 'last'] + }; + }); - it('works with array of valid items', () => { - validateSchema(schema, Ajv)(hookBeforeArray); - }); + it('works with valid single item', () => { + validateSchema(schema, Ajv)(hookBefore); + }); - it('fails with in valid single item', () => { - hookBefore.data = { first: 1 }; + it('works with array of valid items', () => { + validateSchema(schema, Ajv)(hookBeforeArray); + }); - validateSchema(schema, Ajv)(hookBefore) - .catch((err) => { - assert.fail(true, false, 'test succeeds unexpectedly'); - assert.deepEqual(err.errors, [ - '\'first\' should be string', - 'should have required property \'last\'' - ]); - }); - }); + it('fails with in valid single item', () => { + hookBefore.data = { first: 1 }; - it('fails with array of invalid items', () => { - hookBeforeArray.data[0] = { first: 1 }; - delete hookBeforeArray.data[2].last; + 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\'' + ]); + } + }); - validateSchema(schema, Ajv)(hookBefore) - .catch((err) => { + 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'" ]); + } + }); + }); + + describe('Async validation', () => { + before(() => { + ajvAsync = new Ajv({ allErrors: true }); + + ajvAsync.addKeyword('equalsDoe', { + async: true, + schema: false, + validate: (item) => new Promise((resolve, reject) => { + setTimeout(() => { + item === 'Doe' + ? resolve(true) + : reject(new Ajv.ValidationError([{message: 'should be Doe'}])); + }, 50); + }) }); + + ajvAsync.addFormat('3or4chars', { + async: true, + validate: (item) => new Promise((resolve, reject) => { + setTimeout(() => { + (item.length === 3 || item.length === 4) + ? resolve(true) + : resolve(false); + }, 50); + }) + }); + }); + + beforeEach(() => { + asyncSchema = { + '$async': true, + 'properties': { + 'first': { + 'type': 'string', + 'format': '3or4chars' + }, + 'last': { + 'type': 'string', + 'equalsDoe': true + } + }, + 'required': ['first', 'last'] + }; + }); + + it('works with valid single item', (next) => { + validateSchema(asyncSchema, ajvAsync)(hookBefore) + .then(() => { + next(); + }) + .catch((err) => { + console.log(err); + assert.fail(true, false, 'test fails unexpectedly'); + }); + }); + + it('works with array of valid items', (next) => { + validateSchema(asyncSchema, ajvAsync)(hookBeforeArray) + .then(() => { + next(); + }) + .catch(() => { + assert.fail(true, false, 'test fails unexpectedly'); + }); + }); + + it('fails with in valid single item', (next) => { + hookBefore.data = { first: '1' }; + + validateSchema(asyncSchema, ajvAsync)(hookBefore) + .then(() => { + assert.fail(true, false, 'test succeeds unexpectedly'); + }) + .catch((err) => { + assert.deepEqual(err.errors, [ + '\'first\' should match format "3or4chars"', + 'should have required property \'last\'' + ]); + next(); + }); + }); + + it('fails with array of invalid items', (next) => { + hookBeforeArray.data[0].last = 'not Doe'; + delete hookBeforeArray.data[2].last; + + validateSchema(asyncSchema, ajvAsync)(hookBeforeArray) + .then(() => { + assert.fail(true, false, 'test succeeds unexpectedly'); + }) + .catch((err) => { + assert.deepEqual(err.errors, [ + 'in row 3 of 3, should have required property \'last\'', + '\'in row 1 of 3, last\' should be Doe' + ]); + next(); + }); + }); }); });