decoval
Version:
DecoVal is a decorator-driven validation pattern that allows you to add validation rules directly to the properties of TypeScript classes, simplifying data control and increasing code readability and reusability.
153 lines (152 loc) • 7.86 kB
JavaScript
import { ContainsNumber } from "../helpers/containsNumber";
import { LowerCase } from "../helpers/lowerCase";
import { Optional } from "../helpers/optional";
import { SpecialChar } from "../helpers/specialChar";
import { UpperCase } from "../helpers/uppercase";
import { ValidationError } from "./error/validation.error";
import { registerValidation } from "./register.validation";
import { skipValidation } from "./skip/skip.validation";
export function DvText(options = {}) {
// Normalize options to ensure consistency
const { empty = false, null: allowNull = false, optional = false, lowercase = false, maxLength = 40, minLength = 3, number = false, specialChar = false, uppercase = false, noSpaces = false, emailProviders = false, regex = false, } = options;
// Check for conflicts at the start
if (lowercase && uppercase) {
throw new Error("The options 'lowercase' and 'uppercase' cannot be used together.");
}
if (optional && empty) {
throw new Error("The options 'optional' and 'empty' cannot be used together. 'optional' allows empty values, but 'empty' requires a non-empty value.");
}
// Normalize minLength and maxLength values
const minLen = typeof minLength === "object" ? minLength.value : minLength;
const maxLen = typeof maxLength === "object" ? maxLength.value : maxLength;
if (minLen > maxLen) {
throw new Error("The value of 'minLength' cannot be greater than 'maxLength'.");
}
return function (target, propertyKey) {
const alreadyApplied = Reflect.getMetadata("valtext:applied", target, propertyKey);
if (alreadyApplied) {
throw new Error(`The @ValText decorator has already been applied to the field "${propertyKey}".`);
}
Reflect.defineMetadata("valtext:applied", true, target, propertyKey);
// Helper to skip validation when necessary
const shouldSkipValidation = (value) => skipValidation(value, {
optional: typeof optional === "object" ? optional.value : optional,
null: typeof allowNull === "object" ? allowNull.value : allowNull,
empty: typeof empty === "object" ? empty.value : empty,
});
// Base validation to check string value and empty checks
registerValidation(target, propertyKey, (value) => {
if (shouldSkipValidation(value))
return;
if (typeof value !== "string") {
throw new ValidationError(`The "${propertyKey}" field must be a string.`);
}
if (!empty && value === "") {
throw new ValidationError(`The "${propertyKey}" field is required.`);
}
});
// Optional flag
if (optional)
Optional()(target, propertyKey);
// MinLength and MaxLength validation
const minLengthValue = typeof minLength === "object" ? minLength.value : minLength;
const maxLengthValue = typeof maxLength === "object" ? maxLength.value : maxLength;
const minLengthMessage = typeof minLength === "object"
? minLength.message
: `The "${propertyKey}" field must have at least ${minLengthValue} characters.`;
const maxLengthMessage = typeof maxLength === "object"
? maxLength.message
: `The "${propertyKey}" field must be at most ${maxLengthValue} characters long.`;
registerValidation(target, propertyKey, (value) => {
if (shouldSkipValidation(value))
return;
if (typeof value === "string" && value.length < minLengthValue) {
throw new ValidationError(minLengthMessage);
}
});
registerValidation(target, propertyKey, (value) => {
if (shouldSkipValidation(value))
return;
if (typeof value === "string" && value.length > maxLengthValue) {
throw new ValidationError(maxLengthMessage);
}
});
// Other validations
if (uppercase)
UpperCase(`The "${propertyKey}" field must contain only uppercase letters.`)(target, propertyKey);
if (lowercase)
LowerCase(`The "${propertyKey}" field must contain only lowercase letters.`)(target, propertyKey);
if (number)
ContainsNumber(`The "${propertyKey}" field must contain at least one number`)(target, propertyKey);
if (noSpaces) {
registerValidation(target, propertyKey, (value) => {
if (shouldSkipValidation(value))
return;
if (typeof value === "string" && /\s/.test(value)) {
throw new ValidationError(`The "${propertyKey}" field must not contain spaces.`);
}
});
}
if (specialChar)
SpecialChar(`The "${propertyKey}" field must contain at least one special character`)(target, propertyKey);
// Email providers validation
if (emailProviders) {
let allowed = [];
let customMessage = "";
if (typeof emailProviders === "string") {
allowed = [emailProviders];
customMessage = `The email must be from the provider ${allowed[0]}`;
}
else if (Array.isArray(emailProviders)) {
allowed = emailProviders;
customMessage = `The email must be from one of the following providers: ${allowed.join(", ")}`;
}
else if (typeof emailProviders === "object") {
allowed = Array.isArray(emailProviders.value) ? emailProviders.value : [emailProviders.value];
customMessage =
emailProviders.message || `The email must be from one of the following providers: ${allowed.join(", ")}`;
}
registerValidation(target, propertyKey, (value) => {
if (shouldSkipValidation(value))
return;
if (typeof value !== "string")
return;
const match = /^[^\s@]+@([^\s@]+\.[^\s@]+)$/i.exec(value);
if (!match || !match[1]) {
throw new ValidationError(`The "${propertyKey}" field must be a valid email.`);
}
const domain = match[1].toLowerCase();
if (!allowed.some((prov) => domain === prov.toLowerCase())) {
throw new ValidationError(customMessage);
}
});
}
// Regex validation
if (regex) {
let patterns = [];
let customMessage = "";
if (typeof regex === "string" || regex instanceof RegExp) {
patterns = [regex];
customMessage = `${propertyKey} does not match the required pattern.`;
}
else if (Array.isArray(regex)) {
patterns = regex;
customMessage = `${propertyKey} does not match any of the required patterns.`;
}
else if (typeof regex === "object") {
patterns = Array.isArray(regex.value) ? regex.value : [regex.value];
customMessage = regex.message || `${propertyKey} does not match any of the required patterns.`;
}
registerValidation(target, propertyKey, (value) => {
if (shouldSkipValidation(value))
return;
if (typeof value !== "string")
return;
const cleanValue = value.replace(/\s+/g, "");
const isValid = patterns.some((pattern) => typeof pattern === "string" ? cleanValue.startsWith(pattern) : pattern.test(cleanValue));
if (!isValid)
throw new ValidationError(customMessage);
});
}
};
}