node-input-validator
Version:
validation library for nodejs, inspired by laravel.
614 lines (528 loc) • 14.8 kB
JavaScript
const rules = require('./rules/index');
const postRules = require('./postRules/index');
// const filters = require('./filters/index');
const messages = require('./messages/index');
const messageParser = require('./util/messageParser');
const empty = require('./util/empty');
const { strNotations } = require('./util/obj');
// you can change this value with static method
let shouldBreakIfFailed = true;
const implicitRules = [
'required',
'requiredIf',
'requiredNotIf',
'requiredWith',
'requiredWithout',
'accepted',
'sometimes',
'nullable',
];
class Validator {
/**
* @constructor
* @param {*} inputs
* @param {*} validationsRules
* @param {*} customMessages
*/
constructor(inputs = {}, validationsRules = {}, customMessages = {}) {
// inputs collection
this.inputs = inputs;
// errors collections
this.errors = {};
// post validation collection
this.postValidations = [];
// filters collection
this.filters = {};
// default language
this.lang = messages.defaultLang;
// validation messages for field
this.customMessages = customMessages;
// has custom messages
this.hasCustomMessages = (Object.keys(this.customMessages).length);
// attribute nice names
this.attributeNames = {};
// store validation results
this.wasFailed = null;
// break rules validation loop when failed
this.breakWhenFailed = shouldBreakIfFailed;
// parse validations collection, this should be after all assignments
this.parseRules(validationsRules);
}
/**
* globally should break/bail on failed validation or not
* @param {boolean} sure
*/
static bailable(sure) {
shouldBreakIfFailed = sure;
}
/**
* enable/disable multiple errors on current instance only
* @param {*} sure
*/
bail(sure) {
this.breakWhenFailed = sure;
}
/**
* Allows a custom rule to be added as an implicit rule
* @param {String} ruleName
*/
static addImplicitRule(ruleName) {
implicitRules.push(ruleName);
}
/**
* set attributes nice names
* @param {*} attributeNames
*/
niceNames(attributeNames) {
this.attributeNames = attributeNames;
}
/**
* prepare rules to be used
* @param {*} validationRules
*/
parseRules(validationRules) {
// rules collection
this.validationRules = {};
// attributes in rules
const attrs = Object.keys(validationRules);
// loop through attributes
attrs.forEach((attr) => {
if (attr === '*') {
this.addPostRules(validationRules[attr].split('|'));
return;
}
// get rules from attribute
const strRules = validationRules[attr];
// rule blueprint
const attrRule = {
name: attr,
attr,
value: null,
rules: [],
nullable: false,
};
let rulesArray = strRules;
// if array of rules
if (!Array.isArray(rulesArray)) {
// get array of rules from string
rulesArray = (strRules || '').toString()
.split('|');
}
// set rules of attribute
this.validationRules[attr] = attrRule;
// parse array of string rules
rulesArray.forEach((rule) => {
// split by colon to get rule name and args
let ruleName;
let argsStr;
if (Array.isArray(rule)) {
[ruleName, ...argsStr] = rule;
} else if (rule.indexOf(':') !== -1) {
[ruleName, argsStr] = rule.split(':');
} else {
ruleName = rule;
}
// if rule is nullable
if (ruleName === 'nullable') {
attrRule.nullable = true;
}
// parse rule object with name and args
let parsedRule;
// if args exists
if (typeof argsStr !== 'undefined') {
const ruleArgs = Array.isArray(argsStr) ? argsStr : argsStr.split(',');
parsedRule = { rule: ruleName, args: ruleArgs };
} else {
// in case of no arguments
parsedRule = { rule: ruleName, args: [] };
}
// if rule has greater priority over
if (implicitRules.indexOf(ruleName) >= 0) {
attrRule.rules.unshift(parsedRule);
attrRule.required = true;
} else {
attrRule.rules.push(parsedRule);
}
});
// set parse rules of attribute
this.validationRules[attr] = attrRule;
});
}
/**
* get errors
*/
getErrors() {
return this.errors;
}
/**
* alias for check
*/
validate() {
return this.check();
}
/**
* validate inputs
* @return {Promise.<boolean>}
*/
async check() {
// collection of promises
const validations = [];
// get keys of rules
const attributes = Object.keys(this.validationRules);
const deeplyNestedAttrs = [];
// loop through each attribute
attributes.forEach((attr) => {
if ((attr.match(/\*/g) || []).length > 1 || Array.isArray(this.inputs)) {
deeplyNestedAttrs.push(attr);
return;
}
validations.push(this.apply(attr));
});
if (deeplyNestedAttrs.length) {
validations.push(this.applyOnDeep(deeplyNestedAttrs));
}
this.postValidations.forEach((postRule) => {
validations.push(this.postApply(postRule));
});
// check all promises resolved
await Promise.all(validations);
// store results
this.wasFailed = (Object.keys(this.errors).length);
return !this.wasFailed;
}
/**
* find attribute value, can also be used to get object value
* @param {string} attr
*/
parseValue(attr) {
const path = attr.split('.');
// return with value, if attribute is not an Object
if (path.length === 1) {
return this.inputs[attr];
}
// find attribute value from object
let value;
const keySplit = path.filter((e) => e !== '');
keySplit.map((item) => {
if (typeof value === 'undefined') {
value = this.inputs[item];
} else {
value = value[item];
}
return value;
});
if (typeof value === 'undefined') {
return '';
}
return value;
}
/**
* process deeply nested attributes
* @param {*} attrs
*/
async applyOnDeep(attrs) {
const notations = strNotations(this.inputs);
const validations = [];
Object.keys(notations).forEach((notation) => {
attrs.forEach((attr) => {
const pttren = attr.replace(/\*/g, '[0-9]+');
const results = notation.match(RegExp(`^${pttren}$`));
if (results) {
this.validationRules[notation] = Object.create(this.validationRules[attr]);
this.validationRules[notation].value = notations[notation];
this.validationRules[notation].name = notation;
validations.push(this.apply(notation));
}
});
});
await Promise.all(validations);
}
/**
* process single levelt nesting
* @param {*} attr
*/
parseNestedAttr(attr) {
const validation = this.validationRules[attr];
const splitedAttr = validation.name.split('*');
const startKey = splitedAttr[0];
const value = this.parseValue(startKey);
let index = 0;
Object.keys(value).forEach(() => {
const absKey = attr.replace('*', index);
this.validationRules[absKey] = Object.create(validation);
this.validationRules[absKey].value = this.parseValue(absKey);
this.validationRules[absKey].name = absKey;
this.apply(absKey);
index++;
});
}
/**
* Apply rule on attribute, rules must be parsed first
* @param {string} attr
*/
async apply(attr) {
// get parse rule of attribute
const validation = this.validationRules[attr];
const attrRules = validation.rules;
const rulesLen = validation.rules.length;
// if not, return
if (!rulesLen) return;
// if attribute is nested
if (attr.indexOf('*') >= 0) {
this.parseNestedAttr(attr);
return;
}
// collection of async rules
const rulesPromise = [];
const rulesPromiseProps = [];
// loop all rules
for (let r = 0; r < rulesLen; r++) {
const { rule, args } = attrRules[r];
// get value
validation.value = this.parseValue(attr);
// return if nullable or value is null
if (rule === 'nullable' || (validation.nullable === true && validation.value === null)) {
continue;
}
// in case rule not found
if (typeof rules[rule] !== 'function') {
throw new Error(`Validation Rule: ${rule} does not exists.`);
}
// if value is really empty, skip validation
if (!validation.required && empty.reallyEmpty(validation.value)) {
continue;
}
// call rule method
const result = rules[rule]({ attr, value: validation.value, args }, this);
// if promise, add to async rules collection
if (result instanceof Promise) {
// result = await result;
rulesPromise.push(result);
rulesPromiseProps.push({ rule, args });
continue;
}
if (result && implicitRules.indexOf(validation.rules[r].rule) > 0) {
// eslint-disable-next-line no-param-reassign
validation.required = false;
}
// if validtion failed, get message
if (!result) {
const message = this.getParsedMessage({
rule,
args,
attr: validation.attr,
value: validation.value,
});
if (this.breakWhenFailed) {
// add error
this.addError(attr, rule, message);
return;
}
this.appendError(attr, rule, message);
}
} // rules loop ends here
if (rulesPromise.length) {
const asyncResults = await Promise.all(rulesPromise);
let promiseIndex = 0;
asyncResults.every((result) => {
const { rule, args } = rulesPromiseProps[promiseIndex];
// if validtion failed, get message
if (!result) {
const message = this.getParsedMessage({
rule,
args,
attr: validation.attr,
value: validation.value,
});
if (this.breakWhenFailed) {
// add error
this.addError(attr, rule, message);
return false;
}
this.appendError(attr, rule, message);
}
promiseIndex++;
return true;
});
}
}
async postApply(rule) {
if (rule.rule === 'function') {
// eslint-disable-next-line no-return-await
return await rule.handler(this);
}
// eslint-disable-next-line no-return-await
return await postRules[rule.rule](rule, this);
}
/**
* add post rule
*
* post rule is applied to whole input and is used to check constraints
* across multiple fields
*
* @param {*} rule
*/
addPostRule(rule) {
if (typeof rule === 'function') {
this.postValidations.push({
rule: 'function',
handler: rule,
});
return;
}
const ruleArray = rule.split(':', 2);
const ruleName = ruleArray[0];
const ruleFields = ruleArray[1].split(','); // there always be a list of fields
// // eslint-disable-next-line no-return-assign
// const values = ruleFields.reduce((acc, field) => {
// acc[field] = this.parseValue(field);
// return [acc[field], acc];
// }, {});
this.postValidations.push({
rule: ruleName,
params: ruleFields,
});
}
/**
* add set of post rules
*
* @param {string[]} postRulesObj
*/
addPostRules(postRulesObj) {
postRulesObj.map((rule) => this.addPostRule(rule));
}
/**
* parse message for rule
* @param {{rule, args?:[], attr, value, useDefaultMessage?}} args
*/
getParsedMessage({
rule,
args = [],
attr,
value,
useDefaultMessage = true,
}) {
/**
* 1. check for attribute.rule
* 2. check for rule
* 3. check for attribute
* 4. fallback to default message
*/
let attributeName = attr;
let message;
const defaultMessage = messages[this.lang].$default || 'The :attribute value is malformed.';
// check for local scope messages
if (this.hasCustomMessages) {
message = this.customMessages[`${attr}.${rule}`]
|| this.customMessages[attr]
|| this.customMessages[rule];
}
// not found in local scope, check for global scope
if (!message) {
// from global messages bucket
messages[this.lang].$custom = messages[this.lang].$custom || {};
// message.$custom['attribute.rule']
message = messages[this.lang].$custom[`${attr}.${rule}`]
// message.$custom.rule
|| messages[this.lang].$custom[rule]
// message.$custom.attr
|| messages[this.lang].$custom[attr]
// message.rule
|| messages[this.lang][rule];
if (useDefaultMessage && !message) {
message = defaultMessage;
}
}
// global attribute name
if (messages[this.lang].$niceNames && messages[this.lang].$niceNames[attr]) {
attributeName = messages[this.lang].$niceNames[attr];
}
// check if attribute has some nice name in local scope
if (this.attributeNames[attr]) {
attributeName = this.attributeNames[attr];
}
return messageParser({
rule,
args,
attr: attributeName,
value,
message,
});
}
/**
* parse existing message only
* @param {*} args
*/
getExistinParsedMessage({
rule,
args = [],
attr,
value,
}) {
/**
* 1. check for attribute.rule
* 2. check for rule
* 3. check for attribute
* 4. fallback to default message
*/
return this.getParsedMessage({
rule,
args,
attr,
value,
useDefaultMessage: false,
});
}
/**
*
* @param {string} key
* @param {string} rule
* @param {string} message
*/
error(key, rule, message) {
if (this.breakWhenFailed) {
this.addError(key, rule, message);
return;
}
this.appendError(key, rule, message);
}
/**
* add error
* @param {string} key
* @param {string} rule
* @param {string} message
*/
addError(key, rule, message) {
this.errors[key] = {
message,
rule,
};
}
/**
* append errors
* @param {string} key
* @param {string} rule
* @param {string} message
*/
appendError(key, rule, message) {
if (!this.errors[key]) {
this.errors[key] = [];
}
this.errors[key].push({ message, rule });
}
/**
* check for validation fails
* @return {Promise}
*/
async fails() {
return !(await this.check());
}
/**
* check if validation passes
* @return {Promise}
*/
passes() {
return this.check();
}
}
module.exports = Validator;