Skip to content

Commit

Permalink
Merge pull request #19 from feathers-plus/validate-context
Browse files Browse the repository at this point in the history
🎁 Allow validating anything in context
  • Loading branch information
marshallswain authored Feb 13, 2020
2 parents 49f8d41 + 39dc934 commit 15cbefc
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 78 deletions.
145 changes: 79 additions & 66 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,79 @@
/* eslint comma-dangle: 0, object-shorthand: 0, prefer-arrow-callback: 0*/ /* ES5 code */
const errors = require('@feathersjs/errors');
const utils = require('feathers-hooks-common/lib/services');
const joiErrorsForForms = require('joi-errors-for-forms');

// 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
const joiDefaults = {
abortEarly: true,
allowUnknown: false,
cache: true,
convert: true,
debug: false,
externals: true,
noDefaults: false,
nonEnumerables: false,
presence: 'optional',
skipFunctions: false,
stripUnknown: false
};

function setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest) {
if (!['undefined', 'object'].includes(typeof joiOptions)) {
throw new errors.GeneralError('joiOptions must be a valid object.');
}

const mergedOptions = Object.assign({}, joiDefaults, joiOptions);

return async function validateWithJoi(context, next) {
utils.checkContext(context, 'before', ['create', 'update', 'patch'], 'validate-joi');
const values = utils.getItems(context);


try {
const convertedValues = await joiSchema.validateAsync(values, mergedOptions);

if (mergedOptions.convert === true) {
utils.replaceItems(context, convertedValues);
}

if (typeof next === 'function') {
return next(null, context);
}
return context;
} catch (error) {
const formErrors = translator(error);
if (formErrors) {
// Hacky, but how else without a custom assert?
const msg = ifTest ? JSON.stringify(formErrors) : 'Invalid data';
throw new errors.BadRequest(msg, { errors: formErrors });
}
return formErrors || error;
}
};
}

module.exports = {
form: function (joiSchema, joiOptions, translations, ifTest) {
const translator = joiErrorsForForms.form(translations);
return setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest);
},
mongoose: function (joiSchema, joiOptions, translations, ifTest) {
const translator = joiErrorsForForms.mongoose(translations);
return setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest);
}
};
/* eslint comma-dangle: 0, object-shorthand: 0, prefer-arrow-callback: 0 */
const errors = require('@feathersjs/errors');
const utils = require('feathers-hooks-common/lib/services');
const joiErrorsForForms = require('joi-errors-for-forms');

// 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
const joiDefaults = {
abortEarly: true,
allowUnknown: false,
cache: true,
convert: true,
debug: false,
externals: true,
noDefaults: false,
nonEnumerables: false,
presence: 'optional',
skipFunctions: false,
stripUnknown: false,
getContext: undefined,
setContext: undefined
};

function setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest) {
if (!['undefined', 'object'].includes(typeof joiOptions)) {
throw new errors.GeneralError('joiOptions must be a valid object.');
}

const { getContext, setContext, ...mergedOptions } = { ...joiDefaults, ...joiOptions };

if ((getContext || setContext) && (!getContext || !setContext)) {
throw new errors.GeneralError('getContext and setContext must be used together');
}

return async function validateWithJoi(context, next) {
let values;
if (typeof getContext === 'function') {
values = getContext(context);
} else {
values = utils.getItems(context);
}

try {
const convertedValues = await joiSchema.validateAsync(values, mergedOptions);

if (mergedOptions.convert === true) {
if (typeof setContext === 'function') {
setContext(context, convertedValues);
} else {
utils.replaceItems(context, convertedValues);
}
}

if (typeof next === 'function') {
return next(null, context);
}
return context;
} catch (error) {
const formErrors = translator(error);
if (formErrors) {
// Hacky, but how else without a custom assert?
const msg = ifTest ? JSON.stringify(formErrors) : 'Invalid data';
throw new errors.BadRequest(msg, { errors: formErrors });
}
return formErrors || error;
}
};
}

module.exports = {
form: function (joiSchema, joiOptions, translations, ifTest) {
const translator = joiErrorsForForms.form(translations);
return setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest);
},
mongoose: function (joiSchema, joiOptions, translations, ifTest) {
const translator = joiErrorsForForms.mongoose(translations);
return setupValidateWithJoi(joiSchema, joiOptions, translator, ifTest);
}
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.18.3",
"istanbul": "^0.4.5",
"mocha": "^7.0.1"
"mocha": "^7.0.1",
"mongodb": "^3.5.3"
},
"engines": {
"node": ">4.2.4"
Expand Down
11 changes: 7 additions & 4 deletions test/invalid_spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/* eslint-disable no-template-curly-in-string */

/* eslint comma-dangle:0, newline-per-chained-call: 0, no-shadow: 0,
object-shorthand: 0, one-var: 0, one-var-declaration-per-line: 0,
prefer-arrow-callback: 0 */ /* ES5 code */

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

const Joi = require('@hapi/joi');
const name = Joi.string().trim().regex(/^[\sa-zA-Z0-9]{5,30}$/).required();
const password = Joi.string().trim().min(2).max(30).required();
const schema = Joi.object().keys({
Expand Down Expand Up @@ -169,10 +170,12 @@ describe('invalid data - form UI', () => {

it('throws on error. translate using substrings', async () => {
const translations = [
{ regex: 'at least 2 characters long',
{
regex: 'at least 2 characters long',
message: '"${key}" must be 2 or more chars.'
},
{ regex: /required pattern/,
{
regex: /required pattern/,
message: '"${key}" is badly formed.'
}
];
Expand Down
68 changes: 66 additions & 2 deletions test/valid_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
/* eslint newline-per-chained-call: 0, no-shadow: 0, one-var: 0,
one-var-declaration-per-line: 0, prefer-arrow-callback: 0 */ /* ES5 code */

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

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

const { ObjectID } = require('mongodb');
/**
* Custom objectId validator
*/
function objectId() {
return Joi.custom((value, helpers) => {
if (value === null || value === undefined) {
return value;
}
try {
return new ObjectID(value);
} catch (error) {
const errVal = helpers.error('any.invalid');
errVal.message = `"${errVal.path.join('.')}" objectId validation failed because ${error.message}`;
return errVal;
}
}, 'objectId');
}

describe('custom context with getContext', async () => {
const schema = Joi.object({
userId: objectId(),
});

it('allows validating params.query', async () => {
const joiOptions = {
convert: true,
getContext(context) {
return context.params.query;
},
setContext(context, newValues) {
Object.assign(context.params.query, newValues);
},
};
const context = {
params: {
query: {
userId: '5e44b40855534a38798ba1aa',
},
},
};
try {
const validateWithJoi = validate.form(schema, joiOptions);
const responseContext = await validateWithJoi(context);
assert(responseContext.params.query.userId instanceof ObjectID, 'params.query.userId should be converted to an objectId');
} catch (error) {
assert(!error, 'should not have failed');
}
});

it('throws if getContext is used without setContext', async () => {
const joiOptions = {
getContext() {},
};
try {
const validateWithJoi = validate.form(schema, joiOptions);
const responseContext = await validateWithJoi(context);
assert(!responseContext, 'should have failed');
} catch (error) {
assert.equal(error.message, 'getContext and setContext must be used together');
}
});
});

describe('valid values', () => {
var joiOptions, values, converted, context; // eslint-disable-line no-var

Expand Down
Loading

0 comments on commit 15cbefc

Please sign in to comment.