anchor
Version:
High-level validation library for Node.js (used in Waterline)
384 lines (351 loc) • 15.1 kB
JavaScript
/**
* Module dependencies
*/
var util = require('util');
var _ = require('@sailshq/lodash');
var validator = require('validator');
/**
* Type rules
*/
var rules = {
// ┬┌─┐┌┐┌┌─┐┬─┐┌─┐ ┌┐┌┬ ┬┬ ┬
// ││ ┬││││ │├┬┘├┤ ││││ ││ │
// ┴└─┘┘└┘└─┘┴└─└─┘ ┘└┘└─┘┴─┘┴─┘
'isBoolean': {
fn: function(x) {
return typeof x === 'boolean';
},
defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not a boolean.'; },
expectedTypes: ['json', 'ref']
},
'isNotEmptyString': {
fn: function(x) {
return x !== '';
},
defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was an empty string.'; },
expectedTypes: ['json', 'ref', 'string']
},
'isInteger': {
fn: function(x) {
return typeof x === 'number' && (parseInt(x) === x);
},
defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not an integer.'; },
expectedTypes: ['json', 'ref', 'number']
},
'isNumber': {
fn: function(x) {
return typeof x === 'number';
},
defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not a number.'; },
expectedTypes: ['json', 'ref']
},
'isString': {
fn: function(x) {
return typeof x === 'string';
},
defaultErrorMessage: function(x) { return 'Value ('+util.inspect(x)+') was not a string.'; },
expectedTypes: ['json', 'ref']
},
'max': {
fn: function(x, maximum) {
if (typeof x !== 'number') { throw new Error ('Value was not a number.'); }
return x <= maximum;
},
defaultErrorMessage: function(x, maximum) { return 'Value ('+util.inspect(x)+') was greater than the configured maximum (' + maximum + ')'; },
expectedTypes: ['json', 'ref', 'number'],
checkConfig: function(constraint) {
if (typeof constraint !== 'number') {
return 'Maximum must be specified as a number; instead got `' + util.inspect(constraint) + '`.';
}
return false;
}
},
'min': {
fn: function(x, minimum) {
if (typeof x !== 'number') { throw new Error ('Value was not a number.'); }
return x >= minimum;
},
defaultErrorMessage: function(x, minimum) { return 'Value ('+util.inspect(x)+') was less than the configured minimum (' + minimum + ')'; },
expectedTypes: ['json', 'ref', 'number'],
checkConfig: function(constraint) {
if (typeof constraint !== 'number') {
return 'Minimum must be specified as a number; instead got `' + util.inspect(constraint) + '`.';
}
return false;
}
},
// ┬┌─┐┌┐┌┌─┐┬─┐┌─┐ ┌┐┌┬ ┬┬ ┬ ┌─┐┌┐┌┌┬┐ ┌─┐┌┬┐┌─┐┌┬┐┬ ┬ ┌─┐┌┬┐┬─┐┬┌┐┌┌─┐
// ││ ┬││││ │├┬┘├┤ ││││ ││ │ ├─┤│││ ││ ├┤ │││├─┘ │ └┬┘ └─┐ │ ├┬┘│││││ ┬
// ┴└─┘┘└┘└─┘┴└─└─┘ ┘└┘└─┘┴─┘┴─┘ ┴ ┴┘└┘─┴┘ └─┘┴ ┴┴ ┴ ┴ └─┘ ┴ ┴└─┴┘└┘└─┘
'isAfter': {
fn: function(x, constraint) {
var normalizedX;
if (_.isNumber(x)) {
normalizedX = new Date(x).getTime();
} else if (_.isDate(x)) {
normalizedX = x.getTime();
} else {
normalizedX = Date.parse(x);
}
var normalizedConstraint;
if (_.isNumber(constraint)) {
normalizedConstraint = new Date(constraint).getTime();
} else if (_.isDate(constraint)) {
normalizedConstraint = constraint.getTime();
} else {
normalizedConstraint = Date.parse(constraint);
}
return normalizedX > normalizedConstraint;
},
expectedTypes: ['json', 'ref', 'string', 'number'],
defaultErrorMessage: function(x, constraint) { return 'Value ('+util.inspect(x)+') was before the configured time (' + constraint + ')'; },
ignoreEmptyString: true,
checkConfig: function(constraint) {
var isValidConstraint = (_.isNumber(constraint) || _.isDate(constraint) || (_.isString(constraint) && _.isNull(validator.toDate(constraint))));
if (!isValidConstraint) {
return 'Validation rule must be specified as a JS timestamp (number of ms since epoch), a natively-parseable date string, or a JavaScript Date instance; instead got `' + util.inspect(constraint) + '`.';
} else {
return false;
}
}
},
'isBefore': {
fn: function(x, constraint) {
var normalizedX;
if (_.isNumber(x)) {
normalizedX = new Date(x).getTime();
} else if (_.isDate(x)) {
normalizedX = x.getTime();
} else {
normalizedX = Date.parse(x);
}
var normalizedConstraint;
if (_.isNumber(constraint)) {
normalizedConstraint = new Date(constraint).getTime();
} else if (_.isDate(constraint)) {
normalizedConstraint = constraint.getTime();
} else {
normalizedConstraint = Date.parse(constraint);
}
return normalizedX < normalizedConstraint;
},
expectedTypes: ['json', 'ref', 'string', 'number'],
defaultErrorMessage: function(x, constraint) { return 'Value ('+util.inspect(x)+') was after the configured time (' + constraint + ')'; },
ignoreEmptyString: true,
checkConfig: function(constraint) {
var isValidConstraint = (_.isNumber(constraint) || _.isDate(constraint) || (_.isString(constraint) && _.isNull(validator.toDate(constraint))));
if (!isValidConstraint) {
return 'Validation rule must be specified as a JS timestamp (number of ms since epoch), a natively-parseable date string, or a JavaScript Date instance; instead got `' + util.inspect(constraint) + '`.';
} else {
return false;
}
}
},
'isCreditCard': {
fn: function(x) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return validator.isCreditCard(x);
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function () { return 'Value was not a valid credit card.'; },
ignoreEmptyString: true
},
'isEmail': {
fn: function(x) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return validator.isEmail(x);
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid email address.'; },
ignoreEmptyString: true
},
'isHexColor': {
fn: function(x) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return validator.isHexColor(x);
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid hex color.'; },
ignoreEmptyString: true
},
'isIn': {
fn: function(x, constraint) {
return _.contains(constraint, x);
},
expectedTypes: ['json', 'ref', 'string', 'number'],
defaultErrorMessage: function(x, whitelist) { return 'Value ('+util.inspect(x)+') was not in the configured whitelist (' + whitelist.join(', ') + ')'; },
ignoreEmptyString: true,
checkConfig: function(constraint) {
if (!_.isArray(constraint)) {
return 'Allowable values must be specified as an array; instead got `' + util.inspect(constraint) + '`.';
}
return false;
}
},
'isIP': {
fn: function(x) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return validator.isIP(x);
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid IP address.'; },
ignoreEmptyString: true
},
'isNotIn': {
fn: function(x, constraint) {
return !_.contains(constraint, x);
},
expectedTypes: ['json', 'ref', 'string', 'number'],
defaultErrorMessage: function(x, blacklist) { return 'Value ('+util.inspect(x)+') was in the configured blacklist (' + blacklist.join(', ') + ')'; },
ignoreEmptyString: true,
checkConfig: function(constraint) {
if (!_.isArray(constraint)) {
return 'Blacklisted values must be specified as an array; instead got `' + util.inspect(constraint) + '`.';
}
return false;
}
},
'isURL': {
fn: function(x, opt) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
var defaultOptions = {};
// As of Validator 8.0.0, isURL requires `require_tld` to be set to false to validate localhost as a URL.
if(x.match(/^(https?:\/\/)?localhost(\:|\/|$)/)) {
// If the value we're checking is localhost, we'll add require_tld: false to the options we pass into validator
defaultOptions = {
require_tld: false,//eslint-disable-line camelcase
};
}
var options = _.extend(defaultOptions, opt); // Note: If the provided options include `require_tld: true`, that value will override the value set in defaultOptions.
return validator.isURL(x, options);
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid URL.'; },
ignoreEmptyString: true
},
'isUUID': {
fn: function(x) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return validator.isUUID(x);
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') was not a valid UUID.'; },
ignoreEmptyString: true
},
'minLength': {
fn: function(x, minLength) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return x.length >= minLength;
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function(x, minLength) { return 'Value ('+util.inspect(x)+') was shorter than the configured minimum length (' + minLength + ')'; },
ignoreEmptyString: true,
checkConfig: function(constraint) {
if (typeof constraint !== 'number' && parseInt(constraint) !== constraint) {
return 'Minimum length must be specified as an integer; instead got `' + util.inspect(constraint) + '`.';
}
return false;
}
},
'maxLength': {
fn: function(x, maxLength) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return x.length <= maxLength;
},
expectedTypes: ['json', 'ref', 'string'],
defaultErrorMessage: function(x, maxLength) { return 'Value was '+(maxLength-x.length)+' character'+((maxLength-x.length !== 1) ? 's' : '')+' longer than the configured maximum length (' + maxLength + ')'; },
ignoreEmptyString: true,
checkConfig: function(constraint) {
if (typeof constraint !== 'number' && parseInt(constraint) !== constraint) {
return 'Maximum length must be specified as an integer; instead got `' + util.inspect(constraint) + '`.';
}
return false;
}
},
'regex': {
fn: function(x, regex) {
if (typeof x !== 'string') { throw new Error ('Value was not a string.'); }
return validator.matches(x, regex);
},
defaultErrorMessage: function(x, regex) { return 'Value ('+util.inspect(x)+') did not match the configured regular expression (' + regex + ')'; },
expectedTypes: ['json', 'ref', 'string'],
ignoreEmptyString: true,
checkConfig: function(constraint) {
if (!_.isRegExp(constraint)) {
return 'Expected a regular expression as the constraint; instead got `' + util.inspect(constraint) + '`.';
}
return false;
}
},
// ┌─┐┬ ┬┌─┐┌┬┐┌─┐┌┬┐
// │ │ │└─┐ │ │ ││││
// └─┘└─┘└─┘ ┴ └─┘┴ ┴
// Custom rule function.
'custom': {
fn: function(x, customFn) {
return customFn(x);
},
expectedTypes: ['json', 'ref', 'string', 'number', 'boolean'],
defaultErrorMessage: function (x) { return 'Value ('+util.inspect(x)+') failed custom validation.'; },
checkConfig: function(constraint) {
if (!_.isFunction(constraint)) {
return 'Expected a function as the constraint; instead got `' + util.inspect(constraint) + '`. Please return `true` to indicate success, or otherwise return `false` or throw to indicate failure';
}
if (constraint.constructor.name === 'AsyncFunction') {
return 'Custom validation function cannot be an `async function` -- please use synchronous logic and return `true` to indicate success, or otherwise return `false` or throw to indicate failure.';
}
return false;
}
}
};
// Wrap a rule in a function that handles nulls and empty strings as requested,
// and adds an `expectedTypes` array that users of the rule can check to see
// if their value is of a type that the rule is designed to handle. Note that
// this list of types is not necessarily validated in the rule itself; that is,
// just because it lists "json, ref, string" doesn't necessarily mean that it
// will automatically kick out numbers (it might stringify them). It's up to
// you to decide whether to run the validation based on its `expectedTypes`.
module.exports = _.reduce(rules, function createRule(memo, rule, ruleName) {
// Wrap the original rule in a function that kicks out null and empty string if necessary.
var wrappedRule = function(x) {
// Never allow null or undefined.
if (_.isNull(x) || _.isUndefined(x)) {
return 'Got invalid value `' + x + '`!';
}
// Allow empty strings if we're explicitly ignoring them.
if (x === '' && rule.ignoreEmptyString) {
return false;
}
var passed;
// Run the original rule function.
try {
passed = rule.fn.apply(rule, arguments);
} catch (e) {
// console.error('ERROR:',e);
if (_.isError(e)) {
return e.message;
} else {
return String(e);
}
}
if (passed) { return false; }
return _.isFunction(rule.defaultErrorMessage) ? rule.defaultErrorMessage.apply(rule, arguments) : rule.defaultErrorMessage;
};//ƒ
// If the rule doesn't declare its own config-checker, assume that the constraint is supposed to be `true`.
// This is the case for most of the `is` rules like `isBoolean`, `isCreditCard`, `isEmail`, etc.
if (_.isUndefined(rule.checkConfig)) {
wrappedRule.checkConfig = function (constraint) {
if (constraint !== true) {
return 'This validation only accepts `true` as a constraint. Instead, saw `' + constraint + '`.';
}
return false;
};
} else {
wrappedRule.checkConfig = rule.checkConfig;
}
// Set the `expectedTypes` property of the wrapped function.
wrappedRule.expectedTypes = rule.expectedTypes;
// Return the wrapped function.
memo[ruleName] = wrappedRule;
return memo;
}, {});