mirror of
https://github.com/atlanticbiomedical/biomedjs.git
synced 2025-07-02 00:47:26 -04:00
409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
![]() |
var validator = require('validator');
|
||
|
var _ = require('lodash');
|
||
|
var Promise = require('bluebird');
|
||
|
|
||
|
// validators and sanitizers not prefixed with is/to
|
||
|
var additionalValidators = ['contains', 'equals', 'matches'];
|
||
|
var additionalSanitizers = ['trim', 'ltrim', 'rtrim', 'escape', 'stripLow', 'whitelist', 'blacklist', 'normalizeEmail'];
|
||
|
|
||
|
/**
|
||
|
* Initializes a chain of validators
|
||
|
*
|
||
|
* @class
|
||
|
* @param {(string|string[])} param path to property to validate
|
||
|
* @param {string} failMsg validation failure message
|
||
|
* @param {Request} req request to attach validation errors
|
||
|
* @param {string} location request property to find value (body, params, query, etc.)
|
||
|
* @param {object} options options containing error formatter
|
||
|
*/
|
||
|
|
||
|
function ValidatorChain(param, failMsg, req, location, options) {
|
||
|
this.errorFormatter = options.errorFormatter;
|
||
|
this.param = param;
|
||
|
this.value = location ? _.get(req[location], param) : undefined;
|
||
|
this.validationErrors = [];
|
||
|
this.failMsg = failMsg;
|
||
|
this.req = req;
|
||
|
this.lastError = null; // used by withMessage to get the values of the last error
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Initializes a sanitizer
|
||
|
*
|
||
|
* @class
|
||
|
* @param {(string|string[])} param path to property to sanitize
|
||
|
* @param {[type]} req request to sanitize
|
||
|
* @param {[type]} location request property to find value
|
||
|
*/
|
||
|
|
||
|
function Sanitizer(param, req, locations) {
|
||
|
this.values = locations.map(function(location) {
|
||
|
return _.get(req[location], param);
|
||
|
});
|
||
|
|
||
|
this.req = req;
|
||
|
this.param = param;
|
||
|
this.locations = locations;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds validation methods to request object via express middleware
|
||
|
*
|
||
|
* @method expressValidator
|
||
|
* @param {object} options
|
||
|
* @return {function} middleware
|
||
|
*/
|
||
|
|
||
|
var expressValidator = function(options) {
|
||
|
options = options || {};
|
||
|
var defaults = {
|
||
|
customValidators: {},
|
||
|
customSanitizers: {},
|
||
|
errorFormatter: function(param, msg, value) {
|
||
|
return {
|
||
|
param: param,
|
||
|
msg: msg,
|
||
|
value: value
|
||
|
};
|
||
|
}
|
||
|
};
|
||
|
|
||
|
_.defaults(options, defaults);
|
||
|
|
||
|
// _.set validators and sanitizers as prototype methods on corresponding chains
|
||
|
_.forEach(validator, function(method, methodName) {
|
||
|
if (methodName.match(/^is/) || _.contains(additionalValidators, methodName)) {
|
||
|
ValidatorChain.prototype[methodName] = makeValidator(methodName, validator);
|
||
|
}
|
||
|
|
||
|
if (methodName.match(/^to/) || _.contains(additionalSanitizers, methodName)) {
|
||
|
Sanitizer.prototype[methodName] = makeSanitizer(methodName, validator);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
ValidatorChain.prototype.notEmpty = function() {
|
||
|
return this.isLength(1);
|
||
|
};
|
||
|
|
||
|
ValidatorChain.prototype.len = function() {
|
||
|
return this.isLength.apply(this, arguments);
|
||
|
};
|
||
|
|
||
|
ValidatorChain.prototype.optional = function(opts) {
|
||
|
opts = opts || {};
|
||
|
// By default, optional checks if the key exists, but the user can pass in
|
||
|
// checkFalsy: true to skip validation if the property is falsy
|
||
|
var defaults = {
|
||
|
checkFalsy: false
|
||
|
};
|
||
|
|
||
|
var options = _.assign(defaults, opts);
|
||
|
|
||
|
if (options.checkFalsy) {
|
||
|
if (!this.value) {
|
||
|
this.skipValidating = true;
|
||
|
}
|
||
|
} else {
|
||
|
if (this.value === undefined) {
|
||
|
this.skipValidating = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
ValidatorChain.prototype.withMessage = function(message) {
|
||
|
if (this.lastError) {
|
||
|
this.validationErrors.pop();
|
||
|
this.req._validationErrors.pop();
|
||
|
var error = formatErrors.call(this, this.lastError.param, message, this.lastError.value);
|
||
|
this.validationErrors.push(error);
|
||
|
this.req._validationErrors.push(error);
|
||
|
this.lastError = null;
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
_.forEach(options.customValidators, function(method, customValidatorName) {
|
||
|
ValidatorChain.prototype[customValidatorName] = makeValidator(customValidatorName, options.customValidators);
|
||
|
});
|
||
|
|
||
|
_.forEach(options.customSanitizers, function(method, customSanitizerName) {
|
||
|
Sanitizer.prototype[customSanitizerName] = makeSanitizer(customSanitizerName, options.customSanitizers);
|
||
|
});
|
||
|
|
||
|
return function(req, res, next) {
|
||
|
var locations = ['body', 'params', 'query'];
|
||
|
|
||
|
req._validationErrors = [];
|
||
|
req._asyncValidationErrors = [];
|
||
|
req.validationErrors = function(mapped, promisesResolved) {
|
||
|
if (!promisesResolved && req._asyncValidationErrors.length > 0) {
|
||
|
console.warn('WARNING: You have asynchronous validators but you have not used asyncValidateErrors to check for errors.');
|
||
|
}
|
||
|
|
||
|
if (mapped && req._validationErrors.length > 0) {
|
||
|
var errors = {};
|
||
|
req._validationErrors.forEach(function(err) {
|
||
|
errors[err.param] = err;
|
||
|
});
|
||
|
|
||
|
return errors;
|
||
|
}
|
||
|
|
||
|
return req._validationErrors.length > 0 ? req._validationErrors : false;
|
||
|
};
|
||
|
|
||
|
req.asyncValidationErrors = function(mapped) {
|
||
|
return new Promise(function(resolve, reject) {
|
||
|
var promises = req._asyncValidationErrors;
|
||
|
Promise.settle(promises).then(function(results) {
|
||
|
|
||
|
results.forEach(function(result) {
|
||
|
if (result.isRejected()) { req._validationErrors.push(result.reason()); }
|
||
|
});
|
||
|
|
||
|
if (req._validationErrors.length > 0) {
|
||
|
return reject(req.validationErrors(mapped, true));
|
||
|
}
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
|
||
|
locations.forEach(function(location) {
|
||
|
req['sanitize' + _.capitalize(location)] = function(param) {
|
||
|
return new Sanitizer(param, req, [location]);
|
||
|
};
|
||
|
});
|
||
|
|
||
|
req.sanitizeHeaders = function(param) {
|
||
|
if (param === 'referrer') {
|
||
|
param = 'referer';
|
||
|
}
|
||
|
|
||
|
return new Sanitizer(param, req, ['headers']);
|
||
|
};
|
||
|
|
||
|
req.sanitize = function(param) {
|
||
|
return new Sanitizer(param, req, locations);
|
||
|
};
|
||
|
|
||
|
locations.forEach(function(location) {
|
||
|
req['check' + _.capitalize(location)] = function(param, failMsg) {
|
||
|
if (_.isPlainObject(param)) {
|
||
|
return validateSchema(param, req, location, options);
|
||
|
}
|
||
|
return new ValidatorChain(param, failMsg, req, location, options);
|
||
|
};
|
||
|
});
|
||
|
|
||
|
req.checkFiles = function(param, failMsg) {
|
||
|
return new ValidatorChain(param, failMsg, req, 'files', options);
|
||
|
};
|
||
|
|
||
|
req.checkHeaders = function(param, failMsg) {
|
||
|
if (param === 'referrer') {
|
||
|
param = 'referer';
|
||
|
}
|
||
|
|
||
|
return new ValidatorChain(param, failMsg, req, 'headers', options);
|
||
|
};
|
||
|
|
||
|
req.check = function(param, failMsg) {
|
||
|
if (_.isPlainObject(param)) {
|
||
|
return validateSchema(param, req, 'any', options);
|
||
|
}
|
||
|
return new ValidatorChain(param, failMsg, req, locate(req, param), options);
|
||
|
};
|
||
|
|
||
|
req.filter = req.sanitize;
|
||
|
req.assert = req.check;
|
||
|
req.validate = req.check;
|
||
|
|
||
|
next();
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* validate an object using a schema, using following format:
|
||
|
*
|
||
|
* {
|
||
|
* paramName: {
|
||
|
* validatorName: true,
|
||
|
* validator2Name: true
|
||
|
* }
|
||
|
* }
|
||
|
*
|
||
|
* Pass options or a custom error message:
|
||
|
*
|
||
|
* {
|
||
|
* paramName: {
|
||
|
* validatorName: {
|
||
|
* options: ['', ''],
|
||
|
* errorMessage: 'An Error Message'
|
||
|
* }
|
||
|
* }
|
||
|
* }
|
||
|
*
|
||
|
* @method validateSchema
|
||
|
* @param {Object} schema schema of validations
|
||
|
* @param {Request} req request to attach validation errors
|
||
|
* @param {string} location request property to find value (body, params, query, etc.)
|
||
|
* @param {Object} options options containing custom validators & errorFormatter
|
||
|
* @return {object[]} array of errors
|
||
|
*/
|
||
|
|
||
|
function validateSchema(schema, req, loc, options) {
|
||
|
for (var param in schema) {
|
||
|
loc = loc === 'any' ? locate(req, param) : loc;
|
||
|
var validator = new ValidatorChain(param, null, req, loc, options);
|
||
|
var paramErrorMessage = schema[param].errorMessage;
|
||
|
delete schema[param].errorMessage;
|
||
|
|
||
|
for (var methodName in schema[param]) {
|
||
|
validator.failMsg = schema[param][methodName].errorMessage || paramErrorMessage || 'Invalid param';
|
||
|
validator[methodName].apply(validator, schema[param][methodName].options);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates and handles errors, return instance of itself to allow for chaining
|
||
|
*
|
||
|
* @method makeValidator
|
||
|
* @param {string} methodName
|
||
|
* @param {object} container
|
||
|
* @return {function}
|
||
|
*/
|
||
|
|
||
|
function makeValidator(methodName, container) {
|
||
|
return function() {
|
||
|
if (this.skipValidating) {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
var args = [];
|
||
|
args.push(this.value);
|
||
|
args = args.concat(Array.prototype.slice.call(arguments));
|
||
|
|
||
|
var isValid = container[methodName].apply(container, args);
|
||
|
var error = formatErrors.call(this, this.param, this.failMsg || 'Invalid value', this.value);
|
||
|
|
||
|
if (isValid.then) {
|
||
|
var promise = isValid.catch(function() {
|
||
|
return Promise.reject(error);
|
||
|
});
|
||
|
this.req._asyncValidationErrors.push(promise);
|
||
|
}
|
||
|
|
||
|
if (!isValid) {
|
||
|
this.validationErrors.push(error);
|
||
|
this.req._validationErrors.push(error);
|
||
|
this.lastError = { param: this.param, value: this.value };
|
||
|
} else {
|
||
|
this.lastError = null;
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sanitizes and sets sanitized value on the request, then return instance of itself to allow for chaining
|
||
|
*
|
||
|
* @method makeSanitizer
|
||
|
* @param {string} methodName
|
||
|
* @param {object} container
|
||
|
* @return {function}
|
||
|
*/
|
||
|
|
||
|
function makeSanitizer(methodName, container) {
|
||
|
return function() {
|
||
|
var _arguments = arguments;
|
||
|
var result;
|
||
|
this.values.forEach(function(value, i) {
|
||
|
if (value != null) {
|
||
|
var args = [value];
|
||
|
args = args.concat(Array.prototype.slice.call(_arguments));
|
||
|
result = container[methodName].apply(container, args);
|
||
|
|
||
|
_.set(this.req[this.locations[i]], this.param, result);
|
||
|
this.values[i] = result;
|
||
|
}
|
||
|
}.bind(this));
|
||
|
|
||
|
return result;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* find location of param
|
||
|
*
|
||
|
* @method param
|
||
|
* @param {Request} req express request object
|
||
|
* @param {(string|string[])} name [description]
|
||
|
* @return {string}
|
||
|
*/
|
||
|
|
||
|
function locate(req, name) {
|
||
|
if (_.get(req.params, name)) {
|
||
|
return 'params';
|
||
|
} else if (_.has(req.query, name)) {
|
||
|
return 'query';
|
||
|
} else if (_.has(req.body, name)) {
|
||
|
return 'body';
|
||
|
}
|
||
|
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* format param output if passed in as array (for nested)
|
||
|
* before calling errorFormatter
|
||
|
*
|
||
|
* @method param
|
||
|
* @param {(string|string[])} param parameter as a string or array
|
||
|
* @param {string} msg
|
||
|
* @param {string} value
|
||
|
* @return {function}
|
||
|
*/
|
||
|
function formatErrors(param, msg, value) {
|
||
|
var formattedParam = formatParamOutput(param);
|
||
|
|
||
|
return this.errorFormatter(formattedParam, msg, value);
|
||
|
}
|
||
|
|
||
|
// Convert nested params as array into string for output
|
||
|
// Ex: ['users', '0', 'fields', 'email'] to 'users[0].fields.email'
|
||
|
function formatParamOutput(param) {
|
||
|
if (Array.isArray(param)) {
|
||
|
param = param.reduce(function(prev, curr) {
|
||
|
var part = '';
|
||
|
if (validator.isInt(curr)) {
|
||
|
part = '[' + curr + ']';
|
||
|
} else {
|
||
|
if (prev) {
|
||
|
part = '.' + curr;
|
||
|
} else {
|
||
|
part = curr;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return prev + part;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return param;
|
||
|
}
|
||
|
|
||
|
module.exports = expressValidator;
|
||
|
module.exports.validator = validator;
|
||
|
module.exports.utils = {
|
||
|
formatParamOutput: formatParamOutput
|
||
|
};
|