import "reflect-metadata";
import {
DateValidatorOptions,
DiffValidatorOptions,
EqualsValidatorOptions,
GreaterThanOrEqualValidatorOptions,
GreaterThanValidatorOptions,
LessThanOrEqualValidatorOptions,
LessThanValidatorOptions,
ListValidatorOptions,
MaxLengthValidatorOptions,
MaxValidatorOptions,
MinLengthValidatorOptions,
MinValidatorOptions,
PatternValidatorOptions,
StepValidatorOptions,
ValidationMetadata,
ValidatorOptions,
} from "./types";
import {
DEFAULT_ERROR_MESSAGES,
DEFAULT_PATTERNS,
ValidationKeys,
} from "./Validators/constants";
import { sf } from "../utils/strings";
import { Constructor, ModelConstructor } from "../model/types";
import { parseDate } from "../utils/dates";
import { propMetadata } from "../utils/decorators";
import { Validation } from "./Validation";
import { Decoration } from "../utils/Decoration";
import { apply } from "@decaf-ts/reflection";
import { ASYNC_META_KEY } from "../constants";
/**
* @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<V>(decorator: any, key: string, value: V) {
Validation.registerDecorator(key, decorator);
return apply(propMetadata<V>(key, value));
}
export function async() {
return (model: object): void => {
if (!Object.prototype.hasOwnProperty.call(model, ASYNC_META_KEY))
(model as any)[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: string = DEFAULT_ERROR_MESSAGES.REQUIRED) {
const key = Validation.key(ValidationKeys.REQUIRED);
const meta: ValidatorOptions = {
message: message,
description: `defines the attribute as required`,
async: false,
};
return Decoration.for(key)
.define(validationMetadata<ValidatorOptions>(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: number | Date | string,
message: string = DEFAULT_ERROR_MESSAGES.MIN
) {
const key = Validation.key(ValidationKeys.MIN);
const meta: MinValidatorOptions = {
[ValidationKeys.MIN]: value,
message: message,
types: [Number.name, Date.name],
description: `defines the max value of the attribute as ${value} (applies to numbers or Dates)`,
async: false,
};
return Decoration.for(key)
.define(validationMetadata<MinValidatorOptions>(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: number | Date | string,
message: string = DEFAULT_ERROR_MESSAGES.MAX
) {
const key = Validation.key(ValidationKeys.MAX);
const meta: MaxValidatorOptions = {
[ValidationKeys.MAX]: value,
message: message,
types: [Number.name, Date.name],
description: `defines the max value of the attribute as ${value} (applies to numbers or Dates)`,
async: false,
};
return Decoration.for(key)
.define(validationMetadata<MaxValidatorOptions>(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: number,
message: string = DEFAULT_ERROR_MESSAGES.STEP
) {
const key = Validation.key(ValidationKeys.STEP);
const meta: StepValidatorOptions = {
[ValidationKeys.STEP]: value,
message: message,
types: [Number.name],
description: `defines the step of the attribute as ${value}`,
async: false,
};
return Decoration.for(key)
.define(validationMetadata<StepValidatorOptions>(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: number,
message: string = DEFAULT_ERROR_MESSAGES.MIN_LENGTH
) {
const key = Validation.key(ValidationKeys.MIN_LENGTH);
const meta: MinLengthValidatorOptions = {
[ValidationKeys.MIN_LENGTH]: value,
message: message,
types: [String.name, Array.name, Set.name],
description: `defines the min length of the attribute as ${value} (applies to strings or lists)`,
async: false,
};
return validationMetadata(minlength, key, meta);
//
// Decoration.for(key)
// .define(validationMetadata<MinLengthValidatorOptions>(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: number,
message: string = DEFAULT_ERROR_MESSAGES.MAX_LENGTH
) {
const key = Validation.key(ValidationKeys.MAX_LENGTH);
const meta: MaxLengthValidatorOptions = {
[ValidationKeys.MAX_LENGTH]: value,
message: message,
types: [String.name, Array.name, Set.name],
description: `defines the max length of the attribute as ${value} (applies to strings or lists)`,
async: false,
};
return Decoration.for(key)
.define(validationMetadata<MaxLengthValidatorOptions>(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: RegExp | string,
message: string = DEFAULT_ERROR_MESSAGES.PATTERN
) {
const key = Validation.key(ValidationKeys.PATTERN);
const meta: PatternValidatorOptions = {
[ValidationKeys.PATTERN]:
typeof value === "string" ? value : value.toString(),
message: message,
types: [String.name],
description: `assigns the ${value === "string" ? value : value.toString()} pattern to the attribute`,
async: false,
};
return Decoration.for(key)
.define(validationMetadata<PatternValidatorOptions>(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: string = DEFAULT_ERROR_MESSAGES.EMAIL) {
const key = Validation.key(ValidationKeys.EMAIL);
const meta: PatternValidatorOptions = {
[ValidationKeys.PATTERN]: DEFAULT_PATTERNS.EMAIL.toString(),
message: message,
types: [String.name],
description: "marks the attribute as an email",
async: false,
};
return Decoration.for(key)
.define(validationMetadata<PatternValidatorOptions>(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: string = DEFAULT_ERROR_MESSAGES.URL) {
const key = Validation.key(ValidationKeys.URL);
const meta: PatternValidatorOptions = {
[ValidationKeys.PATTERN]: DEFAULT_PATTERNS.URL.toString(),
message: message,
types: [String.name],
description: "marks the attribute as an url",
async: false,
};
return Decoration.for(key)
.define(validationMetadata<PatternValidatorOptions>(url, key, meta))
.apply();
}
export interface TypeMetadata extends ValidatorOptions {
customTypes: string[] | string;
}
/**
* @summary Enforces type verification
* @description Validators to validate a decorated property must use key {@link ValidationKeys#TYPE}
*
* @param {string[] | string} types accepted types
* @param {string} [message] the error message. Defaults to {@link DEFAULT_ERROR_MESSAGES#TYPE}
*
* @function type
* @category Property Decorators
*/
export function type(
types: string[] | string,
message: string = DEFAULT_ERROR_MESSAGES.TYPE
) {
const key = Validation.key(ValidationKeys.TYPE);
const meta: TypeMetadata = {
customTypes: types,
message: message,
description: "defines the accepted types for the attribute",
async: false,
};
return Decoration.for(key)
.define(validationMetadata<TypeMetadata>(type, key, meta))
.apply();
}
export interface DateMetadata extends DateValidatorOptions {
types: string[];
}
/**
* @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: string = "dd/MM/yyyy",
message: string = DEFAULT_ERROR_MESSAGES.DATE
) {
const key = Validation.key(ValidationKeys.DATE);
const meta: DateMetadata = {
[ValidationKeys.FORMAT]: format,
message: message,
types: [Date.name],
description: `defines the attribute as a date with the format ${format}`,
async: false,
};
const dateDec = (target: Record<string, any>, propertyKey?: any): any => {
validationMetadata(date, key, meta)(target, propertyKey);
const values = new WeakMap();
Object.defineProperty(target, propertyKey, {
configurable: false,
set(this: any, newValue: string | Date) {
const descriptor = Object.getOwnPropertyDescriptor(this, propertyKey);
if (!descriptor || descriptor.configurable)
Object.defineProperty(this, propertyKey, {
enumerable: true,
configurable: false,
get: () => values.get(this),
set: (newValue: string | Date | number) => {
let val: Date | undefined;
try {
val = parseDate(format, newValue);
values.set(this, val);
} catch (e: any) {
console.error(sf("Failed to parse date: {0}", e.message || e));
}
},
});
this[propertyKey] = newValue;
},
get() {
console.log("here");
},
});
};
return Decoration.for(key).define(dateDec).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: RegExp = DEFAULT_PATTERNS.PASSWORD.CHAR8_ONE_OF_EACH,
message: string = DEFAULT_ERROR_MESSAGES.PASSWORD
) {
const key = Validation.key(ValidationKeys.PASSWORD);
const meta: PatternValidatorOptions = {
[ValidationKeys.PATTERN]: pattern.toString(),
message: message,
types: [String.name],
description: `attribute as a password`,
async: false,
};
return Decoration.for(key)
.define(validationMetadata(password, key, meta))
.apply();
}
export interface ListMetadata extends ListValidatorOptions {
type: "Array" | "Set";
}
/**
* @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: Constructor<any> | Constructor<any>[],
collection: "Array" | "Set" = "Array",
message: string = DEFAULT_ERROR_MESSAGES.LIST
) {
const key = Validation.key(ValidationKeys.LIST);
const meta: ListMetadata = {
clazz: Array.isArray(clazz) ? clazz.map((c) => c.name) : [clazz.name],
type: collection,
message: message,
async: false,
description: `defines the attribute as a ${collection} of ${(clazz as ModelConstructor<any>).name}`,
};
return Decoration.for(key)
.define(validationMetadata(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: ModelConstructor<any>,
message: string = 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 {string} [message=DEFAULT_ERROR_MESSAGES.EQUALS] - Custom error message to return if validation fails.
*
* @returns {PropertyDecorator} A property decorator used to register the equality validation metadata.
*
* @function eq
* @category Property Decorators
*/
export function eq(
propertyToCompare: string,
message: string = DEFAULT_ERROR_MESSAGES.EQUALS
) {
const options: EqualsValidatorOptions = {
message: message,
[ValidationKeys.EQUALS]: propertyToCompare,
description: `defines attribute as equal to ${propertyToCompare}`,
};
return validationMetadata<ValidationMetadata>(
eq,
Validation.key(ValidationKeys.EQUALS),
{ ...options, async: false } as ValidationMetadata
);
}
/**
* @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 {string} [message=DEFAULT_ERROR_MESSAGES.DIFF] - Custom error message to return if validation fails.
*
* @returns {PropertyDecorator} A property decorator used to register the difference validation metadata.
*
* @function diff
* @category Property Decorators
*/
export function diff(
propertyToCompare: string,
message: string = DEFAULT_ERROR_MESSAGES.DIFF
) {
const options: DiffValidatorOptions = {
message: message,
[ValidationKeys.DIFF]: propertyToCompare,
description: `defines attribute as different to ${propertyToCompare}`,
};
return validationMetadata<ValidationMetadata>(
diff,
Validation.key(ValidationKeys.DIFF),
{
...options,
async: false,
} as ValidationMetadata
);
}
/**
* @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 {string} [message=DEFAULT_ERROR_MESSAGES.LESS_THAN] - Custom error message to return 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: string,
message: string = DEFAULT_ERROR_MESSAGES.LESS_THAN
) {
const options: LessThanValidatorOptions = {
message: message,
[ValidationKeys.LESS_THAN]: propertyToCompare,
description: `defines attribute as less than to ${propertyToCompare}`,
};
return validationMetadata<ValidationMetadata>(
lt,
Validation.key(ValidationKeys.LESS_THAN),
{ ...options, async: false } as ValidationMetadata
);
}
/**
* @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 {string} [message=DEFAULT_ERROR_MESSAGES.LESS_THAN_OR_EQUAL] - Custom error message to return 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: string,
message: string = DEFAULT_ERROR_MESSAGES.LESS_THAN_OR_EQUAL
) {
const options: LessThanOrEqualValidatorOptions = {
message: message,
[ValidationKeys.LESS_THAN_OR_EQUAL]: propertyToCompare,
description: `defines attribute as less or equal to ${propertyToCompare}`,
};
return validationMetadata<ValidationMetadata>(
lte,
Validation.key(ValidationKeys.LESS_THAN_OR_EQUAL),
{ ...options, async: false } as ValidationMetadata
);
}
/**
* @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 {string} [message=DEFAULT_ERROR_MESSAGES.GREATER_THAN] - Custom error message to return 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: string,
message: string = DEFAULT_ERROR_MESSAGES.GREATER_THAN
) {
const options: GreaterThanValidatorOptions = {
message: message,
[ValidationKeys.GREATER_THAN]: propertyToCompare,
description: `defines attribute as greater than ${propertyToCompare}`,
};
return validationMetadata<ValidationMetadata>(
gt,
Validation.key(ValidationKeys.GREATER_THAN),
{ ...options, async: false } as ValidationMetadata
);
}
/**
* @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 {string} [message=DEFAULT_ERROR_MESSAGES.GREATER_THAN_OR_EQUAL] - Custom error message to return 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: string,
message: string = DEFAULT_ERROR_MESSAGES.GREATER_THAN_OR_EQUAL
) {
const options: GreaterThanOrEqualValidatorOptions = {
message: message,
[ValidationKeys.GREATER_THAN_OR_EQUAL]: propertyToCompare,
description: `defines attribute as greater or equal to ${propertyToCompare}`,
};
return validationMetadata<ValidationMetadata>(
gte,
Validation.key(ValidationKeys.GREATER_THAN_OR_EQUAL),
{ ...options, async: false } as ValidationMetadata
);
}
Source