abolish
Version:
A javascript object validator.
963 lines (962 loc) • 37.8 kB
JavaScript
"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;