Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Evan King committed Nov 7, 2016
0 parents commit 0f6c3a6
Show file tree
Hide file tree
Showing 9 changed files with 575 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
coverage/
node_modules/
dist/
npm-debug.log
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test/
coverage/
.travis.yml
.npmignore
11 changes: 11 additions & 0 deletions .travis.yml
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
21 changes: 21 additions & 0 deletions LICENSE
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.
113 changes: 113 additions & 0 deletions README.md
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
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

module.exports = require('./lib/joi-validate-patch');
175 changes: 175 additions & 0 deletions lib/joi-validate-patch.js
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};
}
}
40 changes: 40 additions & 0 deletions package.json
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"
}
}

0 comments on commit 0f6c3a6

Please sign in to comment.