UNPKG

@decaf-ts/decorator-validation

Version:
672 lines 25.9 kB
import { DEFAULT_ERROR_MESSAGES, DEFAULT_PATTERNS, ValidationKeys, } from "./Validators/constants.js"; import { sf } from "./../utils/strings.js"; import { parseDate } from "./../utils/dates.js"; import { Validation } from "./Validation.js"; import { Decoration, apply, propMetadata, } from "@decaf-ts/decoration"; import { ASYNC_META_KEY } from "./../constants/index.js"; /** * @description Combined property decorator factory for metadata and attribute marking * @summary Creates a decorator that both marks a property as a model attribute and assigns metadata to it * * @template V * @param {PropertyDecorator} decorator - The metadata key * @param {string} key - The metadata key * @param {V} value - The metadata value to associate with the property * @return {Function} - Combined decorator function * @function validationMetadata * @category Property Decorators */ export function validationMetadata(decorator, key, value) { return apply(propMetadata(key, value)); } export function innerValidationDecorator(dec, key, meta) { Validation.registerDecorator(key, dec); return function innerValidationDecorator(obj, prop) { return validationMetadata(dec, `${ValidationKeys.REFLECT}.${prop}.${key}`, meta)(obj, prop); }; } export function async() { return (model) => { if (!Object.prototype.hasOwnProperty.call(model, ASYNC_META_KEY)) model[ASYNC_META_KEY] = true; }; } /** * @description Property decorator that marks a field as required * @summary Marks the property as required, causing validation to fail if the property is undefined, null, or empty. * Validators to validate a decorated property must use key {@link ValidationKeys#REQUIRED}. * This decorator is commonly used as the first validation step for important fields. * * @param {string} [message] - The error message to display when validation fails. Defaults to {@link DEFAULT_ERROR_MESSAGES#REQUIRED} * @return {PropertyDecorator} A decorator function that can be applied to class properties * * @function required * @category Property Decorators * * @example * ```typescript * class User { * @required() * username: string; * * @required("Email address is mandatory") * email: string; * } * ``` */ export function required(message = DEFAULT_ERROR_MESSAGES.REQUIRED) { const key = ValidationKeys.REQUIRED; const meta = { message: message, description: `defines the attribute as required`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [required, key, meta], }) .apply(); } /** * @description Property decorator that enforces a minimum value constraint * @summary Defines a minimum value for the property, causing validation to fail if the property value is less than the specified minimum. * Validators to validate a decorated property must use key {@link ValidationKeys#MIN}. * This decorator works with numeric values and dates. * * @param {number | Date | string} value - The minimum value allowed. For dates, can be a Date object or a string that can be converted to a date * @param {string} [message] - The error message to display when validation fails. Defaults to {@link DEFAULT_ERROR_MESSAGES#MIN} * @return {PropertyDecorator} A decorator function that can be applied to class properties * * @function min * @category Property Decorators * * @example * ```typescript * class Product { * @min(0) * price: number; * * @min(new Date(2023, 0, 1), "Date must be after January 1, 2023") * releaseDate: Date; * } * ``` */ export function min(value, message = DEFAULT_ERROR_MESSAGES.MIN) { const key = ValidationKeys.MIN; const meta = { [ValidationKeys.MIN]: value, message: message, description: `defines the max value of the attribute as ${value} (applies to numbers or Dates)`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [min, key, meta], }) .apply(); } /** * @summary Defines a maximum value for the property * @description Validators to validate a decorated property must use key {@link ValidationKeys#MAX} * * @param {number | Date} value * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#MAX} * * @function max * @category Property Decorators */ export function max(value, message = DEFAULT_ERROR_MESSAGES.MAX) { const key = ValidationKeys.MAX; const meta = { [ValidationKeys.MAX]: value, message: message, description: `defines the max value of the attribute as ${value} (applies to numbers or Dates)`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [max, key, meta], }) .apply(); } /** * @summary Defines a step value for the property * @description Validators to validate a decorated property must use key {@link ValidationKeys#STEP} * * @param {number} value * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#STEP} * * @function step * @category Property Decorators */ export function step(value, message = DEFAULT_ERROR_MESSAGES.STEP) { const key = ValidationKeys.STEP; const meta = { [ValidationKeys.STEP]: value, message: message, description: `defines the step of the attribute as ${value}`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [step, key, meta], }) .apply(); } /** * @summary Defines a minimum length for the property * @description Validators to validate a decorated property must use key {@link ValidationKeys#MIN_LENGTH} * * @param {string} value * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#MIN_LENGTH} * * @function minlength * @category Property Decorators */ export function minlength(value, message = DEFAULT_ERROR_MESSAGES.MIN_LENGTH) { const key = ValidationKeys.MIN_LENGTH; const meta = { [ValidationKeys.MIN_LENGTH]: value, message: message, description: `defines the min length of the attribute as ${value} (applies to strings or lists)`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [minlength, key, meta], }) .apply(); } /** * @summary Defines a maximum length for the property * @description Validators to validate a decorated property must use key {@link ValidationKeys#MAX_LENGTH} * * @param {string} value * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#MAX_LENGTH} * * @function maxlength * @category Property Decorators */ export function maxlength(value, message = DEFAULT_ERROR_MESSAGES.MAX_LENGTH) { const key = ValidationKeys.MAX_LENGTH; const meta = { [ValidationKeys.MAX_LENGTH]: value, message: message, description: `defines the max length of the attribute as ${value} (applies to strings or lists)`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [maxlength, key, meta], }) .apply(); } /** * @summary Defines a RegExp pattern the property must respect * @description Validators to validate a decorated property must use key {@link ValidationKeys#PATTERN} * * @param {string} value * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#PATTERN} * * @function pattern * @category Property Decorators */ export function pattern(value, message = DEFAULT_ERROR_MESSAGES.PATTERN) { const key = ValidationKeys.PATTERN; const meta = { [ValidationKeys.PATTERN]: typeof value === "string" ? value : value.toString(), message: message, description: `assigns the ${value === "string" ? value : value.toString()} pattern to the attribute`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [pattern, key, meta], }) .apply(); } /** * @summary Defines the property as an email * @description Validators to validate a decorated property must use key {@link ValidationKeys#EMAIL} * * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#EMAIL} * * @function email * @category Property Decorators */ export function email(message = DEFAULT_ERROR_MESSAGES.EMAIL) { const key = ValidationKeys.EMAIL; const meta = { [ValidationKeys.PATTERN]: DEFAULT_PATTERNS.EMAIL.toString(), message: message, description: "marks the attribute as an email", async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [email, key, meta], }) .apply(); } /** * @summary Defines the property as an URL * @description Validators to validate a decorated property must use key {@link ValidationKeys#URL} * * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#URL} * * @function url * @category Property Decorators */ export function url(message = DEFAULT_ERROR_MESSAGES.URL) { const key = ValidationKeys.URL; const meta = { [ValidationKeys.PATTERN]: DEFAULT_PATTERNS.URL.toString(), message: message, description: "marks the attribute as an url", async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [url, key, meta], }) .apply(); } /** * @summary Enforces type verification * @description Validators to validate a decorated property must use key {@link ValidationKeys#TYPE} * * @param {Constructor[] | Constructor} types accepted types * @param {Constructor} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#TYPE} * * @function type * @category Property Decorators */ //TODO export function type(types, message = DEFAULT_ERROR_MESSAGES.TYPE) { const key = ValidationKeys.TYPE; const meta = { customTypes: types, message: message, description: "defines the accepted types for the attribute", async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [type, key, meta], }) .apply(); } /** * @summary Date Handler Decorator * @description Validators to validate a decorated property must use key {@link ValidationKeys#DATE} * * Will enforce serialization according to the selected format * * @param {string} format accepted format according to {@link formatDate} * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#DATE} * * @function date * * @category Property Decorators */ export function date(format = "dd-MM-yyyy HH:mm:ss:S", message = DEFAULT_ERROR_MESSAGES.DATE) { const key = ValidationKeys.DATE; function innerDateDec(format, message) { const meta = { [ValidationKeys.FORMAT]: format, message: message, description: `defines the attribute as a date with the format ${format}`, async: false, }; // return function dateDec( // target: Record<string, any>, // propertyKey?: any // ): any { // const definitionTarget = // typeof target === "function" ? target.prototype : target; // const values = new WeakMap<any, Date | undefined>(); // // const ensureInstanceDescriptor = (instance: any) => { // if ( // Object.prototype.hasOwnProperty.call(instance, propertyKey) && // !Object.getOwnPropertyDescriptor(instance, propertyKey)?.configurable // ) // return; // // Object.defineProperty(instance, propertyKey, { // enumerable: true, // configurable: false, // get(this: any) { // return values.get(this); // }, // set(this: any, newValue: string | Date | number | undefined | null) { // if (typeof newValue === "undefined" || newValue === null) { // values.delete(this); // return; // } // // try { // const val = parseDate(format, newValue); // values.set(this, val); // } catch (e: any) { // console.error(sf("Failed to parse date: {0}", e.message || e)); // } // }, // }); // }; // // Object.defineProperty(definitionTarget, propertyKey, { // configurable: true, // enumerable: true, // get(this: any) { // ensureInstanceDescriptor(this); // return (this as any)[propertyKey]; // }, // set(this: any, newValue: string | Date | number | undefined | null) { // ensureInstanceDescriptor(this); // (this as any)[propertyKey] = newValue; // }, // }); // // return innerValidationDecorator(date, key, meta)(target, propertyKey); // }; return function dateDec(target, propertyKey) { const values = new WeakMap(); Object.defineProperty(target, propertyKey, { configurable: true, set(newValue) { const descriptor = Object.getOwnPropertyDescriptor(this, propertyKey); if (!descriptor || descriptor.configurable) Object.defineProperty(this, propertyKey, { enumerable: true, configurable: false, get: () => values.get(this), set: (newValue) => { let val; try { val = parseDate(format, newValue); values.set(this, val); } catch (e) { console.error(sf("Failed to parse date: {0}", e.message || e)); } }, }); this[propertyKey] = newValue; }, get() { return values.get(this); }, }); return innerValidationDecorator(date, key, meta)(target, propertyKey); }; } return Decoration.for(key) .define({ decorator: innerDateDec, args: [format, message], }) .apply(); } /** * @summary Password Handler Decorator * @description Validators to validate a decorated property must use key {@link ValidationKeys#PASSWORD} * * @param {RegExp} [pattern] defaults to {@link DEFAULT_PATTERNS#CHAR8_ONE_OF_EACH} * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#PASSWORD} * * @function password * * @category Property Decorators */ export function password(pattern = DEFAULT_PATTERNS.PASSWORD.CHAR8_ONE_OF_EACH, message = DEFAULT_ERROR_MESSAGES.PASSWORD) { const key = ValidationKeys.PASSWORD; const meta = { [ValidationKeys.PATTERN]: pattern.toString(), message: message, description: `attribute as a password`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [password, key, meta], }) .apply(); } /** * @summary List Decorator * @description Also sets the {@link type} to the provided collection * * @param {ModelConstructor} clazz * @param {string} [collection] The collection being used. defaults to Array * @param {string} [message] defaults to {@link DEFAULT_ERROR_MESSAGES#LIST} * * @function list * * @category Property Decorators */ export function list(clazz, collection = "Array", message = DEFAULT_ERROR_MESSAGES.LIST) { const key = ValidationKeys.LIST; const meta = { clazz: (Array.isArray(clazz) ? clazz : [clazz]), type: collection, message: message, async: false, description: `defines the attribute as a ${collection} of ${clazz.name}`, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [list, key, meta], }) .apply(); } /** * @summary Set Decorator * @description Wrapper for {@link list} with the 'Set' Collection * * @param {ModelConstructor} clazz * @param {string} [message] defaults to {@link DEFAULT_ERROR_MESSAGES#LIST} * * @function set * * @category Property Decorators */ export function set(clazz, message = DEFAULT_ERROR_MESSAGES.LIST) { return list(clazz, "Set", message); } /** * @summary Declares that the decorated property must be equal to another specified property. * @description Applies the {@link ValidationKeys.EQUALS} validator to ensure the decorated value matches the value of the given property. * * @param {string} propertyToCompare - The name of the property to compare equality against. * @param {ComparisonValidatorOptions} options - Options for the validator. * @param {string} [options.label] - The label text displayed in the error message. * @param {string} [options.message=DEFAULT_ERROR_MESSAGES.EQUALS] - Custom error message to be returned if validation fails. * * @returns {PropertyDecorator} A property decorator used to register the equality validation metadata. * * @function eq * @category Property Decorators */ export function eq(propertyToCompare, options // message: string = DEFAULT_ERROR_MESSAGES.EQUALS ) { const equalsOptions = { label: options?.label || propertyToCompare, message: options?.message || DEFAULT_ERROR_MESSAGES.EQUALS, [ValidationKeys.EQUALS]: propertyToCompare, description: `defines attribute as equal to ${propertyToCompare}`, }; return innerValidationDecorator(eq, ValidationKeys.EQUALS, { ...equalsOptions, async: false, }); } /** * @summary Declares that the decorated property must be different from another specified property. * @description Applies the {@link ValidationKeys.DIFF} validator to ensure the decorated value is different from the value of the given property. * * @param {string} propertyToCompare - The name of the property to compare difference against. * @param {ComparisonValidatorOptions} options - Options for the validator. * @param {string} [options.label] - The label text displayed in the error message. * @param {string} [options.message=DEFAULT_ERROR_MESSAGES.DIFF] - Custom error message to be returned if validation fails. * * @returns {PropertyDecorator} A property decorator used to register the difference validation metadata. * * @function diff * @category Property Decorators */ export function diff(propertyToCompare, options) { const diffOptions = { label: options?.label || propertyToCompare, message: options?.message || DEFAULT_ERROR_MESSAGES.DIFF, [ValidationKeys.DIFF]: propertyToCompare, description: `defines attribute as different to ${propertyToCompare}`, }; return innerValidationDecorator(diff, ValidationKeys.DIFF, { ...diffOptions, async: false, }); } /** * @summary Declares that the decorated property must be less than another specified property. * @description Applies the {@link ValidationKeys.LESS_THAN} validator to ensure the decorated value is less than the value of the given property. * * @param {string} propertyToCompare - The name of the property to compare against. * @param {ComparisonValidatorOptions} options - Options for the validator. * @param {string} [options.label] - The label text displayed in the error message. * @param {string} [options.message=DEFAULT_ERROR_MESSAGES.LESS_THAN] - Custom error message to be returned if validation fails. * * @returns {PropertyDecorator} A property decorator used to register the less than validation metadata. * * @function lt * @category Property Decorators */ export function lt(propertyToCompare, options) { const ltOptions = { label: options?.label || propertyToCompare, message: options?.message || DEFAULT_ERROR_MESSAGES.LESS_THAN, [ValidationKeys.LESS_THAN]: propertyToCompare, description: `defines attribute as less than to ${propertyToCompare}`, }; return innerValidationDecorator(lt, ValidationKeys.LESS_THAN, { ...ltOptions, async: false, }); } /** * @summary Declares that the decorated property must be equal or less than another specified property. * @description Applies the {@link ValidationKeys.LESS_THAN_OR_EQUAL} validator to ensure the decorated value is equal or less than the value of the given property. * * @param {string} propertyToCompare - The name of the property to compare against. * @param {ComparisonValidatorOptions} options - Options for the validator. * @param {string} [options.label] - The label text displayed in the error message. * @param {string} [options.message=DEFAULT_ERROR_MESSAGES.LESS_THAN_OR_EQUAL] - Custom error message to be returned if validation fails. * * @returns {PropertyDecorator} A property decorator used to register the less than or equal validation metadata. * * @function lte * @category Property Decorators */ export function lte(propertyToCompare, options) { const lteOptions = { label: options?.label || propertyToCompare, message: options?.message || DEFAULT_ERROR_MESSAGES.LESS_THAN_OR_EQUAL, [ValidationKeys.LESS_THAN_OR_EQUAL]: propertyToCompare, description: `defines attribute as less or equal to ${propertyToCompare}`, }; return innerValidationDecorator(lte, ValidationKeys.LESS_THAN_OR_EQUAL, { ...lteOptions, async: false, }); } /** * @summary Declares that the decorated property must be greater than another specified property. * @description Applies the {@link ValidationKeys.GREATER_THAN} validator to ensure the decorated value is greater than the value of the given property. * * @param {string} propertyToCompare - The name of the property to compare against. * @param {ComparisonValidatorOptions} options - Options for the validator. * @param {string} [options.label] - The label text displayed in the error message. * @param {string} [options.message=DEFAULT_ERROR_MESSAGES.GREATER_THAN] - Custom error message to be returned if validation fails. * * @returns {PropertyDecorator} A property decorator used to register the greater than validation metadata. * * @function gt * @category Property Decorators */ export function gt(propertyToCompare, options) { const gtOptions = { label: options?.label || propertyToCompare, message: options?.message || DEFAULT_ERROR_MESSAGES.GREATER_THAN, [ValidationKeys.GREATER_THAN]: propertyToCompare, description: `defines attribute as greater than ${propertyToCompare}`, }; return innerValidationDecorator(gt, ValidationKeys.GREATER_THAN, { ...gtOptions, async: false, }); } /** * @summary Declares that the decorated property must be equal or greater than another specified property. * @description Applies the {@link ValidationKeys.GREATER_THAN_OR_EQUAL} validator to ensure the decorated value is equal or greater than the value of the given property. * * @param {string} propertyToCompare - The name of the property to compare against. * @param {ComparisonValidatorOptions} options - Options for the validator. * @param {string} [options.label] - The label text displayed in the error message. * @param {string} [options.message=DEFAULT_ERROR_MESSAGES.GREATER_THAN_OR_EQUAL] - Custom error message to be returned if validation fails. * * @returns {PropertyDecorator} A property decorator used to register the greater than or equal validation metadata. * * @function gte * @category Property Decorators */ export function gte(propertyToCompare, options) { const gteOptions = { label: options?.label || propertyToCompare, message: options?.message || DEFAULT_ERROR_MESSAGES.GREATER_THAN_OR_EQUAL, [ValidationKeys.GREATER_THAN_OR_EQUAL]: propertyToCompare, description: `defines attribute as greater or equal to ${propertyToCompare}`, }; return innerValidationDecorator(gte, ValidationKeys.GREATER_THAN_OR_EQUAL, { ...gteOptions, async: false, }); } /** * @summary Defines a list or an object of accepted values for the property * @description Validators to validate a decorated property must use key {@link ValidationKeys#ENUM} * * @param {any[] | Record<any, any>} value * @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#ENUM} * * @function option * @category Property Decorators */ export function option(value, message = DEFAULT_ERROR_MESSAGES.ENUM) { const key = ValidationKeys.ENUM; const meta = { [ValidationKeys.ENUM]: value, message: message, description: `defines a list or an object of accepted values for the attribute`, async: false, }; return Decoration.for(key) .define({ decorator: innerValidationDecorator, args: [option, key, meta], }) .apply(); } //# sourceMappingURL=decorators.js.map