-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Evan King
committed
Nov 7, 2016
0 parents
commit 0f6c3a6
Showing
9 changed files
with
575 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
coverage/ | ||
node_modules/ | ||
dist/ | ||
npm-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
test/ | ||
coverage/ | ||
.travis.yml | ||
.npmignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
language: node_js | ||
node_js: | ||
- 4 | ||
- 5 | ||
|
||
script: | ||
- npm run-script travis | ||
|
||
after_script: | ||
- npm install coveralls@2.10.0 | ||
- cat ./coverage/lcov.info | coveralls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2016 Evan King | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
|
||
# joi-validate-patch | ||
|
||
[![version][version-img]][version-url] | ||
[![npm][npmjs-img]][npmjs-url] | ||
[![build status][travis-img]][travis-url] | ||
[![Coveralls][coveralls-img]][coveralls-url] | ||
[![deps status][daviddm-img]][daviddm-url] | ||
[![mit license][license-img]][license-url] | ||
|
||
JoiValidatePatch is a node library which validates that operations in a JSON patch | ||
document fit within a Joi validation schema describing a document structure. Validation | ||
is performed using only the schema, independently from the document(s) to be modified. | ||
|
||
Note: only validation of independent values can be meaningfully supported. | ||
|
||
The primary use-case is for simple schemas covering the basics of sanity validation | ||
when accepting a JSON patch to be converted into some other form of dynamic operation | ||
where loading the documents, applying the patch, and validating the result is impractical. | ||
The typical example would be updating a mongo store or relational database | ||
|
||
Within the limitations of the use-case, some validations are easy (can the path | ||
of the operation exist in the schema?), others are challenging (if moving content | ||
from one path to another, are the schema rules compatible?), and others still are | ||
impossible (if two paths have interdependent rules, will they still be satisfied | ||
when changing one of those paths?). JoiValidatePatch only handles the easy rules | ||
and leaves the rest up to custom solutions. It can however sidestep some complexities | ||
by simply receiving a subset of the true document schema, consisting only of the | ||
paths that are safe to independently modify and/or covered by additional validation | ||
logic elsewhere. | ||
|
||
|
||
## Basic Usage | ||
|
||
Validating a patch document against a Joi schema: | ||
```js | ||
const | ||
Joi = require('joi'), | ||
JVPatch = require('joi-validate-patch'); | ||
|
||
const schema = Joi.object().keys({ | ||
id: Joi.string().guid().required().label('id'), | ||
name: Joi.string().required().label('name'), | ||
description: Joi.string().optional().label('description'), | ||
favoriteToys: Joi.array().items(Joi.string().label('toy')).default([]).label('favoriteToys'), | ||
meta: { | ||
born: Joi.date().required().label('born'), | ||
weight: Joi.number().positive().unit('pounds').label('weight') | ||
} | ||
}).label('cat'); | ||
|
||
const patch = [ | ||
{op: 'replace', path: '/name', value: 'Tigger'}, | ||
{op: 'add', path: '/favoriteToys/-', value: 'laser pointer'}, | ||
]; | ||
|
||
const result = JVPatch.validate(patch, schema); | ||
|
||
if(result.error) throw result.error; | ||
|
||
const normalizedPatch = result.value; | ||
``` | ||
|
||
## API | ||
|
||
#### lib.ValidationError(message, details) ⇒ ValidationError | ||
|
||
Constructor for custom error class. Takes on the properties of a patch | ||
step passed into it, or adds an `errors` property aggregating sub-errors. | ||
|
||
Params: | ||
- string message | ||
- object details (optional single JSON patch operation or {errors: [ValidationError, ...]}) | ||
|
||
Returns: ValidationError | ||
|
||
#### lib.validate(patchDocs, joiSchema, [options], [callback]) ⇒ {error: ValidationError|null, value: normalizedPatchDocs} | ||
|
||
Main library method, performs validation against a Joi schema like Joi, but | ||
accepts a json-patch item or array rather than the actual document. | ||
|
||
Maintains consistency with Joi.validate signature and behavior (even down to the | ||
non-async callback support). | ||
|
||
Params: | ||
- array patchDocs (array of objects describing JSON patch operations) | ||
- object joiSchema | ||
- object options | ||
- abortEarly (default true) | ||
- allowedOps (array of strings for allowed patch operation types - default all) | ||
- allowUnknown (default false) | ||
- convert (default true) | ||
- function callback (if provided, called with error, value instead of returning an object) | ||
|
||
Returns: `{error: ValidationError|null, value: [patchOperation, ...]}` | ||
|
||
[version-url]: https://github.com/evan-king/joi-validate-patch/releases | ||
[version-img]: https://img.shields.io/github/release/evan-king/joi-validate-patch.svg?style=flat | ||
|
||
[npmjs-url]: https://www.npmjs.com/package/joi-validate-patch | ||
[npmjs-img]: https://img.shields.io/npm/v/joi-validate-patch.svg?style=flat | ||
|
||
[coveralls-url]: https://coveralls.io/r/evan-king/joi-validate-patch?branch=master | ||
[coveralls-img]: https://img.shields.io/coveralls/evan-king/joi-validate-patch.svg?style=flat | ||
|
||
[license-url]: https://github.com/evan-king/joi-validate-patch/blob/master/LICENSE | ||
[license-img]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat | ||
|
||
[travis-url]: https://travis-ci.org/evan-king/joi-validate-patch | ||
[travis-img]: https://img.shields.io/travis/evan-king/joi-validate-patch.svg?style=flat | ||
|
||
[daviddm-url]: https://david-dm.org/evan-king/joi-validate-patch | ||
[daviddm-img]: https://img.shields.io/david/evan-king/joi-validate-patch.svg?style=flat |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
module.exports = require('./lib/joi-validate-patch'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
"use strict"; | ||
|
||
const | ||
util = require('util'), | ||
jsonpatch = require('fast-json-patch'), | ||
Joi = require('joi'); | ||
|
||
|
||
const | ||
supportMap = { | ||
add: ['path', 'value'], | ||
remove: ['path'], | ||
replace: ['path', 'value'], | ||
copy: ['from', 'path'], | ||
move: ['from', 'path'], | ||
test: ['path', 'value'] | ||
}, | ||
defaultOptions = { | ||
abortEarly: true, | ||
allowedOps: Object.keys(supportMap), | ||
allowUnknown: false, | ||
convert: true | ||
}; | ||
|
||
// Custom error class | ||
class ValidationError extends Error { | ||
constructor(message, patchStep) { | ||
super(message); | ||
Error.captureStackTrace(this, this.constructor); | ||
this.name = this.constructor.name; | ||
|
||
if(!patchStep) return; | ||
|
||
['op', 'from', 'path', 'value', 'errors'].forEach(addProp.bind(this, patchStep)); | ||
if(this.path) this.path = this.path.replace('/', '.').substr(1); | ||
} | ||
} | ||
exports.ValidationError = ValidationError; | ||
|
||
function addProp(patchStep, prop) { | ||
if(patchStep[prop]) this[prop] = patchStep[prop]; | ||
} | ||
|
||
function getValidator(schema, pointer) { | ||
const parts = (pointer || '').split('/').splice(1); | ||
|
||
let cursor = schema; | ||
parts.forEach(function(key) { | ||
if(!cursor) return false; | ||
|
||
// support schemas not wholly wrapped in Joi | ||
if(!cursor.isJoi) return cursor = cursor[key]; | ||
|
||
// indexing into arrays only supported when there's exactly one | ||
// rule - constraints on the array itself when altering its | ||
// contents also cannot be supported | ||
if(cursor._type == 'array') { | ||
return cursor = (cursor._inner.items.length == 1) | ||
? cursor._inner.items[0] | ||
: undefined; | ||
} | ||
|
||
const child = cursor._inner.children.find(obj => obj.key == key); | ||
cursor = child ? child.schema : null; | ||
}); | ||
|
||
return cursor; | ||
} | ||
|
||
function isArrayPath(path) { | ||
return false; | ||
} | ||
|
||
function validateStep(schema, options, step) { | ||
const | ||
opts = Object.assign({}, options), | ||
clean = Object.assign({}, step), | ||
rule = getValidator(schema, step.path), | ||
allowed = opts.allowedOps, | ||
fail = msg => { return {error: new ValidationError(msg, step), value: null}; }, | ||
pass = () => { return {error: null, value: clean}; }; | ||
|
||
// Joi rejects unknown configuration options | ||
delete opts.allowedOps; | ||
|
||
if(allowed.indexOf(step.op) < 0) return fail(`disallowed op ${step.op}`); | ||
if(!rule && !opts.allowUnknown) return fail(`invalid path ${step.path}`); | ||
|
||
const formatErr = jsonpatch.validate([step]); | ||
if(formatErr) return fail(new ValidationError(formatErr.message)); | ||
|
||
if(step.from) { | ||
const source = getValidator(schema, step.from); | ||
if(!source && !opts.allowUnknown) return fail(`invalid source ${step.path}`); | ||
|
||
if(source && step.op == 'move') { | ||
const result = Joi.validate(undefined, source, opts); | ||
if(result.error) return fail(result.error.toString()); | ||
} | ||
} | ||
|
||
// TODO: handle specialized details of insert in array (add op with path ending in int or '-') | ||
// (or are there none?) | ||
if(step.op == 'add' && isArrayPath(step.path)) { | ||
|
||
} | ||
|
||
if(rule && step.op == 'remove') { | ||
const result = Joi.validate(undefined, rule, opts); | ||
if(result.error) return fail(result.error.toString()); | ||
} | ||
|
||
// validate and normalize value | ||
if(rule && step.value) { | ||
const result = Joi.validate(step.value, rule, opts); | ||
|
||
clean.value = result.value; | ||
if(result.error) return fail(result.error.toString()); | ||
} | ||
|
||
return pass(); | ||
} | ||
|
||
|
||
/** | ||
* validate | ||
* | ||
* Validates a json patch according to a schema for the document(s) to be patched. | ||
* Also sanitizes the values in the patch the same way Joi would for a document. | ||
* | ||
* Mirrors Joi's validate interface, down to the optional callback yet lack of | ||
* async support. | ||
* | ||
* @param array patch A valid JSON patch array | ||
* @param object schema A Joi validation schema | ||
* @param function cb Optional standard node callback - if excluded {error: *, value: patch} is returned | ||
* @return a data-sanitized version of the json patch | ||
*/ | ||
exports.validate = function(patch, schema, options, cb) { | ||
|
||
// normalize arguments | ||
if(!Array.isArray(patch)) patch = [patch]; | ||
if(!cb && typeof options == 'function') { | ||
cb = options; | ||
options = {}; | ||
} | ||
options = Object.assign({}, defaultOptions, options || {}); | ||
|
||
const sanitized = [], errors = []; | ||
|
||
patch.every(function(step) { | ||
const result = validateStep(schema, options, step); | ||
|
||
if(result.value) sanitized.push(result.value); | ||
|
||
if(result.error) { | ||
errors.push(result.error); | ||
if(options.abortEarly) return false; | ||
} | ||
return true; | ||
}); | ||
|
||
// encapsulate failures in a single error | ||
const err = errors.length == 1 | ||
? errors[0] | ||
: errors.length > 1 | ||
? new ValidationError('patch invalid', {errors: errors}) | ||
: null; | ||
|
||
if(cb) { | ||
cb(err, sanitized); | ||
} else { | ||
return {error: err, value: sanitized}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"name": "joi-validate-patch", | ||
"version": "0.0.1", | ||
"description": "Validator for json patch contents according to joi document schemas", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "mocha --recursive", | ||
"coverage": "istanbul cover --report html _mocha", | ||
"travis": "istanbul cover --report lcovonly _mocha", | ||
"posttravis": "istanbul check-coverage --statements $npm_package_config_min_coverage --branches $npm_package_config_min_coverage --lines $npm_package_config_min_coverage" | ||
}, | ||
"config": { | ||
"min_coverage": 90 | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/evan-king/joi-validate-patch.git" | ||
}, | ||
"keywords": [ | ||
"joi", | ||
"json", | ||
"patch", | ||
"validation" | ||
], | ||
"author": "Evan King", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/evan-king/joi-validate-patch/issues" | ||
}, | ||
"homepage": "https://github.com/evan-king/joi-validate-patch#readme", | ||
"devDependencies": { | ||
"chai": "^3.2.0", | ||
"istanbul": "^0.4.3", | ||
"mocha": "^1.21.4" | ||
}, | ||
"dependencies": { | ||
"fast-json-patch": "^1.1.1", | ||
"joi": "^9.2.0" | ||
} | ||
} |
Oops, something went wrong.