UNPKG

@decaf-ts/decorator-validation

Version:
1,162 lines (1,149 loc) 580 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('reflect-metadata'), require('@decaf-ts/reflection'), require('tslib')) : typeof define === 'function' && define.amd ? define(['exports', 'reflect-metadata', '@decaf-ts/reflection', 'tslib'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["decorator-validation"] = {}, null, global.reflection, global.tslib)); })(this, (function (exports, reflectMetadata, reflection, tslib) { 'use strict'; const COMPARISON_ERROR_MESSAGES = { INVALID_PATH: "Invalid path argument. Expected non-empty string but received: '{0}'", CONTEXT_NOT_OBJECT_COMPARISON: "Unable to access parent at level {0} for path '{1}': current context is not an object", PROPERTY_INVALID: "Failed to resolve path {0}: property '{1}' is invalid or does not exist.", PROPERTY_NOT_EXIST: "Failed to resolve path: property '{0}' does not exist.", UNSUPPORTED_TYPES_COMPARISON: "Unsupported types for comparison: '{0}' and '{1}'", NULL_OR_UNDEFINED_COMPARISON: "Comparison failed due to null or undefined value", INVALID_DATE_COMPARISON: "Invalid Date objects are not comparable", TYPE_MISMATCH_COMPARISON: "Cannot compare values of different types: {0} and {1}.", NAN_COMPARISON: "Comparison not supported for NaN values", // NO_PARENT_COMPARISON: "Unable to access parent at level {0} for path '{1}': no parent available", // PROPERTY_NOT_FOUND: "Failed to resolve path {0}: property '{1}' does not exist.", // PROPERTY_NOT_FOUND_ON_PARENT: "Failed to resolve path {0}: property '{1}' does not exist on parent.", // PROPERTY_NOT_FOUND_AFTER_PARENT: "Failed to resolve path {0}: property '{1}' does not exist after {2} parent level(s).", }; /** * @description Symbol key for tracking parent-child relationships in validation * @summary Symbol used to internally track the parent object during nested validation * * @const VALIDATION_PARENT_KEY * @memberOf module:decorator-validation */ const VALIDATION_PARENT_KEY = Symbol("_parent"); const ASYNC_META_KEY = Symbol("isAsync"); /** * @description Enum containing metadata keys used for reflection in the model system * @summary Defines the various Model keys used for reflection and metadata storage. * These keys are used throughout the library to store and retrieve metadata about models, * their properties, and their behavior. * * @property {string} REFLECT - Prefix to all other keys, used as a namespace * @property {string} TYPE - Key for storing design type information * @property {string} PARAMS - Key for storing method parameter types * @property {string} RETURN - Key for storing method return type * @property {string} MODEL - Key for identifying model metadata * @property {string} ANCHOR - Anchor key that serves as a ghost property in the model * @property {string} CONSTRUCTION - Key for storing construction information * @property {string} ATTRIBUTE - Key for storing attribute metadata * @property {string} HASHING - Key for storing hashing configuration * @property {string} SERIALIZATION - Key for storing serialization configuration * * @readonly * @enum {string} * @memberOf module:decorator-validation * @category Model */ exports.ModelKeys = void 0; (function (ModelKeys) { ModelKeys["REFLECT"] = "decaf.model."; ModelKeys["DESCRIPTION"] = "decaf.description."; ModelKeys["TYPE"] = "design:type"; ModelKeys["PARAMS"] = "design:paramtypes"; ModelKeys["RETURN"] = "design:returntype"; ModelKeys["MODEL"] = "model"; ModelKeys["ANCHOR"] = "__model"; ModelKeys["CONSTRUCTION"] = "constructed-by"; ModelKeys["ATTRIBUTE"] = "__attributes"; ModelKeys["HASHING"] = "hashing"; ModelKeys["SERIALIZATION"] = "serialization"; })(exports.ModelKeys || (exports.ModelKeys = {})); /** * @description Default flavour identifier for the decorator system * @summary Defines the default flavour used by the Decoration class when no specific flavour is provided. * This constant is used throughout the library as the fallback flavour for decorators. * * @const {string} * @memberOf module:decorator-validation * @category Model */ const DefaultFlavour = "decaf"; /** * @summary Keys used for comparison-based validations. * * @property {string} EQUALS - Validates if two values are equal. * @property {string} DIFF - Validates if two values are different. * @property {string} LESS_THAN - Validates if a value is less than another. * @property {string} LESS_THAN_OR_EQUAL - Validates if a value is less than or equal to another. * @property {string} GREATER_THAN - Validates if a value is greater than another. * @property {string} GREATER_THAN_OR_EQUAL - Validates if a value is greater than or equal to another. * * @constant ComparisonValidationKeys * @memberof module:decorator-validation.Validation * @category Validation */ const ComparisonValidationKeys = { EQUALS: "equals", DIFF: "different", LESS_THAN: "lessThan", LESS_THAN_OR_EQUAL: "lessThanOrEqual", GREATER_THAN: "greaterThan", GREATER_THAN_OR_EQUAL: "greaterThanOrEqual", }; /** * @summary The keys used for validation * * @property {string} REFLECT prefixes others * @property {string} REQUIRED sets as required * @property {string} MIN defines min value * @property {string} MAX defines max value * @property {string} STEP defines step * @property {string} MIN_LENGTH defines min length * @property {string} MAX_LENGTH defines max length * @property {string} PATTERN defines pattern * @property {string} EMAIL defines email * @property {string} URL defines url * @property {string} DATE defines date * @property {string} TYPE defines type * @property {string} PASSWORD defines password * @property {string} LIST defines list * * @constant ValidationKeys * @memberOf module:decorator-validation.Validation * @category Validation */ const ValidationKeys = { REFLECT: `${exports.ModelKeys.REFLECT}validation.`, DATE: "date", EMAIL: "email", FORMAT: "format", LIST: "list", MAX: "max", MAX_LENGTH: "maxlength", MIN: "min", MIN_LENGTH: "minlength", PASSWORD: "password", PATTERN: "pattern", REQUIRED: "required", STEP: "step", TYPE: "type", UNIQUE: "unique", URL: "url", VALIDATOR: "validator", ...ComparisonValidationKeys, }; /** * @summary list of month names * @description Stores month names. Can be changed for localization purposes * * @constant MONTH_NAMES * @memberOf module:decorator-validation.Validation * @category Validation */ const MONTH_NAMES = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; /** * @summary list of names of days of the week * @description Stores names for days of the week. Can be changed for localization purposes * * @constant DAYS_OF_WEEK_NAMES * @memberOf module:decorator-validation.Validation * @category Validation */ const DAYS_OF_WEEK_NAMES = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", ]; /** * @summary Defines the default error messages * * @property {string} REQUIRED default error message * @property {string} MIN default error message * @property {string} MAX default error message * @property {string} MIN_LENGTH default error message * @property {string} MAX_LENGTH default error message * @property {string} PATTERN default error message * @property {string} EMAIL default error message * @property {string} URL default error message * @property {string} TYPE default error message * @property {string} STEP default error message * @property {string} DATE default error message * @property {string} DEFAULT default error message * @property {string} PASSWORD default error message * @property {string} LIST default error message * @property {string} LIST_INSIDE default error message * @property {string} MODEL_NOT_FOUND default error message * * @constant DEFAULT_ERROR_MESSAGES * @memberOf module:decorator-validation.Validation * @category Validation */ const DEFAULT_ERROR_MESSAGES = { REQUIRED: "This field is required", MIN: "The minimum value is {0}", MAX: "The maximum value is {0}", MIN_LENGTH: "The minimum length is {0}", MAX_LENGTH: "The maximum length is {0}", PATTERN: "The value does not match the pattern", EMAIL: "The value is not a valid email", URL: "The value is not a valid URL", TYPE: "Invalid type. Expected {0}, received {1}", STEP: "Invalid value. Not a step of {0}", DATE: "Invalid value. not a valid Date", DEFAULT: "There is an Error", PASSWORD: "Must be at least 8 characters and contain one of number, lower and upper case letters, and special character (@$!%*?&_-.,)", LIST: "Invalid list of {0}", MODEL_NOT_FOUND: "No model registered under {0}", EQUALS: "This field must be equal to field {0}", DIFF: "This field must be different from field {0}", LESS_THAN: "This field must be less than field {0}", LESS_THAN_OR_EQUAL: "This field must be less than or equal to field {0}", GREATER_THAN: "This field must be greater than field {0}", GREATER_THAN_OR_EQUAL: "This field must be greater than or equal to field {0}", UNIQUE: "Duplicate found, this field must be unique.", }; /** * @summary Defines the various default regexp patterns used * * @enum DEFAULT_PATTERNS * @memberOf module:decorator-validation.Validation * @category Validation */ const DEFAULT_PATTERNS = { EMAIL: /[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?/, URL: /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i, PASSWORD: { CHAR8_ONE_OF_EACH: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&_\-.,])[A-Za-z\d@$!%*?&_\-.,]{8,}$/g, }, }; /** * @summary Util function to provide string format functionality similar to C#'s string.format * * @param {string} string * @param {Array<string | number>} [args] replacements made by order of appearance (replacement0 wil replace {0} and so on) * @return {string} formatted string * * @function stringFormat * @memberOf module:decorator-validation * @category Model */ function stringFormat(string, ...args) { return string.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] !== "undefined" ? args[number].toString() : "undefined"; }); } /** * @summary Util function to provide string format functionality similar to C#'s string.format * @description alias for {@link stringFormat} * * @param {string} string * @param {string} args replacements made by order of appearance (replacement0 wil replace {0} and so on) * @return {string} formatted string * * @function sf * @memberOf module:decorator-validation * @category Model */ const sf = stringFormat; /** * @summary Reverses the process from {@link formatDate} * * @param {string} date the date string to be converted back into date * @param {string} format the date format * @return {Date} the date from the format or the standard new Date({@prop date}) if the string couldn't be parsed (are you sure the format matches the string?) * * @function dateFromFormat * @memberOf module:decorator-validation * @category Model */ function dateFromFormat(date, format) { let formatRegexp = format; // Hour if (formatRegexp.match(/hh/)) formatRegexp = formatRegexp.replace("hh", "(?<hour>\\d{2})"); else if (formatRegexp.match(/h/)) formatRegexp = formatRegexp.replace("h", "(?<hour>\\d{1,2})"); else if (formatRegexp.match(/HH/)) formatRegexp = formatRegexp.replace("HH", "(?<hour>\\d{2})"); else if (formatRegexp.match(/H/)) formatRegexp = formatRegexp.replace("H", "(?<hour>\\d{1,2})"); // Minutes if (formatRegexp.match(/mm/)) formatRegexp = formatRegexp.replace("mm", "(?<minutes>\\d{2})"); else if (formatRegexp.match(/m/)) formatRegexp = formatRegexp.replace("m", "(?<minutes>\\d{1,2})"); // Seconds if (formatRegexp.match(/ss/)) formatRegexp = formatRegexp.replace("ss", "(?<seconds>\\d{2})"); else if (formatRegexp.match(/s/)) formatRegexp = formatRegexp.replace("s", "(?<seconds>\\d{1,2})"); // Day if (formatRegexp.match(/dd/)) formatRegexp = formatRegexp.replace("dd", "(?<day>\\d{2})"); else if (formatRegexp.match(/d/)) formatRegexp = formatRegexp.replace("d", "(?<day>\\d{1,2})"); // Day Of Week if (formatRegexp.match(/EEEE/)) formatRegexp = formatRegexp.replace("EEEE", "(?<dayofweek>\\w+)"); // eslint-disable-next-line no-dupe-else-if else if (formatRegexp.match(/EEEE/)) formatRegexp = formatRegexp.replace("EEE", "(?<dayofweek>\\w+)"); // Year if (formatRegexp.match(/yyyy/)) formatRegexp = formatRegexp.replace("yyyy", "(?<year>\\d{4})"); else if (formatRegexp.match(/yy/)) formatRegexp = formatRegexp.replace("yy", "(?<year>\\d{2})"); // Month if (formatRegexp.match(/MMMM/)) formatRegexp = formatRegexp.replace("MMMM", "(?<monthname>\\w+)"); else if (formatRegexp.match(/MMM/)) formatRegexp = formatRegexp.replace("MMM", "(?<monthnamesmall>\\w+)"); if (formatRegexp.match(/MM/)) formatRegexp = formatRegexp.replace("MM", "(?<month>\\d{2})"); else if (formatRegexp.match(/M/)) formatRegexp = formatRegexp.replace("M", "(?<month>\\d{1,2})"); // Milis and Am Pm formatRegexp = formatRegexp .replace("S", "(?<milis>\\d{1,3})") .replace("aaa", "(?<ampm>\\w{2})"); const regexp = new RegExp(formatRegexp, "g"); const match = regexp.exec(date); if (!match || !match.groups) return new Date(date); const safeParseInt = function (n) { if (!n) return 0; const result = parseInt(n); return isNaN(result) ? 0 : result; }; const year = safeParseInt(match.groups.year); const day = safeParseInt(match.groups.day); const amPm = match.groups.ampm; let hour = safeParseInt(match.groups.hour); if (amPm) hour = amPm === "PM" ? hour + 12 : hour; const minutes = safeParseInt(match.groups.minutes); const seconds = safeParseInt(match.groups.seconds); const ms = safeParseInt(match.groups.milis); const monthName = match.groups.monthname; const monthNameSmall = match.groups.monthnamesmall; let month = match.groups.month; if (monthName) month = MONTH_NAMES.indexOf(monthName); else if (monthNameSmall) { const m = MONTH_NAMES.find((m) => m.toLowerCase().startsWith(monthNameSmall.toLowerCase())); if (!m) return new Date(date); month = MONTH_NAMES.indexOf(m); } else month = safeParseInt(`${month}`); return new Date(year, month - 1, day, hour, minutes, seconds, ms); } /** * @description Binds a specific date format to a Date object's toString and toISOString methods * @summary Modifies a Date object to return a formatted string when toString or toISOString is called. * This function overrides the default toString and toISOString methods of the Date object to return * the date formatted according to the specified format string. * @param {Date} [date] The Date object to modify * @param {string} [format] The format string to use for formatting the date * @return {Date|undefined} The modified Date object or undefined if no date was provided * @function bindDateToString * @memberOf module:decorator-validation * @category Model */ function bindDateToString(date, format) { if (!date) return; const func = () => formatDate(date, format); Object.defineProperty(date, "toISOString", { enumerable: false, configurable: false, value: func, }); Object.defineProperty(date, "toString", { enumerable: false, configurable: false, value: func, }); // Object.setPrototypeOf(date, Date.prototype); return date; } /** * @description Safely checks if a value is a valid Date object * @summary A utility function that determines if a value is a valid Date object. * This function is more reliable than using instanceof Date as it also checks * that the date is not NaN, which can happen with invalid date strings. * @param {any} date The value to check * @return {boolean} True if the value is a valid Date object, false otherwise * @function isValidDate * @memberOf module:decorator-validation * @category Validation */ function isValidDate(date) { return (date && Object.prototype.toString.call(date) === "[object Date]" && !Number.isNaN(date)); } /** * @summary Util function to pad numbers * @param {number} num * * @return {string} * * @function twoDigitPad * @memberOf module:decorator-validation * @category Model */ function twoDigitPad(num) { return num < 10 ? "0" + num : num.toString(); } /** * @summary Date Format Handling * @description Code from {@link https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date} * * <pre> * Using similar formatting as Moment.js, Class DateTimeFormatter (Java), and Class SimpleDateFormat (Java), * I implemented a comprehensive solution formatDate(date, patternStr) where the code is easy to read and modify. * You can display date, time, AM/PM, etc. * * Date and Time Patterns * yy = 2-digit year; yyyy = full year * M = digit month; MM = 2-digit month; MMM = short month name; MMMM = full month name * EEEE = full weekday name; EEE = short weekday name * d = digit day; dd = 2-digit day * h = hours am/pm; hh = 2-digit hours am/pm; H = hours; HH = 2-digit hours * m = minutes; mm = 2-digit minutes; aaa = AM/PM * s = seconds; ss = 2-digit seconds * S = miliseconds * </pre> * * @param {Date} date * @param {string} [patternStr] defaults to 'yyyy/MM/dd' * @return {string} the formatted date * * @function formatDate * @memberOf module:decorator-validation * @category Model */ function formatDate(date, patternStr = "yyyy/MM/dd") { const day = date.getDate(), month = date.getMonth(), year = date.getFullYear(), hour = date.getHours(), minute = date.getMinutes(), second = date.getSeconds(), miliseconds = date.getMilliseconds(), h = hour % 12, hh = twoDigitPad(h), HH = twoDigitPad(hour), mm = twoDigitPad(minute), ss = twoDigitPad(second), aaa = hour < 12 ? "AM" : "PM", EEEE = DAYS_OF_WEEK_NAMES[date.getDay()], EEE = EEEE.substr(0, 3), dd = twoDigitPad(day), M = month + 1, MM = twoDigitPad(M), MMMM = MONTH_NAMES[month], MMM = MMMM.substr(0, 3), yyyy = year + "", yy = yyyy.substr(2, 2); // checks to see if month name will be used patternStr = patternStr .replace("hh", hh) .replace("h", h.toString()) .replace("HH", HH) .replace("H", hour.toString()) .replace("mm", mm) .replace("m", minute.toString()) .replace("ss", ss) .replace("s", second.toString()) .replace("S", miliseconds.toString()) .replace("dd", dd) .replace("d", day.toString()) .replace("EEEE", EEEE) .replace("EEE", EEE) .replace("yyyy", yyyy) .replace("yy", yy) .replace("aaa", aaa); if (patternStr.indexOf("MMM") > -1) { patternStr = patternStr.replace("MMMM", MMMM).replace("MMM", MMM); } else { patternStr = patternStr.replace("MM", MM).replace("M", M.toString()); } return patternStr; } /** * @summary Parses a date from a specified format * @param {string} format * @param {string | Date | number} [v] * @memberOf module:decorator-validation * @category Model */ function parseDate(format, v) { let value = undefined; if (!v) return undefined; if (v instanceof Date) try { value = dateFromFormat(formatDate(v, format), format); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(sf("Could not convert date {0} to format: {1}", v.toString(), format)); } else if (typeof v === "string") { value = dateFromFormat(v, format); } else if (typeof v === "number") { const d = new Date(v); value = dateFromFormat(formatDate(d, format), format); } else if (isValidDate(v)) { try { const d = new Date(v); value = dateFromFormat(formatDate(d, format), format); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(sf("Could not convert date {0} to format: {1}", v, format)); } } else { throw new Error(`Invalid value provided ${v}`); } return bindDateToString(value, format); } // eslint-disable-next-line @typescript-eslint/no-unused-vars function defaultFlavourResolver(target) { return DefaultFlavour; } /** * @description A decorator management class that handles flavoured decorators * @summary The Decoration class provides a builder pattern for creating and managing decorators with different flavours. * It supports registering, extending, and applying decorators with context-aware flavour resolution. * The class implements a fluent interface for defining, extending, and applying decorators with different flavours, * allowing for framework-specific decorator implementations while maintaining a consistent API. * @template T Type of the decorator (ClassDecorator | PropertyDecorator | MethodDecorator) * @param {string} [flavour] Optional flavour parameter for the decorator context * @class * @category Model * @example * ```typescript * // Create a new decoration for 'component' with default flavour * const componentDecorator = new Decoration() * .for('component') * .define(customComponentDecorator); * * // Create a flavoured decoration * const vueComponent = new Decoration('vue') * .for('component') * .define(vueComponentDecorator); * * // Apply the decoration * @componentDecorator * class MyComponent {} * ``` * @mermaid * sequenceDiagram * participant C as Client * participant D as Decoration * participant R as FlavourResolver * participant F as DecoratorFactory * * C->>D: new Decoration(flavour) * C->>D: for(key) * C->>D: define(decorators) * D->>D: register(key, flavour, decorators) * D->>F: decoratorFactory(key, flavour) * F->>R: resolve(target) * R-->>F: resolved flavour * F->>F: apply decorators * F-->>C: decorated target */ class Decoration { /** * @description Static map of registered decorators * @summary Stores all registered decorators organized by key and flavour */ static { this.decorators = {}; } /** * @description Function to resolve flavour from a target * @summary Resolver function that determines the appropriate flavour for a given target */ static { this.flavourResolver = defaultFlavourResolver; } constructor(flavour = DefaultFlavour) { this.flavour = flavour; } /** * @description Sets the key for the decoration builder * @summary Initializes a new decoration chain with the specified key * @param {string} key The identifier for the decorator * @return {DecorationBuilderMid} Builder instance for method chaining */ for(key) { this.key = key; return this; } /** * @description Adds decorators to the current context * @summary Internal method to add decorators with addon support * @param {boolean} [addon=false] Whether the decorators are addons * @param decorators Array of decorators * @return {this} Current instance for chaining */ decorate(addon = false, ...decorators) { if (!this.key) throw new Error("key must be provided before decorators can be added"); if ((!decorators || !decorators.length) && !addon && this.flavour !== DefaultFlavour) throw new Error("Must provide overrides or addons to override or extend decaf's decorators"); if (this.flavour === DefaultFlavour && addon) throw new Error("Default flavour cannot be extended"); this[addon ? "extras" : "decorators"] = new Set([ ...(this[addon ? "extras" : "decorators"] || new Set()).values(), ...decorators, ]); return this; } /** * @description Defines the base decorators * @summary Sets the primary decorators for the current context * @param decorators Decorators to define * @return Builder instance for finishing the chain */ define(...decorators) { return this.decorate(false, ...decorators); } /** * @description Extends existing decorators * @summary Adds additional decorators to the current context * @param decorators Additional decorators * @return {DecorationBuilderBuild} Builder instance for building the decorator */ extend(...decorators) { return this.decorate(true, ...decorators); } decoratorFactory(key, f = DefaultFlavour) { const contextDecorator = function contextDecorator(target, propertyKey, descriptor) { const flavour = Decoration.flavourResolver(target); let decorators; const extras = Decoration.decorators[key][flavour] ? Decoration.decorators[key][flavour].extras : Decoration.decorators[key][DefaultFlavour].extras; if (Decoration.decorators[key] && Decoration.decorators[key][flavour] && Decoration.decorators[key][flavour].decorators) { decorators = Decoration.decorators[key][flavour].decorators; } else { decorators = Decoration.decorators[key][DefaultFlavour].decorators; } const toApply = [ ...(decorators ? decorators.values() : []), ...(extras ? extras.values() : []), ]; toApply.forEach((d) => d(target, propertyKey, descriptor, descriptor)); }; Object.defineProperty(contextDecorator, "name", { value: [f, key].join("_decorator_for_"), writable: false, }); return contextDecorator; } /** * @description Creates the final decorator function * @summary Builds and returns the decorator factory function * @return {function(any, any?, TypedPropertyDescriptor?): any} The generated decorator function */ apply() { if (!this.key) throw new Error("No key provided for the decoration builder"); Decoration.register(this.key, this.flavour, this.decorators, this.extras); return this.decoratorFactory(this.key, this.flavour); } /** * @description Registers decorators for a specific key and flavour * @summary Internal method to store decorators in the static registry * @param {string} key Decorator key * @param {string} flavour Decorator flavour * @param [decorators] Primary decorators * @param [extras] Additional decorators */ static register(key, flavour, decorators, extras) { if (!key) throw new Error("No key provided for the decoration builder"); if (!decorators) throw new Error("No decorators provided for the decoration builder"); if (!flavour) throw new Error("No flavour provided for the decoration builder"); if (!Decoration.decorators[key]) Decoration.decorators[key] = {}; if (!Decoration.decorators[key][flavour]) Decoration.decorators[key][flavour] = {}; if (decorators) Decoration.decorators[key][flavour].decorators = decorators; if (extras) Decoration.decorators[key][flavour].extras = extras; } /** * @description Sets the global flavour resolver * @summary Configures the function used to determine decorator flavours * @param {FlavourResolver} resolver Function to resolve flavours */ static setFlavourResolver(resolver) { Decoration.flavourResolver = resolver; } static for(key) { return new Decoration().for(key); } static flavouredAs(flavour) { return new Decoration(flavour); } } /** * @description Property decorator factory for model attributes * @summary Creates a decorator that marks class properties as model attributes * * @param {string} [key=ModelKeys.ATTRIBUTE] - The metadata key under which to store the property name * @return {function(object, any?): void} - Decorator function that registers the property * @function prop * @category Property Decorators * * @mermaid * sequenceDiagram * participant D as Decorator * participant M as Model * * D->>M: Check if key exists * alt key exists * M-->>D: Return existing props array * else key doesn't exist * D->>M: Create new props array * end * D->>M: Check if property exists * alt property not in array * D->>M: Add property to array * end */ function prop(key = exports.ModelKeys.ATTRIBUTE) { return (model, propertyKey) => { let props; if (Object.prototype.hasOwnProperty.call(model, key)) { props = model[key]; } else { props = model[key] = []; } if (!props.includes(propertyKey)) props.push(propertyKey); }; } /** * @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 {string} key - The metadata key * @param {V} value - The metadata value to associate with the property * @return {Function} - Combined decorator function * @function propMetadata * @category Property Decorators */ function propMetadata(key, value) { return reflection.apply(prop(), reflection.metadata(key, value)); } /** * @summary Mimics Java's String's Hash implementation * * @param {string | number | symbol | Date} obj * @return {number} hash value of obj * * @function hashCode * @memberOf module:decorator-validation * @category Model */ function hashCode(obj) { obj = String(obj); let hash = 0; for (let i = 0; i < obj.length; i++) { const character = obj.charCodeAt(i); hash = (hash << 5) - hash + character; hash = hash & hash; // Convert to 32bit integer } return hash.toString(); } /** * @summary Hashes an object by combining the hash of all its properties * * @param {Record<string, any>} obj * @return {string} the resulting hash * * @function hashObj * @memberOf module:decorator-validation * @category Model */ function hashObj(obj) { const hashReducer = function (h, el) { const elHash = hashFunction(el); if (typeof elHash === "string") return hashFunction((h || "") + hashFunction(el)); h = h || 0; h = (h << 5) - h + elHash; return h & h; }; const func = hashCode; const hashFunction = function (value) { if (typeof value === "undefined") return ""; if (["string", "number", "symbol"].indexOf(typeof value) !== -1) return func(value.toString()); if (value instanceof Date) return func(value.getTime()); if (Array.isArray(value)) return value.reduce(hashReducer, undefined); return Object.values(value).reduce(hashReducer, undefined); }; const result = Object.values(obj).reduce(hashReducer, 0); return (typeof result === "number" ? Math.abs(result) : result).toString(); } const DefaultHashingMethod = "default"; /** * @description Manages hashing methods and provides a unified hashing interface * @summary A utility class that provides a registry for different hashing functions and methods to hash objects. * The class maintains a cache of registered hashing functions and allows setting a default hashing method. * It prevents direct instantiation and provides static methods for registration and hashing. * * @class Hashing * @category Model * * @example * ```typescript * // Register a custom hashing function * Hashing.register('md5', (obj) => createMD5Hash(obj), true); * * // Hash an object using default method * const hash1 = Hashing.hash(myObject); * * // Hash using specific method * const hash2 = Hashing.hash(myObject, 'md5'); * ``` */ class Hashing { /** * @description Current default hashing method identifier * @private */ static { this.current = DefaultHashingMethod; } /** * @description Cache of registered hashing functions * @private */ static { this.cache = { default: hashObj, }; } constructor() { } /** * @description Retrieves a registered hashing function * @summary Fetches a hashing function from the cache by its key. Throws an error if the method is not registered. * * @param {string} key - The identifier of the hashing function to retrieve * @return {HashingFunction} The requested hashing function * @private */ static get(key) { if (key in this.cache) return this.cache[key]; throw new Error(`No hashing method registered under ${key}`); } /** * @description Registers a new hashing function * @summary Adds a new hashing function to the registry. Optionally sets it as the default method. * Throws an error if a method with the same key is already registered. * * @param {string} key - The identifier for the hashing function */ static register(key, func, setDefault = false) { if (key in this.cache) throw new Error(`Hashing method ${key} already registered`); this.cache[key] = func; if (setDefault) this.current = key; } static hash(obj, method, ...args) { if (!method) return this.get(this.current)(obj, ...args); return this.get(method)(obj, ...args); } static setDefault(method) { this.current = this.get(method); } } const fallbackGetParent = (target) => { return target[VALIDATION_PARENT_KEY]; }; const fallbackGetValue = (target, prop) => { if (!Object.prototype.hasOwnProperty.call(target, prop)) throw new Error(sf(COMPARISON_ERROR_MESSAGES.PROPERTY_NOT_EXIST, prop)); return target[prop]; }; /** * Standard path resolution utility for accessing nested object properties. * Provides consistent dot-notation access to both parent and child properties * across complex object structures. * * - Dot-notation path resolution ('object.child.property') * - Parent traversal using '../' notation * - Configurable property access behavior * - Null/undefined safety checks */ class PathProxyEngine { /** * Creates a path-aware proxy for the target object * @template T - The type of the target object * @param {T} rootTarget - The target object to proxy * @param opts - Configuration options * @param opts.getValue - Custom function to get property value * @param opts.getParent - Custom function to get parent object * @param opts.ignoreUndefined - Whether to ignore undefined values in paths * @param opts.ignoreNull - Whether to ignore null values in paths * @returns A proxy object with path access capabilities */ static create(rootTarget, opts) { const { getValue, getParent, ignoreUndefined, ignoreNull } = { getParent: fallbackGetParent, getValue: fallbackGetValue, ignoreNull: false, ignoreUndefined: false, ...opts, }; const proxy = new Proxy({}, { get(target, prop) { if (prop === "getValueFromPath") { return function (path) { const parts = PathProxyEngine.parsePath(path); let current = rootTarget; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part === "..") { const parent = getParent(current); if (!parent || typeof parent !== "object") { throw new Error(sf(COMPARISON_ERROR_MESSAGES.CONTEXT_NOT_OBJECT_COMPARISON, i + 1, path)); } current = parent; //PathProxyEngine.create(parentTarget, opts); continue; } current = getValue(current, part); if (!ignoreUndefined && typeof current === "undefined") throw new Error(sf(COMPARISON_ERROR_MESSAGES.PROPERTY_INVALID, path, part)); if (!ignoreNull && current === null) throw new Error(sf(COMPARISON_ERROR_MESSAGES.PROPERTY_INVALID, path, part)); } return current; }; } return target[prop]; }, }); // Object.defineProperty(proxy, PROXY_PROP, { // value: true, // overwrite by proxy behavior // enumerable: false, // configurable: false, // writable: false, // }); return proxy; } /** * Parses a path string into individual components * @param path - The path string to parse (e.g., "user.address.city") * @returns An array of path components * @throws Error if the path is invalid */ static parsePath(path) { if (typeof path !== "string" || !path.trim()) throw new Error(sf(COMPARISON_ERROR_MESSAGES.INVALID_PATH, path)); return path.match(/(\.\.|[^/.]+)/g) || []; } } /** * @summary Helper Class to hold the error results * @description holds error results in an 'indexable' manner * while still providing the same result on toString * * @param {ModelErrors} errors * * @class ModelErrorDefinition * * @category Model */ class ModelErrorDefinition { constructor(errors) { for (const prop in errors) { if (Object.prototype.hasOwnProperty.call(errors, prop) && errors[prop]) Object.defineProperty(this, prop, { enumerable: true, configurable: false, value: errors[prop], writable: false, }); } } /** * @summary Outputs the class to a nice readable string * * @override */ toString() { const self = this; return Object.keys(self) .filter((k) => Object.prototype.hasOwnProperty.call(self, k) && typeof self[k] !== "function") .reduce((accum, prop) => { let propError = Object.keys(self[prop]).reduce((propAccum, key) => { if (!propAccum) propAccum = self[prop][key]; else propAccum += `\n${self[prop][key]}`; return propAccum; }, undefined); if (propError) { propError = `${prop} - ${propError}`; if (!accum) accum = propError; else accum += `\n${propError}`; } return accum; }, ""); } } /** * @description Abstract base class for all validators in the validation framework. * @summary The BaseValidator class provides the foundation for all synchronous and asynchronous validator implementations. * It handles type checking, error message formatting, and defines the interface that all validators must implement. * This class is designed to be extended by specific validator classes that define their own validation logic. * * @template V - Validator options type * @template IsAsync - Whether the validator is async (true) or sync (false). Default `false`. * * @param {boolean} async - Defines if the validator is async (must match the subclass signature) * @param {string} message - Default error message to display when validation fails (defaults to {@link DEFAULT_ERROR_MESSAGES#DEFAULT}) * @param {string[]} acceptedTypes - Type names that this validator accepts (used for runtime type checking) * * @class BaseValidator * @abstract * * @example * // Example of a synchronous validator * class SyncValidator extends BaseValidator<SomeOptions, false> { * constructor() { * super(false, "Sync validation failed", String.name); * } * * public hasErrors(value: any, options?: SomeOptions): string | undefined { * if (typeof value !== "string") return this.getMessage(this.message); * return undefined; * } * } * * @example * // Example of an asynchronous custom validator * class AsyncValidator extends BaseValidator<SomeOptions, true> { * constructor() { * super(true, "Async validation failed", String.name); * } * * public async hasErrors(value: any, options?: SomeOptions): Promise<string | undefined> { * const result = await someAsyncCheck(value); * if (!result) return this.getMessage(this.message); * return undefined; * } * } * * @mermaid * sequenceDiagram * participant C as Client * participant V as Validator Subclass * participant B as BaseValidator * * C->>V: new CustomValidator(async, message) * V->>B: super(async, message, acceptedTypes) * B->>B: Store message, async flag, and accepted types * B->>B: Optionally wrap hasErrors with type checking * C->>V: hasErrors(value, options) * alt value type not in acceptedTypes * B-->>C: Type error message * else value type is accepted * V->>V: Custom validation logic * V-->>C: Validation result * end * * @category Validators */ class BaseValidator { constructor(async, message = DEFAULT_ERROR_MESSAGES.DEFAULT, ...acceptedTypes) { this.async = async; this.message = message; if (acceptedTypes.length) this.acceptedTypes = acceptedTypes; if (this.acceptedTypes) this.hasErrors = this.checkTypeAndHasErrors(this.hasErrors.bind(this)); } /** * @description Formats an error message with optional arguments * @summary Creates a formatted error message by replacing placeholders with provided arguments. * This method uses the string formatting utility to generate consistent error messages * across all validators. * * @param {string} message - The message template with placeholders * @param {...any} args - Values to insert into the message template * @return {string} The formatted error message * @protected */ getMessage(message, ...args) { return sf(message, ...args); } /** * @description Creates a type-checking wrapper around the hasErrors method * @summary Wraps the hasErrors method with type validation logic to ensure that * the value being validated is of an accepted type before performing specific validation. * This method is called during construction if acceptedTypes are provided. * * @param {Function} unbound - The original hasErrors method to be wrapped * @return {Function} A new function that performs type checking before calling the original method * @private */ checkTypeAndHasErrors(unbound) { return function (value, options, proxy, ...args) { if (value === undefined || !this.acceptedTypes) return unbound(value, options, proxy, ...args); if (!reflection.Reflection.checkTypes(value, this.acceptedTypes)) return this.getMessage(DEFAULT_ERROR_MESSAGES.TYPE, this.acceptedTypes.join(", "), typeof value); return unbound(value, options, proxy, ...args); }.bind(this); } /** * @summary Duck typing for Validators * @param val */ static isValidator(val) { return val.constructor && !!val["hasErrors"]; } } /** * @description * Abstract class for defining synchronous validators. * * This class extends the base {@link BaseValidator} and enforces that any implementation of `hasErrors` must be synchronous. * * Use this when the validation process is immediate and does not require asynchronous operations. * * @example * ```typescript * // Example of a sync