Skip to content

Commit

Permalink
Add the validateProvidedData hook
Browse files Browse the repository at this point in the history
See the readme for details
  • Loading branch information
marshallswain committed Apr 14, 2020
1 parent 9d2268f commit c71c40e
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 9 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and optionally translated for clarity or internationalization.
[![Build Status](https://travis-ci.org/feathers-plus/validate-joi.svg?branch=master)](https://travis-ci.org/feathers-plus/validate-joi)
[![Coverage Status](https://coveralls.io/repos/github/feathers-plus/validate-joi/badge.svg?branch=master)](https://coveralls.io/github/feathers-plus/validate-joi?branch=master)

## New in Version 3.2

Version 3.2 adds the `validateProvidedData` hook, which can be very useful in validating patch requests.

## New in Version 3.1

- 🙌 Updated to work with latest `@hapi/joi`.
Expand All @@ -16,6 +20,8 @@ and optionally translated for clarity or internationalization.

Since `Joi.validate()` has been removed, all validations now use `schema.validateAsync()`, which means this package now supports asynchronous validations.

If you're using MongoDB, be sure to take a look at [@feathers-plus/validate-joi-mongodb](https://github.com/feathers-plus/validate-joi-mongodb) for some time-saving utilities.

## Installation

```
Expand Down Expand Up @@ -114,6 +120,44 @@ export.before = {
};
```

## validateProvidedData Hook

The `validateProvidedData` hook is just like `validate.form`, but it only validates the attributes from the schema which are actually present in the request's `data` object. In short, it allows partial validation of the schema attributes. Using it as a hook looks like this:

```js
const validate = require('@featehrs-plus/validate-joi')
const attrs = require('./faqs.model')

const hooks = {
before: {
patch: [
validate.validateProvidedData(attrs, { abortEarly: false })
]
}
}
```

The above example supposes that you have an `/faqs` service with a model that looks like the following. Notice how the `attrs` are defined as a separate object, then they are used in the schema and made available in the export. The `validateProvidedData` hook uses the individual attrs to validate each individual item in the request's `data` object.

```js
// src/services/faqs/faqs.model.js
const Joi = require('@hapi/joi')
const { objectId } = require('@feathers-plus/validate-joi-mongodb')

const attrs = {
_id: objectId(),
question: Joi.string().disallow(null).required(),
answer: Joi.string().disallow(null).required(),
isPublic: Joi.boolean().default(false),
createdBy: objectId().disallow(null).required()
}

module.exports = {
attrs,
schema: Joi.object(attrs)
}
```

## Motivation

Data must be validated and sanitized before the database is changed.
Expand Down
34 changes: 33 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
const errors = require('@feathersjs/errors');
const utils = require('feathers-hooks-common/lib/services');
const joiErrorsForForms = require('joi-errors-for-forms');
const Joi = require('@hapi/joi');
const pick = require('lodash/pick');

// We only directly need the convert option. The others are listed for convenience.
// See defaults at https://hapi.dev/family/joi/api/?v=17.1.0#anyvalidatevalue-options
Expand Down Expand Up @@ -67,7 +69,7 @@ function setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest) {
};
}

module.exports = {
const validators = {
form: function (joiSchema, joiOptions, translations, ifTest) {
const translator = joiErrorsForForms.form(translations);
return setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest);
Expand All @@ -77,3 +79,33 @@ module.exports = {
return setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest);
}
};

/**
* The validatedProvidedAttrs hook is great for validating patch requests, where a partial
* schema needs to be validated. It only validates the attributes that have matching keys
* in `context.data`.
* @param {Object} validationsObj - an object containing the raw keys from a service's
* schema object. It cannot be already wrapped in `Joi.object(validationsObject)`.
* @param {JoiOptionsObject} joiOptions
*/
function setupValidateProvidedData(validationsObj, joiOptions) {
if (!validationsObj || typeof validationsObj !== 'object') {
throw new Error('The `validationsObj` argument is required.');
}
return function validatedProvidedData(context) {
if (context.type === 'after') {
throw new Error('validateProvidedData can only be a before hook');
}
const patchAttrs = pick(validationsObj, Object.keys(context.data));
const patchSchema = Joi.object(patchAttrs);

const validateHook = validators.form(patchSchema, joiOptions);

return validateHook(context);
};
}

Object.assign(validators, { validateProvidedData: setupValidateProvidedData });


module.exports = validators;
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
],
"author": "John Szwaronek <[email protected]>",
"contributors": [
"Luis A. Salas <[email protected]>"
"Luis A. Salas <[email protected]>",
"Marshall Thompson <[email protected]>"
],
"license": "MIT",
"bugs": {
Expand All @@ -42,7 +43,8 @@
"@feathersjs/errors": "^4.5.1",
"@hapi/joi": "^17.1.0",
"feathers-hooks-common": "^5.0.2",
"joi-errors-for-forms": "^0.2.2"
"joi-errors-for-forms": "^0.2.2",
"lodash": "^4.17.15"
},
"devDependencies": {
"babel-eslint": "^10.0.3",
Expand All @@ -55,7 +57,7 @@
"eslint-plugin-react": "7.18.3",
"istanbul": "^0.4.5",
"mocha": "^7.0.1",
"mongodb": "^3.5.3"
"mongodb": "^3.5.5"
},
"engines": {
"node": ">4.2.4"
Expand Down
2 changes: 1 addition & 1 deletion test/valid_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ one-var-declaration-per-line: 0, prefer-arrow-callback: 0 */ /* ES5 code */

const { assert } = require('chai');
const Joi = require('@hapi/joi');
const { ObjectID } = require('mongodb');
const validate = require('../index');

const name = Joi.string().trim().regex(/^[\sa-zA-Z0-9]{5,30}$/).required();
Expand All @@ -14,7 +15,6 @@ const schema = Joi.object().keys({
confirmPassword: password.label('Confirm password'),
});

const { ObjectID } = require('mongodb');
/**
* Custom objectId validator
*/
Expand Down
76 changes: 76 additions & 0 deletions test/validate-provided-data.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const assert = require('assert');
const Joi = require('@hapi/joi');
const { validateProvidedData: setupValidate } = require('../index');

describe('validate-provided-data hook', () => {
it('throws an error if no valdationAttrs are provided', async () => {
try {
setupValidate();
} catch (error) {
assert.equal(error.message, 'The `validationsObj` argument is required.');
}
});

it('throws an error if used as an after hook', async () => {
const attrs = {
name: Joi.string().required(),
email: Joi.string().required(),
};
const context = {
type: 'after',
};

try {
const validate = setupValidate(attrs);
const responseContext = await validate(context);
assert(!responseContext, 'should have failed when used as an after hook');
} catch (error) {
assert.equal(error.message, 'validateProvidedData can only be a before hook',
'should have been able to validate');
}
});

it('validates only the attributes in context.data', async () => {
const attrs = {
name: Joi.string().required(),
email: Joi.string().required(),
};
const context = {
type: 'before',
data: {
name: 'Marshall',
},
};

try {
const validate = setupValidate(attrs);
const responseContext = await validate(context);
assert(responseContext);
} catch (error) {
assert(!error, 'should have been able to validate');
}
});

it('can fail if validations do not match', async () => {
const attrs = {
name: Joi.string().required(),
email: Joi.string().required(),
};
const context = {
type: 'before',
data: {
name: 'Marshall',
email: 200, // this should fail
},
};

try {
const validate = setupValidate(attrs);
const options = { convert: true, abortEarly: false, stripUnknown: true };
const responseContext = await validate(context, options);
assert(!responseContext, 'should have failed email validation');
} catch ({ errors }) {
assert.equal(errors.email, '"email" must be a string', 'validation should have failed for bad email');
}
});
});
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1592,10 +1592,10 @@ mocha@^7.0.1:
yargs-parser "13.1.1"
yargs-unparser "1.6.0"

mongodb@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.3.tgz#f2c7ce9b5fc9a13da116ff1b6e816f6256010a86"
integrity sha512-II7P7A3XUdPiXRgcN96qIoRa1oesM6qLNZkzfPluNZjVkgQk3jnQwOT6/uDk4USRDTTLjNFw2vwfmbRGTA7msg==
mongodb@^3.5.5:
version "3.5.5"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.5.tgz#1334c3e5a384469ac7ef0dea69d59acc829a496a"
integrity sha512-GCjDxR3UOltDq00Zcpzql6dQo1sVry60OXJY3TDmFc2SWFY6c8Gn1Ardidc5jDirvJrx2GC3knGOImKphbSL3A==
dependencies:
bl "^2.2.0"
bson "^1.1.1"
Expand Down

0 comments on commit c71c40e

Please sign in to comment.