UNPKG

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
'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;