Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADD z-schema validator plugin #1157

Merged
merged 6 commits into from
Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog

### comming soon
### coming soon

Features:
- Add a [z-schema](https://github.com/zaggino/z-schema) validator plugin

Typings:
- ADD typings to access the `PouchSyncHandler` of `RxReplicationState`
Expand Down
12 changes: 12 additions & 0 deletions docs-src/custom-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ RxDB.plugin(RxDBAjvValidateModule);
RxDB.plugin(require('rxdb/plugins/ajv-validate'));
```

### validate-z-schema

Both `is-my-json-valid` and `ajv-validate` use `eval()` to perform validation which might not be wanted when `'unsafe-eval'` is not allowed in Content Security Policies. This one is using [z-schema](https://github.com/zaggino/z-schema) as validator which doesn't use `eval`.

```javascript
// es6-import
import RxDBZSchemaValidateModule from 'rxdb/plugins/validate-z-schema';
RxDB.plugin(RxDBZSchemaValidateModule);

// es5-require
RxDB.plugin(require('rxdb/plugins/validate-z-schema'));
```

### no-validate

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@
"spark-md5": "^3.0.0",
"unload": "2.1.0",
"url": "^0.11.0",
"util": "^0.12.0"
"util": "^0.12.0",
"z-schema": "^4.0.2"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that 'ajv' is not listed here, are users supposed to install themselves? if so I guess I should remove z-schema from here

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ajv is missing. Z-schema schould stay there. Im offline since monday btw, i can help then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what version of ajv should I add?

},
"devDependencies": {
"@babel/cli": "7.4.4",
Expand Down
2 changes: 2 additions & 0 deletions plugins/validate-z-schema/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare let _default: {};
export default _default;
6 changes: 6 additions & 0 deletions plugins/validate-z-schema/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "rxdb-plugin-validate-z-schema",
"main": "../../dist/lib/plugins/validate-z-schema.js",
"jsnext:main": "../../dist/es/plugins/validate-z-schema.js",
"module": "../../dist/es/plugins/validate-z-schema.js"
}
103 changes: 103 additions & 0 deletions src/plugins/validate-z-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* this plugin validates documents before they can be inserted into the RxCollection.
* It's using z-schema as jsonschema-validator
* @link https://github.com/zaggino/z-schema
*/
import ZSchema from 'z-schema';
import {
newRxError
} from '../rx-error';
import {
requestIdleCallbackIfAvailable
} from '../util';

/**
* cache the validators by the schema-hash
* so we can reuse them when multiple collections have the same schema
* @type {Object<string, any>}
*/
const validatorsCache = {};


/**
* returns the parsed validator from z-schema
* @param {string} [schemaPath=''] if given, the schema for the sub-path is used
* @
*/
function _getValidator(rxSchema, schemaPath = '') {
const hash = rxSchema.hash;
if (!validatorsCache[hash]) validatorsCache[hash] = {};
const validatorsOfHash = validatorsCache[hash];

if (!validatorsOfHash[schemaPath]) {
const schemaPart = schemaPath === '' ? rxSchema.jsonID : rxSchema.getSchemaByObjectPath(schemaPath);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand where does the getSchemaByObjectPath comes from? @pubkey mmmm


if (!schemaPart) {
throw newRxError('VD1', {
schemaPath: schemaPath
});
}

const validator = new ZSchema();
validatorsOfHash[schemaPath] = (obj) => {
validator.validate(obj, schemaPart);
return validator;
};
}

return validatorsOfHash[schemaPath];
}

/**
* validates the given object against the schema
* @param {any} obj
* @param {String} [schemaPath=''] if given, the sub-schema will be validated
* @throws {RxError} if not valid
* @return {any} obj if validation successful
*/
const validate = function (obj, schemaPath = '') {
const validator = _getValidator(this, schemaPath);
const useValidator = validator(obj);
/** @type {ZSchema.SchemaErrorDetail[]} */
const errors = useValidator.getLastErrors();
if (!errors) return obj;
else {
const formattedZSchemaErrors = errors.map(({ title, description, message }) => ({
title,
description,
message
}));
throw newRxError('VD2', {
errors: formattedZSchemaErrors,
schemaPath,
obj,
schema: this.jsonID
});
}
};

const runAfterSchemaCreated = rxSchema => {
// pre-generate the validator-z-schema from the schema
requestIdleCallbackIfAvailable(() => _getValidator.bind(rxSchema, rxSchema));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why but I had to .bind here... otherwise the test would work, but in the browser it wouldn't.. if you have a better way to handle this let me know

};

export const rxdb = true;
export const prototypes = {
/**
* set validate-function for the RxSchema.prototype
* @param {[type]} prototype of RxSchema
*/
RxSchema: (proto) => {
proto._getValidator = _getValidator;
proto.validate = validate;
}
};
export const hooks = {
createRxSchema: runAfterSchemaCreated
};

export default {
rxdb,
prototypes,
hooks
};
29 changes: 29 additions & 0 deletions test/unit/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,35 @@ config.parallel('plugin.test.js', () => {
}
});
});
describe('validate-z-schema.node.js', () => {
it('should allow everything', async () => {
if (!config.platform.isNode())
return;

const spawn = REQUIRE_FUN('child-process-promise').spawn;
const stdout = [];
const stderr = [];
const promise = spawn('mocha', [config.rootPath + 'test_tmp/unit/validate-z-schema.node.js']);
const childProcess = promise.childProcess;
childProcess.stdout.on('data', data => {
// comment in to debug
// console.log(':: ' + data.toString());
stdout.push(data.toString());
});
childProcess.stderr.on('data', data => stderr.push(data.toString()));
try {
await promise;
} catch (err) {
console.log('errrrr');
console.dir(stdout);
throw new Error(`could not run validate-z-schema.node.js.
# Error: ${err}
# Output: ${stdout}
# ErrOut: ${stderr}
`);
}
});
});
describe('no-validate.node.js', () => {
it('should allow everything', async () => {
if (!config.platform.isNode())
Expand Down
81 changes: 81 additions & 0 deletions test/unit/validate-z-schema.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import assert from 'assert';
import AsyncTestUtil from 'async-test-util';
import config from './config';

import * as schemaObjects from '../helper/schema-objects';
import * as schemas from '../helper/schemas';

import * as util from '../../dist/lib/util';
import Core from '../../dist/lib/core';
Core.plugin(require('../../plugins/validate-z-schema'));
Core.plugin(require('../../plugins/key-compression'));
Core.plugin(require('../../plugins/error-messages'));
Core.plugin(require('pouchdb-adapter-memory'));

config.parallel('validate-z-schema.node.js', () => {
describe('validation', () => {
describe('positive', () => {
it('should not throw', async () => {
const db = await Core.create({
name: util.randomCouchString(10),
adapter: 'memory'
});
const col = await db.collection({
name: 'humans',
schema: schemas.human
});

const doc = await col.insert(schemaObjects.human());
assert.ok(doc);

db.destroy();
});
});
describe('negative', () => {
it('should not validate wrong data', async () => {
const db = await Core.create({
name: util.randomCouchString(10),
adapter: 'memory'
});
const col = await db.collection({
name: 'humans',
schema: schemas.human
});

await AsyncTestUtil.assertThrows(
() => col.insert({
foo: 'bar'
}),
'RxError'
);

db.destroy();
});
it('should have the correct params in error', async () => {
const db = await Core.create({
name: util.randomCouchString(10),
adapter: 'memory'
});
const col = await db.collection({
name: 'humans',
schema: schemas.human
});

let error = null;
try {
await col.insert({
foo: 'bar'
});
} catch (e) {
error = e;
}

console.log(error);

assert.ok(error);
assert.ok(error.parameters.errors.length > 0);
db.destroy();
});
});
});
});