simple-body-validator
Version:
This package is inspired by Laravel validation, and aims to make body validation easier for Javascript developers
475 lines (474 loc) • 19.3 kB
JavaScript
'use strict';
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const build_1 = require("./utils/build");
const formatMessages_1 = require("./utils/formatMessages");
const validateAttributes_1 = __importDefault(require("./validators/validateAttributes"));
const validationRuleParser_1 = __importDefault(require("./validators/validationRuleParser"));
const general_1 = require("./utils/general");
const object_1 = require("./utils/object");
const errorBag_1 = __importDefault(require("./validators/errorBag"));
const ruleContract_1 = __importDefault(require("./rules/ruleContract"));
const lang_1 = __importDefault(require("./lang"));
const password_1 = __importDefault(require("./rules/password"));
const validationData_1 = __importDefault(require("./validators/validationData"));
const replaceAttributes_1 = __importDefault(require("./validators/replaceAttributes"));
const replaceAttributePayload_1 = __importDefault(require("./payloads/replaceAttributePayload"));
class Validator {
constructor(data, rules, customMessages = {}, customAttributes = {}) {
this.data = data;
this.customMessages = (0, object_1.dotify)(customMessages);
this.customAttributes = (0, object_1.dotify)(customAttributes);
this.initalRules = rules;
this.lang = lang_1.default.getDefaultLang();
this.addRules(rules);
this.messages = new errorBag_1.default();
}
;
setData(data) {
this.data = data;
this.addRules(this.initalRules);
return this;
}
;
setRules(rules) {
this.addRules(rules);
this.initalRules = rules;
return this;
}
;
setLang(lang) {
this.lang = lang;
return this;
}
;
getLang() {
return this.lang;
}
setCustomMessages(customMessages = {}) {
this.customMessages = (0, object_1.dotify)(customMessages);
return this;
}
;
setCustomAttributes(customAttributes = {}) {
this.customAttributes = (0, object_1.dotify)(customAttributes);
return this;
}
;
stopOnFirstFailure(stopOnFirstFailure = true) {
this.stopOnFirstFailureFlag = stopOnFirstFailure;
return this;
}
;
errors() {
return this.messages;
}
;
clearErrors(keys = []) {
this.messages = this.messages.clear(keys).clone();
return this.messages;
}
/**
* Create a new ErrorBag instance and set the custom errors, thus removing previous error messages
*/
setErrors(errors) {
this.messages = new errorBag_1.default();
this.addCustomErrors(errors);
return this.messages;
}
/**
* Append the error messages to the existing ErrorBag instance, thus preserving the old error messages if any
*/
appendErrors(errors) {
this.addCustomErrors(errors, true);
return this.messages.clone();
}
/**
* Run the validator's rules against its data.
*/
validate(key = '', value = undefined) {
if (!(0, object_1.isObject)(this.data)) {
throw 'The data attribute must be an object';
}
this.validateAttributes = new validateAttributes_1.default(this.data, this.rules);
if (!key) {
this.runAllValidations();
return this.messages.keys().length === 0;
}
else {
this.runSingleValidation(key, value);
return !this.messages.has(key);
}
}
;
/**
* Run the validator's rules against its data asynchronously.
*/
validateAsync(key = '', value = undefined) {
return __awaiter(this, void 0, void 0, function* () {
if (!(0, object_1.isObject)(this.data)) {
throw 'The data attribute must be an object';
}
this.validateAttributes = new validateAttributes_1.default(this.data, this.rules);
if (!key) {
yield this.runAllValidationsAsync();
return this.messages.keys().length === 0;
}
else {
yield this.runSingleValidationAsync(key, value);
return !this.messages.has(key);
}
});
}
/**
* Get the displayable name of the attribute.
*/
getDisplayableAttribute(attribute) {
const primaryAttribute = this.getPrimaryAttribute(attribute);
const attributeCombinations = (0, formatMessages_1.getKeyCombinations)(attribute);
const translatedAttributes = (0, object_1.dotify)(lang_1.default.get(this.lang)['attributes'] || {});
let expectedAttributes = attributeCombinations;
// Combine both attributes combinations in one array
if (attribute !== primaryAttribute) {
expectedAttributes = [];
const primaryAttributeCombinations = (0, formatMessages_1.getKeyCombinations)(primaryAttribute);
for (let i = 0; i < attributeCombinations.length; i++) {
expectedAttributes.push(attributeCombinations[i]);
if (attributeCombinations[i] !== primaryAttributeCombinations[i]) {
expectedAttributes.push(primaryAttributeCombinations[i]);
}
}
}
let name = '';
let line = '';
for (let i = 0; i < expectedAttributes.length; i++) {
name = expectedAttributes[i];
// The developer may dynamically specify the object of custom attributes on this
// validator instance. If the attribute exists in the object it is used over
// the other ways of pulling the attribute name for this given attribute.
if (this.customAttributes.hasOwnProperty(name)) {
return this.customAttributes[name];
}
line = translatedAttributes[name];
// We allow for a developer to specify language lines for any attribute
if (typeof line === 'string') {
return line;
}
}
return (0, formatMessages_1.getFormattedAttribute)(attribute);
}
addCustomErrors(errors, shouldClearErrors = false) {
let newErrors;
// If the flag is set to true, we will remove the existing messages if any before setting the new ones
if (shouldClearErrors) {
this.messages.clear(Object.keys(errors));
}
for (const key in errors) {
newErrors = typeof errors[key] === 'string' ? [errors[key]] : errors[key];
newErrors.forEach(error => {
this.messages.add(key, { message: error, error_type: 'custom' });
});
}
}
/**
* Replace all error message place-holders with actual values.
*/
makeReplacements(message, attribute, rule, parameters = [], hasNumericRule = false) {
message = message.replace(':attribute', attribute);
const methodName = `replace${(0, build_1.builValidationdMethodName)(rule)}`;
if (typeof replaceAttributes_1.default[methodName] === 'function') {
const payload = new replaceAttributePayload_1.default(this.data, message, parameters, hasNumericRule, (function (attribute) {
return this.getDisplayableAttribute(attribute);
}).bind(this));
message = replaceAttributes_1.default[methodName](payload);
}
return message;
}
;
/**
* Loop through all rules and run validation against each one of them
*/
runAllValidations() {
this.messages = new errorBag_1.default();
this.validateAttributes = new validateAttributes_1.default(this.data, this.rules);
for (const property in this.rules) {
if (this.runValidation(property) === false) {
break;
}
}
}
/**
* Loop through all rules and run validation against each one of them asynchronously.
*/
runAllValidationsAsync() {
return __awaiter(this, void 0, void 0, function* () {
this.messages = new errorBag_1.default();
this.validateAttributes = new validateAttributes_1.default(this.data, this.rules);
for (const property in this.rules) {
if ((yield this.runValidationAsync(property)) === false) {
break;
}
}
});
}
/**
* Run validation for one specific attribute
*/
runSingleValidation(key, value = undefined) {
this.clearErrors([key]);
if (typeof value !== 'undefined') {
(0, object_1.deepSet)(this.data, key, value);
}
this.runValidation(key);
}
/**
* Run validation for one specific attribute asynchronously.
*/
runSingleValidationAsync(key, value = undefined) {
return __awaiter(this, void 0, void 0, function* () {
this.clearErrors([key]);
if (typeof value !== 'undefined') {
(0, object_1.deepSet)(this.data, key, value);
}
yield this.runValidationAsync(key);
});
}
/**
* Run validation rules for the specified property and stop validation if needed
*/
runValidation(property) {
if (this.rules.hasOwnProperty(property) && Array.isArray(this.rules[property])) {
for (let i = 0; i < this.rules[property].length; i++) {
this.validateAttribute(property, this.rules[property][i]);
if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) {
return false;
}
if (this.shouldStopValidating(property)) {
break;
}
}
}
}
/**
* Run validation rules for the specified property asynchronously and stop validation if needed
*/
runValidationAsync(property) {
return __awaiter(this, void 0, void 0, function* () {
if (this.rules.hasOwnProperty(property) && Array.isArray(this.rules[property])) {
for (let i = 0; i < this.rules[property].length; i++) {
yield this.validateAttribute(property, this.rules[property][i]);
if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) {
return false;
}
if (this.shouldStopValidating(property)) {
break;
}
}
}
});
}
/**
* Check if we should stop further validations on a given attribute.
*/
shouldStopValidating(attribute) {
return this.messages.has(attribute) && validationRuleParser_1.default.hasRule(attribute, ['bail'], this.rules);
}
;
/**
* Parse the given rules add assign them to the current rules
*/
addRules(rules) {
// The primary purpose of this parser is to expand any "*" rules to the all
// of the explicit rules needed for the given data. For example the rule
// names.* would get expanded to names.0, names.1, etc. for this data.
const response = validationRuleParser_1.default.explodeRules((0, object_1.dotify)(rules, true), this.data);
this.rules = response.rules;
this.implicitAttributes = response.implicitAttributes;
}
;
/**
* validate a given attribute against a rule.
*/
validateAttribute(attribute, rule) {
let parameters = [];
[rule, parameters] = validationRuleParser_1.default.parse(rule);
const keys = this.getExplicitKeys(attribute);
if (keys.length > 0 && parameters.length > 0) {
parameters = this.replaceAsterisksInParameters(parameters, keys);
}
const value = (0, object_1.deepFind)(this.data, attribute);
const validatable = this.isValidatable(attribute, value, rule);
if (rule instanceof ruleContract_1.default) {
return validatable ? this.validateUsingCustomRule(attribute, value, rule) : null;
}
const method = `validate${(0, build_1.builValidationdMethodName)(rule)}`;
if (rule !== '' && typeof this.validateAttributes[method] === 'undefined') {
throw `Rule ${rule} is not valid`;
}
if (!validatable) {
return;
}
const validation = this.validateAttributes[method](value, parameters, attribute);
if (validation instanceof Promise) {
return validation.then(result => {
if (!result) {
this.addFailure(attribute, rule, value, parameters);
}
});
}
else if (!validation) {
this.addFailure(attribute, rule, value, parameters);
}
}
;
/**
* Validate an attribute using a custom rule object
*/
validateUsingCustomRule(attribute, value, rule) {
rule.setData(this.data).setLang(this.lang);
if (rule instanceof password_1.default) {
rule.setValidator(this);
}
const result = rule.passes(value, attribute);
if (result instanceof Promise) {
return result.then(validationResult => {
if (!validationResult) {
this.setCustomRuleErrorMessages(attribute, rule);
}
});
}
if (!result) {
return this.setCustomRuleErrorMessages(attribute, rule);
}
}
;
/**
* Set the error message linked to a custom validation rule
*/
setCustomRuleErrorMessages(attribute, rule) {
let result = rule.getMessage();
let messages = typeof result === 'string' ? [result] : result;
for (let key in messages) {
this.messages.add(attribute, {
error_type: rule.constructor.name, message: this.makeReplacements(messages[key], this.getDisplayableAttribute(attribute), rule.constructor.name)
});
}
}
/**
* Add a new error message to the messages object
*/
addFailure(attribute, rule, value, parameters) {
const hasNumericRule = validationRuleParser_1.default.hasRule(attribute, (0, general_1.getNumericRules)(), this.rules);
const primaryAttribute = this.getPrimaryAttribute(attribute);
const attributes = attribute !== primaryAttribute ?
[attribute, primaryAttribute] : [attribute];
const message = this.makeReplacements((0, formatMessages_1.getMessage)(attributes, rule, value, this.customMessages, hasNumericRule, this.lang), this.getDisplayableAttribute(attribute), rule, parameters, hasNumericRule);
const error = {
error_type: rule,
message
};
this.messages.add(attribute, error);
}
;
/**
* Replace each field parameter which has asterisks with the given keys.
*
* Example: parameters = [name.*.first] and keys = [1], then the result will be name.1.first
*/
replaceAsterisksInParameters(parameters, keys) {
return parameters.map(parameter => {
let result = '';
if (parameter.indexOf('*') !== -1) {
let parameterArray = parameter.split('*');
result = parameterArray[0];
for (let i = 1; i < parameterArray.length; i++) {
result = result.concat((keys[i - 1] || '*') + parameterArray[i]);
}
}
return result || parameter;
});
}
;
/**
* Determine if the attribute is validatable.
*/
isValidatable(attribute, value, rule) {
return this.presentOrRuleIsImplicit(attribute, value, rule) &&
this.passesOptionalCheck(attribute) &&
this.isNotNullIfMarkedAsNullable(attribute, rule);
}
;
/**
* Determine if the field is present, or the rule implies required.
*/
presentOrRuleIsImplicit(attribute, value, rule) {
if (typeof value === 'string' && value.trim() === '') {
return (0, general_1.isImplicitRule)(rule);
}
return typeof (0, object_1.deepFind)(this.data, attribute) !== 'undefined' ||
(0, general_1.isImplicitRule)(rule);
}
/**
* Determine if the attribute passes any optional check.
*/
passesOptionalCheck(attribute) {
if (!validationRuleParser_1.default.hasRule(attribute, ['sometimes'], this.rules)) {
return true;
}
const data = validationData_1.default.initializeAndGatherData(attribute, this.data);
return data.hasOwnProperty(attribute)
|| this.data.hasOwnProperty(attribute);
}
;
/**
* Determine if the attribute fails the nullable check.
*/
isNotNullIfMarkedAsNullable(attribute, rule) {
if ((0, general_1.isImplicitRule)(rule) || !validationRuleParser_1.default.hasRule(attribute, ['nullable'], this.rules)) {
return true;
}
return (0, object_1.deepFind)(this.data, attribute) !== null;
}
;
/**
* Get the primary attribute name
*
* Example: if "name.0" is given, "name.*" will be returned
*/
getPrimaryAttribute(attribute) {
for (let unparsed in this.implicitAttributes) {
if (this.implicitAttributes[unparsed].indexOf(attribute) !== -1) {
return unparsed;
}
}
return attribute;
}
;
/**
* Get the explicit keys from an attribute flattened with dot notation.
*
* Example: 'foo.1.bar.spark.baz' -> [1, 'spark'] for 'foo.*.bar.*.baz'
*/
getExplicitKeys(attribute) {
const pattern = new RegExp('^' + this.getPrimaryAttribute(attribute).replace(/\*/g, '([^\.]*)'));
let keys = attribute.match(pattern);
if (keys) {
keys.shift();
return keys;
}
return [];
}
;
}
exports.default = Validator;