UNPKG

abolish

Version:

A javascript object validator.

963 lines (962 loc) 37.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SuperKeys = exports.AttemptError = void 0; exports.isAbolishClass = isAbolishClass; exports.isAbolishInstance = isAbolishInstance; const StringToRules_1 = __importDefault(require("./StringToRules")); const GlobalValidators_1 = __importDefault(require("./GlobalValidators")); const inbuilt_fn_1 = require("./inbuilt.fn"); const functions_1 = require("./functions"); const AbolishError_1 = __importDefault(require("./AbolishError")); const ObjectModifier_1 = __importDefault(require("./ObjectModifier")); const Compiler_1 = require("./Compiler"); const types_checker_1 = require("./types-checker"); class AttemptError extends Error { constructor(e) { super(e.message); this.name = "AttemptError"; this.error = e; } static instanceOf(e) { return (0, inbuilt_fn_1.InstanceOf)(this, e); } } exports.AttemptError = AttemptError; /** * Abolish Super Rules and Keys */ exports.SuperKeys = Object.freeze({ Wildcards: new Set(["*", "$"]), Fields: new Set(["*", "$", "$include", "$strict"]), Rules: new Set(["$name", "$skip", "$error", "$errors"]) }); /** * Abolish Class * @class */ class Abolish { constructor() { this.validators = {}; this.config = { useStartCaseInErrors: true }; } /** * Get global validators */ static getGlobalValidators() { return GlobalValidators_1.default; } /** * Get global validators list */ static getGlobalValidatorsList() { return Object.keys(this.getGlobalValidators()); } /** * Add single global validator * @param validator */ static addGlobalValidator(validator) { if (typeof validator === "object" && !Array.isArray(validator)) { // If no error defined set default error if (!validator.error) validator.error = `:param failed {${validator.name}} validation.`; // Add validator to instance validators GlobalValidators_1.default[validator.name] = validator; // set validator function name Object.defineProperty(validator.validator, "name", { value: validator.name }); } else { throw new TypeError("addGlobalValidator argument must be an object."); } return this; } /** * Add multiple global validators * @param validators */ static addGlobalValidators(validators) { if (typeof validators === "object") { validators = Object.values(validators); } if (Array.isArray(validators)) { for (const value of validators) { Abolish.addGlobalValidator(value); } } else { throw new TypeError("addGlobalValidators argument must be an array or an object"); } return this; } /** * Toggle start case in error. * @param value */ useStartCaseInErrors(value = true) { this.config.useStartCaseInErrors = value; return this; } /** * addValidator * @description * Add validator or array of validators * @param validator */ addValidator(validator) { if (typeof validator === "object" && !Array.isArray(validator)) { // If no error defined set default error if (!validator.error) validator.error = `:param failed {${validator.name}} validation.`; // Add validator to instance validators this.validators[validator.name] = validator; } else { throw new TypeError("addValidator argument must be an object."); } return this; } /** * addValidators * @description * Add validator or array of validators * @param validators */ addValidators(validators) { if (typeof validators === "object") { validators = Object.values(validators); } if (Array.isArray(validators)) { for (const value of validators) { this.addValidator(value); } } else { throw new TypeError("addValidators argument must be an array or an object"); } return this; } static validate(object, rules) { return new this().validate(object, rules); } /** * Validate Async * * Waits for all validation defined * @param object * @param rules * @return {Promise<ValidationResult>} */ static validateAsync(object, rules) { return new this().validateAsync(object, rules); } validate(object, rules, isAsync = false) { if (rules instanceof Compiler_1.AbolishCompiled) { return rules.validateObject(object); } const asyncData = { validated: {}, jobs: [], keysToBeValidated: [], includeKeys: [] }; /** * Check for wildcard rules (*, $) */ let internalWildcardRules = {}; if (rules.hasOwnProperty("*") || rules.hasOwnProperty("$")) { internalWildcardRules = rules["*"] || rules["$"]; /** * Convert rules[*] to object if string * Using StringToRules function */ if (typeof internalWildcardRules === "string") internalWildcardRules = (0, StringToRules_1.default)(internalWildcardRules); } /** * Validated clones original object to prevent modifying values in original object */ let validated = { ...object }; /** * Get Keys to be validated */ let includeKeys = []; if (rules.hasOwnProperty("$include")) { includeKeys = rules["$include"]; if (!Array.isArray(includeKeys)) throw new Error(`$include has to be an array!`); } // remove SUPER_RULES from keysToBeValidated let keysToBeValidated = Object.keys(rules).filter((key) => !exports.SuperKeys.Fields.has(key)); let allowedKeys = undefined; if (rules.hasOwnProperty("$strict")) { if (rules["$strict"] === true) { allowedKeys = keysToBeValidated; } else if (Array.isArray(rules["$strict"])) { // if strict is an array, then append it to allowedKeys allowedKeys = keysToBeValidated.concat(rules["$strict"]); } else { throw new Error(`$strict must be a boolean or an array of allowed keys.`); } // add $include keys to allowedKeys allowedKeys = Array.from(new Set(allowedKeys.concat(includeKeys))); // check if all keys in object are allowed const objectKeys = Object.keys(validated); const unknownKeys = objectKeys.filter((key) => !allowedKeys.includes(key)); if (unknownKeys.length > 0) { return [ { code: "object.unknown", type: "internal", key: "$strict", validator: "$strict", message: "Data contains unknown fields!", data: { unknown: unknownKeys } }, {} ]; } } // Loop through defined rules for (const rule of keysToBeValidated) { let ruleData = rules[rule]; /** * Convert ruleData to object if string * Using StringToRules function */ if (typeof ruleData === "string") { ruleData = (0, StringToRules_1.default)(ruleData); } else if (Array.isArray(ruleData)) { ruleData = (0, functions_1.Rule)(ruleData); } /** * if ruleData has property of $skip then check */ let $skip = false; if (ruleData.hasOwnProperty("$skip")) { $skip = ruleData["$skip"]; // delete ruleData["$skip"]; if (typeof $skip === "function") { $skip = $skip(validated[rule], validated); } if (typeof $skip !== "boolean") { throw new Error(`$skip value or resolved function value must be a BOOLEAN in RuleFor: (${rule})`); } } /** * Run validation if not $skip * else remove key from keysToBeValidated */ if ($skip) { keysToBeValidated = keysToBeValidated.filter((v) => v !== rule); } else { /** * if ruleData has property of $name then set to name */ let $name = false; if (ruleData.hasOwnProperty("$name")) { $name = ruleData["$name"]; // delete ruleData["$name"]; if (typeof $name !== "string") { throw new Error(`$name must be a string in RuleFor: (${rule})`); } } /** * check if rules has custom error: $error */ let $error; if (ruleData.hasOwnProperty("$error")) { $error = ruleData["$error"]; // delete ruleData["$error"]; // noinspection SuspiciousTypeOfGuard if (!$error || (typeof $error !== "string" && typeof $error !== "function")) { throw new Error(`$error value must be a STRING or FUNCTION in RuleFor: (${rule})`); } } let $errors; if (ruleData.hasOwnProperty("$errors")) { $errors = ruleData["$errors"]; // delete ruleData["$errors"]; // noinspection SuspiciousTypeOfGuard if (!$errors || typeof $errors !== "object") { throw new Error(`$errors value must be an OBJECT in RuleFor: (${rule})`); } } /** * Append internal Wildcard data */ ruleData = { ...internalWildcardRules, ...(0, inbuilt_fn_1.abolish_Omit)(ruleData, [...exports.SuperKeys.Rules]) }; /** * Loop through ruleData to check if validators defined exists */ for (const validatorName of Object.keys(ruleData)) { /** * Throw Error if validator is not defined in global or local validators */ if (!this.validators.hasOwnProperty(validatorName) && !GlobalValidators_1.default.hasOwnProperty(validatorName)) { throw new Error(`Validator: {${validatorName}} does not exists but defined in rules`); } /** * Validator of rule defined in rules. */ const validator = (this.validators[validatorName] || GlobalValidators_1.default[validatorName]); if (!isAsync && validator.isAsync) { throw new Error(`Validator: {${validatorName}} is async, use async method instead.`); } /** * The value of the validator set in rules * e.g {required: true} * where "true" is validationOption */ const validatorOption = ruleData[validatorName]; /** * Value of key being validated in object * if rule has dot notation e.g. "address.city" * we use `abolish_Get` * else use normal index */ // const objectValue = abolish_Get(validated, rule); // const objectValue = validated[rule]; const objectValue = (0, inbuilt_fn_1.abolish_Get)(validated, rule); /** * If is async push needed data to asyncData */ if (isAsync) { asyncData.jobs.push({ $name, rule, validator, validatorName, validatorOption, $error, $errors }); } else { /** * Try running validator */ let validationResult = false; try { /** * Run Validation * Passing required helpers */ validationResult = validator.validator(objectValue, validatorOption, { error: (message, data) => new AbolishError_1.default(message, data), modifier: new ObjectModifier_1.default(validated, rule, $name), abolish: this }); } catch (e) { /** * If error when running defined function * Send error as validationResult with type as 'internal' */ return [ { code: "default", key: rule, type: "internal", validator: validatorName, message: e.message, data: e.stack }, {} ]; } if (validationResult === false || (0, inbuilt_fn_1.InstanceOf)(AbolishError_1.default, validationResult)) { let message; let data = null; let code = "default"; if ((0, inbuilt_fn_1.InstanceOf)(AbolishError_1.default, validationResult)) { message = validationResult.message; data = validationResult.data; code = validationResult.code; } if ($error) { if (typeof $error === "function") { message = $error({ code, validator: validatorName, data, value: objectValue }); } else { message = $error; } } if ($errors && $errors[validatorName]) { let customError = $errors[validatorName]; if (typeof customError === "function") { message = customError({ code, data, validator: validatorName, value: objectValue }); } else { message = customError; } } /** * Check if option is stringAble * This is required because a rule option could an array or an object * and these cannot be converted to string * * Only strings and numbers can be parsed as :option */ const optionIsStringAble = typeof validatorOption === "string" || typeof validatorOption === "number" || Array.isArray(validatorOption); /** * Replace :param with rule converted to upperCase * and if option is stringAble, replace :option with validatorOption */ message = (message || validator.error).replace(":param", $name ? $name : (0, inbuilt_fn_1.abolish_StartCase)(rule, this)); if (optionIsStringAble) message = message.replace(":option", String(validatorOption)); // Return Error using the ValidationResult format return [ { code, key: rule, type: "validator", validator: validatorName, message, data }, {} ]; } } } } } if (isAsync) { asyncData.validated = validated; asyncData.keysToBeValidated = keysToBeValidated; asyncData.includeKeys = includeKeys; return asyncData; } // abolish_Pick only keys in rules validated = (0, inbuilt_fn_1.abolish_Pick)(validated, keysToBeValidated.concat(includeKeys)); return [undefined, validated]; } validateAsync(object, rules) { if (rules instanceof Compiler_1.AbolishCompiled) { return rules.validateObjectAsync(object); } /** * Get asyncData */ const asyncData = this.validate(object, rules, true); /** * Destruct values in async data */ const { validated, jobs, keysToBeValidated, includeKeys } = asyncData; /** * Return a promise */ return new Promise(async (resolve) => { /** * Loop through jobs and run their validators */ for (const job of jobs) { const { $name, rule, validator, validatorName, validatorOption, $error, $errors } = job; /** * Value of key being validated in object */ const objectValue = (0, inbuilt_fn_1.abolish_Get)(validated, rule); let validationResult = false; try { /** * Run Validation * Passing required helpers */ validationResult = await validator.validator(objectValue, validatorOption, { error: (message, data) => new AbolishError_1.default(message, data), modifier: new ObjectModifier_1.default(validated, rule, $name), abolish: this }); } catch (e) { /** * If error when running defined function * Send error as validationResult with type as 'internal' */ return resolve([ { code: "default", key: rule, type: "internal", validator: validatorName, message: e.message, data: e.stack }, {} ]); } if (validationResult === false || (0, inbuilt_fn_1.InstanceOf)(AbolishError_1.default, validationResult)) { let message; let data = null; let code = "default"; if ((0, inbuilt_fn_1.InstanceOf)(AbolishError_1.default, validationResult)) { message = validationResult.message; data = validationResult.data; code = validationResult.code; } if ($error) { if (typeof $error === "function") { message = $error({ code, validator: validatorName, data, value: objectValue }); } else { message = $error; } } if ($errors && $errors[validatorName]) { let customError = $errors[validatorName]; if (typeof customError === "function") { message = customError({ code, data, validator: validatorName, value: objectValue }); } else { message = customError; } } /** * Replace :param with rule converted to upperCase * and if option is stringable, replace :option with validatorOption */ message = (message || validator.error).replace(":param", $name ? $name : (0, inbuilt_fn_1.abolish_StartCase)(rule, this)); /** * Check if option is stringable * This is required because a rule option could an array or an object * and these cannot be converted to string * * Only strings and numbers can be parsed as :option */ const optionIsStringAble = typeof validatorOption === "string" || typeof validatorOption === "number"; if (optionIsStringAble) message = message.replace(":option", String(validatorOption)); // Return Error using the ValidationResult format return resolve([ { code, key: rule, type: "validator", validator: validatorName, message: message, data }, {} ]); } } return resolve([ undefined, (0, inbuilt_fn_1.abolish_Pick)(validated, keysToBeValidated.concat(includeKeys)) ]); }); } /** * check a variable does not throw error * @param variable * @param rules */ check(variable, rules) { if (rules instanceof Compiler_1.AbolishCompiled) { return rules.validateVariable(variable); } const [e, v] = this.validate({ variable }, { variable: rules, // variable is included in-case if skipped $include: ["variable"] }); return [e, v?.variable]; } /** * Static Check * @param variable * @param rules */ static check(variable, rules) { return new this().check(variable, rules); } /** * Checks a variable Asynchronously * @param variable * @param rules */ async checkAsync(variable, rules) { if (rules instanceof Compiler_1.AbolishCompiled) { return rules.validateVariableAsync(variable); } const [e, v] = await this.validateAsync({ variable }, { variable: rules, // variable is included in-case if skipped $include: ["variable"] }); return [e, v?.variable]; } /** * Static Check Async * @param variable * @param rules */ static checkAsync(variable, rules) { return new this().checkAsync(variable, rules); } /** * Validates a variable * @param variable * @param rules */ attempt(variable, rules) { const data = this.check(variable, rules); if (data[0]) throw new AttemptError(data[0]); return data[1]; } /** * Static Attempt * @param variable * @param rules * @param abolish */ static attempt(variable, rules) { return new this().attempt(variable, rules); } /** * Validates a variable Asynchronously, Throws error * @param variable * @param rules */ async attemptAsync(variable, rules) { const data = await this.checkAsync(variable, rules); if (data[0]) throw new AttemptError(data[0]); return data[1]; } /** * Validates a variable Asynchronously, Throws error * @param variable * @param rules */ static async attemptAsync(variable, rules) { return new this().attemptAsync(variable, rules); } /** * test a variable, returns boolean * @param variable * @param rules */ test(variable, rules) { const data = this.check(variable, rules); return !data[0]; } /** * Static Check * @param variable * @param rules */ static test(variable, rules) { return new this().test(variable, rules); } /** * Checks a variable Asynchronously * @param variable * @param rules */ async testAsync(variable, rules) { const data = await this.checkAsync(variable, rules); return !data[0]; } /** * Static Check Async * @param variable * @param rules */ static testAsync(variable, rules) { return new this().testAsync(variable, rules); } /** * Compile a input * @param schema * @param CustomAbolish */ static compileObject(schema, CustomAbolish) { const abolish = CustomAbolish ? isAbolishInstance(CustomAbolish) ? CustomAbolish : new CustomAbolish() : new Abolish(); const compiled = new Compiler_1.AbolishCompiled((0, functions_1.Schema)(schema)); let internalWildcardRules; let includeFields = []; let allowedKeys = undefined; let schemaEntries = Object.entries(schema); for (const [field, rules] of schemaEntries) { /** * Check for wildcard rules (*, $) */ if (exports.SuperKeys.Wildcards.has(field)) { internalWildcardRules = rules; /** * Convert rules[*] to object if string * Using StringToRules function */ if (typeof internalWildcardRules === "string") internalWildcardRules = (0, StringToRules_1.default)(internalWildcardRules); } if (field === "$include") { includeFields = rules; } if (field === "$strict") { if (rules === true) { allowedKeys = Object.keys(schema); } else if (Array.isArray(rules)) { allowedKeys = rules; } else { throw new Error(`$strict must be a boolean or an array of allowed keys.`); } } } /** * Loop Through each field and rule */ for (const [field, rules] of schemaEntries) { if (exports.SuperKeys.Fields.has(field)) continue; const compiledRule = { validators: {} }; /** * Convert ruleData to object if string * Using StringToRules function */ let parsedRules = rules; if (typeof rules === "string") { parsedRules = (0, StringToRules_1.default)(rules); } else if (Array.isArray(rules)) { parsedRules = (0, functions_1.Rule)(rules); } if (internalWildcardRules) { parsedRules = { ...internalWildcardRules, ...parsedRules }; } let $error; let $errors = {}; /** * Loop Through each rule and generate validator */ for (const [validatorName, option] of Object.entries(parsedRules)) { if (!exports.SuperKeys.Rules.has(validatorName)) continue; if (validatorName === "$name") { (0, types_checker_1.assertType)(option, ["string"], "$name"); compiledRule.$name = option; } else if (validatorName === "$skip") { // $skip = option as $skipRule; (0, types_checker_1.assertType)(option, ["boolean", "function"], "$skip"); // set skip compiledRule.$skip = option; } else if (validatorName === "$error") { // $error = option as $errorRule; (0, types_checker_1.assertType)(option, ["string", "function"], "$error"); $error = option; } else if (validatorName === "$errors") { // $errors = option as Record<string, $errorRule>; (0, types_checker_1.assertType)(option, ["object"], "$errors"); $errors = option; } } if (!compiledRule.$name && abolish.config.useStartCaseInErrors) { compiledRule.$name = (0, inbuilt_fn_1.abolish_StartCase)(field); } /** * Object modifier */ const modifier = new ObjectModifier_1.default({}, field); /** * Loop Through each rule and generate validator */ for (const [validatorName, option] of Object.entries(parsedRules)) { if (exports.SuperKeys.Rules.has(validatorName)) continue; const validator = (abolish.validators[validatorName] || GlobalValidators_1.default[validatorName]); if (!validator) throw new Error(`Validator ${validatorName} not found`); if (validator.isAsync) compiled.async = true; /** * Check if option is stringAble * This is required because a rule option could an array or an object * and these cannot be converted to string * * Only strings and numbers can be parsed as :option */ const optionIsStringAble = typeof option === "string" || typeof option === "number" || typeof option === "boolean" || Array.isArray(option); const ctx = { abolish, modifier, error: (message, data) => new AbolishError_1.default(message, data) }; // If no error defined set default error if (!validator.error) validator.error = `:param failed {${validator.name}} validation.`; /** * Parse error */ let error = validator.error; let errorFn; let customError; /** * Process $error * if $error is a string, use it as error message * if $error is a function, use it as error function */ if ($error) { if (typeof $error === "string") { error = $error; customError = true; } else if (typeof $error === "function") { errorFn = $error; customError = true; } } if ($errors && $errors[validatorName]) { const errorMessage = $errors[validatorName]; if (typeof errorMessage === "string") { error = errorMessage; customError = true; } else if (typeof errorMessage === "function") { errorFn = errorMessage; customError = true; } } if (error.includes(":param")) { // replace all :param with field name error = error.replace(/:param/g, compiledRule.$name || field); } const data = { name: validatorName, option: option, error: error, async: validator.isAsync === true, func(value, validated) { ctx.modifier.setData(validated); return validator.validator(value, this.option, ctx); } }; if (customError) data.customError = true; if (errorFn) data.errorFn = errorFn; if (optionIsStringAble) { data.optionString = String(option); data.error = data.error.replace(/:option/g, data.optionString); } // Set validator name Object.defineProperty(data.func, "name", { value: `Wrapped(${validatorName})` }); compiledRule.validators[validatorName] = data; } compiled.data[field] = compiledRule; } // Populate Fields to be picked // In order to make sure unique fields are picked // We have to loop and check if the field is already picked Object.keys(compiled.data).forEach((field) => { if (!compiled.fields.includes(field)) compiled.fields.push(field); }); includeFields.forEach((field) => { if (!compiled.fields.includes(field)) compiled.fields.push(field); }); compiled.includedFields = includeFields; // Check if any field has dot notation compiled.fieldsHasDotNotation = compiled.fields.some(inbuilt_fn_1.hasDotNotation); // add allowedKeys if (allowedKeys) { // add $include keys to allowedKeys allowedKeys = compiled.fields.concat(includeFields).concat(allowedKeys); // remove SUPER_RULES from allowedKeys allowedKeys = allowedKeys.filter((key) => !exports.SuperKeys.Fields.has(key)); // make sure allowedKeys are unique compiled.allowedFields = Array.from(new Set(allowedKeys)); } return compiled; } /** * Compile for a variable * @param rule * @param CustomAbolish */ static compile(rule, CustomAbolish) { // process rules; rule = (0, functions_1.Rule)(rule); // compile const compiled = this.compileObject({ variable: rule, $include: ["variable"] }, CustomAbolish); // set exact rules received compiled.input = rule; // set object to false. compiled.isObject = false; return compiled; } } /** * Check if a variable can be considered as an Abolish Class * @param $class */ function isAbolishClass($class) { return typeof $class === "function" && typeof $class["addGlobalValidator"] === "function"; } /** * Check if a variable can be considered as an Abolish Instance * @param instance */ function isAbolishInstance(instance) { return (typeof instance === "object" && (instance instanceof Abolish || typeof instance["addValidator"] === "function")); } exports.default = Abolish;