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.
189 lines (156 loc) • 7.4 kB
text/typescript
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 { ValTextOptions } from "../types/dv/dv.text";
import { ValidationError } from "./error/validation.error";
import { registerValidation } from "./register.validation";
import { skipValidation } from "./skip/skip.validation";
export function DvText(options: ValTextOptions = {}) {
// 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: any, propertyKey: string) {
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: any) =>
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: any) => {
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: any) => {
if (shouldSkipValidation(value)) return;
if (typeof value === "string" && value.length < minLengthValue) {
throw new ValidationError(minLengthMessage as string);
}
});
registerValidation(target, propertyKey, (value: any) => {
if (shouldSkipValidation(value)) return;
if (typeof value === "string" && value.length > maxLengthValue) {
throw new ValidationError(maxLengthMessage as string);
}
});
// 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: any) => {
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: string[] = [];
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: any) => {
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: (string | RegExp)[] = [];
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: any) => {
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);
});
}
};
}