validata-jsts
Version:
Dynamic, rule-based validation for JS/TS objects with support for strings, numbers, dates, booleans, arrays, and custom conditions.
553 lines (543 loc) • 19.1 kB
JavaScript
// src/core/extend.ts
var customValidators = {};
function extend(name, fn) {
if (name in customValidators) {
console.warn(`Validator '${name}' is being overridden`);
}
customValidators[name] = fn;
}
// src/core/parser.ts
function parseRule(rule) {
const parts = rule.split("-");
if (parts.length < 2) {
throw new Error(`Invalid rule format: ${rule}. Expected format: "fieldName-type-condition1-condition2"`);
}
const field = parts[0];
const type = parts[1];
let conditions = parts.slice(2);
let isRequired = false;
if (type.endsWith("!")) {
isRequired = true;
parts[1] = type.slice(0, -1);
}
conditions = conditions.map((condition) => {
if (condition.endsWith("!")) {
isRequired = true;
return condition.slice(0, -1);
}
return condition;
});
if (isRequired && !conditions.includes("req")) {
conditions.push("req");
}
return {
field,
type: parts[1],
// Use the potentially modified type
conditions,
originalRule: rule
};
}
// src/utils/constants.ts
var DEFAULT_ERROR_MESSAGES = {
string: "must be a string",
number: "must be a number",
boolean: "must be a boolean",
email: "must be a valid email",
array: "must be an array",
date: "must be a valid date",
pwd: "must contain one uppercase, one lowercase, one number, and one special character",
required: "is required",
objectId: "must be a valid ObjectId",
objectIdArray: "must be an array of ObjectIds",
objectIdOrString: "must be a valid ObjectId or string",
min: (min, type) => {
if (type === "string") return `must be at least ${min} characters`;
if (type === "number") return `must be greater than or equal to ${min}`;
if (type === "array") return `must have at least ${min} items`;
return `must be at least ${min}`;
},
max: (max, type) => {
if (type === "string") return `must be at most ${max} characters`;
if (type === "number") return `must be less than or equal to ${max}`;
if (type === "array") return `must have at most ${max} items`;
return `must be at most ${max}`;
}
};
// src/utils/helpers.ts
function normalizeRules(inputRules) {
const result = [];
if (Array.isArray(inputRules)) {
return inputRules;
}
for (const field in inputRules) {
const fieldRules = inputRules[field];
for (const rule of fieldRules) {
const isPrefixed = rule.startsWith(`${field}-`);
result.push(isPrefixed ? rule : `${field}-${rule}`);
}
}
return result;
}
function isEmail(value) {
if (typeof value !== "string") return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
}
function isValidPassword(value) {
if (typeof value !== "string") return false;
const hasUppercase = /[A-Z]/.test(value);
const hasLowercase = /[a-z]/.test(value);
const hasNumber = /[0-9]/.test(value);
const hasSpecial = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(value);
return hasUppercase && hasLowercase && hasNumber && hasSpecial;
}
function extractErrorMessage(conditions, failedCondition) {
if (!conditions) return null;
const errorCondition = conditions.find((c) => c.startsWith(`valerr:${failedCondition}`));
return errorCondition ? errorCondition.substring(7) : null;
}
function getValueFromPath(path, obj) {
const parts = path.split(".");
let current = obj;
for (const part of parts) {
if (current === void 0 || current === null) {
return void 0;
}
current = current[part];
}
return current;
}
function isValidObjectId(value) {
if (typeof value !== "string") return false;
return /^[0-9a-fA-F]{24}$/.test(value);
}
function isValidDate(value) {
if (value instanceof Date) return !isNaN(value.getTime());
const date = new Date(value);
return !isNaN(date.getTime());
}
function getErrorMessage(type, params) {
const errorMessage = DEFAULT_ERROR_MESSAGES[type];
if (typeof errorMessage === "string") {
return errorMessage;
}
if (typeof errorMessage === "function") {
if (type === "min" && params?.min !== void 0 && params?.type) {
return errorMessage(params.min, params.type);
}
if (type === "max" && params?.max !== void 0 && params?.type) {
return errorMessage(params.max, params.type);
}
}
return "Unknown error";
}
// src/core/builtInValidator.ts
var builtInValidators = {
// Type validators
string: (value) => {
if (typeof value !== "string") return getErrorMessage("string");
if (value.trim() === "") return "must be a non-empty string";
return false;
},
number: (value) => {
return typeof value === "number" ? false : getErrorMessage("number");
},
boolean: (value) => {
return typeof value === "boolean" ? false : getErrorMessage("boolean");
},
email: (value) => {
return isEmail(value) ? false : getErrorMessage("email");
},
array: (value) => {
return Array.isArray(value) ? false : getErrorMessage("array");
},
date: (value, conditions) => {
if (!isValidDate(value)) return getErrorMessage("date");
const dateValue = new Date(value);
const exactCondition = conditions?.find((c) => /^\d+$/.test(c));
if (exactCondition) {
const exactDate = new Date(exactCondition.replace(/_/g, "-"));
if (dateValue.toISOString().split("T")[0] !== exactDate.toISOString().split("T")[0]) {
return `must be exactly ${exactDate.toISOString().split("T")[0]}`;
}
}
const minCondition = conditions?.find((c) => c.startsWith("min"));
if (minCondition) {
const minDate = new Date(minCondition.substring(3).replace(/_/g, "-"));
if (dateValue < minDate) {
return `must be after ${minDate.toISOString().split("T")[0]}`;
}
}
const maxCondition = conditions?.find((c) => c.startsWith("max"));
if (maxCondition) {
const maxDate = new Date(maxCondition.substring(3).replace(/_/g, "-"));
if (dateValue > maxDate) {
return `must be before ${maxDate.toISOString().split("T")[0]}`;
}
}
return false;
},
objectid: (value) => {
return isValidObjectId(value) ? false : "must be a valid MongoDB ObjectId";
},
pwd: (value, conditions) => {
if (typeof value !== "string") return getErrorMessage("string");
const exactCondition = conditions?.find((c) => /^\d+$/.test(c));
const minCondition = conditions?.find((c) => c.startsWith("min"));
const maxCondition = conditions?.find((c) => c.startsWith("max"));
const lengthToUse = exactCondition ? Number.parseInt(exactCondition, 10) : minCondition ? Number.parseInt(minCondition.substring(3), 10) : 8;
if (value.length < lengthToUse) {
return getErrorMessage("min", { min: lengthToUse, type: "string" });
}
if (maxCondition) {
const maxValue = Number.parseInt(maxCondition.substring(3), 10);
if (value.length > maxValue) {
return getErrorMessage("max", { max: maxValue, type: "string" });
}
}
if (conditions?.includes("any")) {
return false;
}
return isValidPassword(value) ? false : "must include uppercase, lowercase, number, and special character";
}
};
// src/core/ruleExecutor.ts
function executeRule(field, type, conditions, value, config) {
const errors = [];
const customError = (cond) => extractErrorMessage(conditions, cond);
if (conditions.includes("req") && (value === void 0 || value === null)) {
if (value === "") {
errors.push(formatError(field, "is empty but is required"));
return errors.length > 0 ? errors.join(", ") : false;
}
errors.push(formatError(field, "is required"));
return errors.length > 0 ? errors.join(", ") : false;
}
if (value === void 0 || value === null) {
return errors.length > 0 ? errors.join(", ") : false;
}
const exactCondition = conditions.find((c) => /^\d+$/.test(c));
if (exactCondition && (typeof value === "string" || Array.isArray(value))) {
const exactValue = Number.parseInt(exactCondition, 10);
if (value.length !== exactValue) {
errors.push(
formatError(field, `must be exactly ${exactValue} characters long, currently ${value.length}`)
);
return errors.length > 0 ? errors.join(", ") : false;
}
}
if (type.startsWith("array<") && Array.isArray(value)) {
const isValid = checkSingleType(value, type);
if (!isValid) {
errors.push(formatError(field, `Invalid value for ${field}. Expected type: ${type}`));
return errors.length > 0 ? errors.join(", ") : false;
}
return false;
}
if (type in builtInValidators) {
const error = builtInValidators[type](value, conditions, config);
if (error) {
errors.push(formatError(field, error));
return errors.length > 0 ? errors.join(", ") : false;
}
} else if (customValidators[type]) {
const error = customValidators[type](value, conditions, config);
if (error) {
errors.push(formatError(field, error));
}
}
for (const condition of conditions) {
if (condition === "req" || condition === "optional" || condition.startsWith("valerr:") || /^\d+$/.test(condition))
continue;
if (isMinMaxCondition(condition)) {
const error = processMinMaxCondition(condition, value, config);
if (error) {
errors.push(formatError(field, error));
}
continue;
}
if (condition.startsWith("enum:")) {
const error = processEnumCondition(condition, value);
if (error) {
errors.push(formatError(field, error));
}
continue;
}
if (condition in builtInValidators) {
const error = builtInValidators[condition](value, conditions, config);
if (error) {
errors.push(formatError(field, error));
}
} else if (customValidators[condition]) {
const error = customValidators[condition](value, [], config);
if (error) {
errors.push(formatError(field, error));
}
}
}
return errors.length > 0 ? errors.join(", ") : false;
}
function isMinMaxCondition(condition) {
return /^(min|max)\d+$/.test(condition);
}
function processMinMaxCondition(condition, value, config) {
if (condition.startsWith("min")) {
const minValue = Number.parseInt(condition.substring(3));
if (typeof value === "string") {
return value.length >= minValue ? false : `must be at least ${minValue} characters long`;
}
if (typeof value === "number") {
return value >= minValue ? false : `must be greater than or equal to ${minValue}`;
}
if (Array.isArray(value)) {
return value.length >= minValue ? false : `must have at least ${minValue} items`;
}
}
if (condition.startsWith("max")) {
const maxValue = Number.parseInt(condition.substring(3));
if (typeof value === "string") {
return value.length <= maxValue ? false : `must be no more than ${maxValue} characters long`;
}
if (typeof value === "number") {
return value <= maxValue ? false : `must be less than or equal to ${maxValue}`;
}
if (Array.isArray(value)) {
return value.length <= maxValue ? false : `must have no more than ${maxValue} items`;
}
}
return false;
}
function processEnumCondition(condition, value) {
const allowedValues = condition.substring(5).split("|");
return allowedValues.includes(value) ? false : `must be one of: ${allowedValues.join(", ")}`;
}
function checkSingleType(value, type) {
if (type.startsWith("array<")) {
if (!Array.isArray(value)) return false;
const innerType = extractInnerArrayType(type);
return value.every((item) => checkSingleType(item, innerType));
}
switch (type) {
case "string":
return typeof value === "string";
case "number":
return typeof value === "number";
case "boolean":
return typeof value === "boolean";
case "objectId":
return typeof value === "string" && /^[a-fA-F0-9]{24}$/.test(value);
case "date":
return value instanceof Date || typeof value === "string" && !isNaN(Date.parse(value));
default:
return true;
}
}
function extractInnerArrayType(type) {
let depth = 0;
let current = type;
while (current.startsWith("array<")) {
current = current.slice(6, -1);
depth++;
}
return type.slice(6, -1);
}
function formatError(field, error) {
return `${field}: ${error}`;
}
// src/core/validataData.ts
function Validata(inputRules, data, config) {
const rules = normalizeRules(inputRules);
const errors = [];
if (!Array.isArray(rules) || typeof data !== "object" || data === null) {
throw new Error("Invalid arguments: rules must be an array and data must be an object");
}
for (const rule of rules) {
const { field, type, conditions } = parseRule(rule);
let value = getValueFromPath(field, data);
const defCond = conditions.find((cond) => cond.startsWith("def:"));
if ((value === void 0 || value === null) && defCond) {
try {
let parsed = JSON.parse(defCond.substring(4));
if (rule.includes("date")) {
parsed = new Date(parsed);
}
value = parsed;
data[field] = value;
} catch (e) {
let fallback = defCond.substring(4);
if (rule.includes("date")) {
fallback = new Date(fallback).toISOString();
}
value = fallback;
data[field] = fallback;
}
}
if (conditions.includes("optional") && value === void 0) {
continue;
}
if (value === void 0 && !conditions.includes("req")) {
continue;
}
if (conditions.includes("validate:fn") && customValidators[field]) {
const result = customValidators[field](value, data, config);
if (result) {
errors.push(`${field} ${result}`);
}
}
const fieldErrors = executeRule(field, type, conditions, value, config);
if (fieldErrors) {
errors.push(fieldErrors);
}
}
return errors.length > 0 ? errors.join(", ") : false;
}
// src/mongoose/extractRules.ts
function extractRulesFromSchema(input) {
const schema = "schema" in input ? input.schema : input;
const rules = [];
const paths = schema.paths || {};
for (const [field, path] of Object.entries(paths)) {
if (field.startsWith("_")) continue;
const simplifiedPath = path;
const rule = buildRuleFromPath(field, simplifiedPath);
if (rule) rules.push(rule);
}
return rules;
}
function buildRuleFromPath(field, path) {
if (!path || !path.instance) return null;
const lowered = field.toLowerCase();
const type = fieldNameTypeOverrides[lowered] ?? getActualType(path);
const conditions = [];
if (isActuallyRequired(path)) {
conditions.push("req");
}
if (typeof path.options?.min === "number") {
conditions.push(`min${path.options.min}`);
}
if (typeof path.options?.max === "number") {
conditions.push(`max${path.options.max}`);
}
if (path.instance === "String") {
const minLen = extractValue(path.options?.minlength);
const maxLen = extractValue(path.options?.maxlength);
const minMsg = extractMessage(path.options?.minlength);
const maxMsg = extractMessage(path.options?.maxlength);
const isReq = extractValue(path.options?.required);
const reqMsg = extractMessage(path.options?.required);
if (minLen != null) {
const msg = minMsg || `${field} must be more than ${minLen} characters`;
conditions.push(`min${minLen}-valerr:${msg}`);
}
if (maxLen != null) {
const msg = maxMsg || `${field} cannot be more than ${maxLen} characters`;
conditions.push(`max${maxLen}-valerr:${msg}`);
}
if (isReq) {
const msg = reqMsg || `${field} is required`;
conditions.push(`valerr:${msg}`);
}
}
const enumVals = path.enumValues ?? path.options?.enum;
if (Array.isArray(enumVals) && enumVals.length > 0) {
conditions.push(`enum:${enumVals.join("|")}`);
}
if (typeof path.options?.message === "string" && path.options.message.trim() !== "") {
conditions.push(`err:${path.options.message}`);
}
if (path.options?.default !== void 0) {
let defValue = path.options.default;
if (typeof defValue === "function") {
try {
defValue = defValue();
} catch (e) {
defValue = void 0;
}
}
const isValidDefault = Array.isArray(defValue) && defValue.length > 0 || typeof defValue === "string" && defValue.trim() !== "" || (typeof defValue === "number" || typeof defValue === "boolean") || defValue && typeof defValue === "object" && !Array.isArray(defValue) && Object.keys(defValue).length > 0;
if (isValidDefault) {
conditions.push(`def:${typeof defValue === "string" ? defValue : JSON.stringify(defValue)}`);
}
}
if (typeof path.options?.alias === "string" && path.options.alias.trim() !== "") {
conditions.push(`alias:${path.options.alias}`);
}
if (typeof path.options?.select === "boolean") {
conditions.push(`select:${path.options.select}`);
}
if (path.options?.validate) {
if (typeof path.options.validate === "function") {
conditions.push("validate:fn");
} else if (typeof path.options.validate === "object" && typeof path.options.validate.validator === "function") {
conditions.push("validate:fn");
if (typeof path.options.validate.message === "string" && path.options.validate.message.trim() !== "") {
conditions.push(`valerr:${path.options.validate.message}`);
}
}
}
return `${field}-${type}${conditions.length > 0 ? "-" + conditions.join("-") : ""}`;
}
function isActuallyRequired(path) {
const requiredOption = path?.options?.required;
if (Array.isArray(requiredOption)) {
return requiredOption[0] === true;
}
return requiredOption === true;
}
function mapMongooseTypeToValidataType(mongooseType) {
const typeMap = {
String: "string",
Number: "number",
Boolean: "boolean",
Date: "date",
Array: "array",
ObjectId: "objectId",
ObjectID: "objectId",
Decimal128: "decimal",
Buffer: "buffer",
Mixed: "mixed",
Map: "map"
};
return typeMap[mongooseType] || "string";
}
function getActualType(path) {
if (path.instance === "Array" && path.caster?.instance) {
const casterType = path.caster.instance;
return `array<${mapMongooseTypeToValidataType(casterType)}>`;
}
return mapMongooseTypeToValidataType(path.instance);
}
var fieldNameTypeOverrides = {
email: "email",
password: "pwd",
pwd: "pwd",
contactEmail: "email",
user_email: "email",
newPassword: "pwd",
confirmPassword: "pwd"
};
var extractValue = (option) => {
return Array.isArray(option) ? option[0] : option;
};
var extractMessage = (option) => {
return Array.isArray(option) && typeof option[1] === "string" ? option[1] : void 0;
};
// src/mongoose/validataWithSchema.ts
function validateWithSchema(schema, data, config) {
const rules = extractRulesFromSchema(schema);
return Validata(rules, data, config);
}
// src/index.ts
var isNotValidata = Validata;
var isInValidata = Validata;
var isInValiData = Validata;
export {
Validata,
extend as extendValidata,
extractRulesFromSchema,
isInValiData,
isInValidata,
isNotValidata,
validateWithSchema
};