UNPKG

@decaf-ts/decorator-validation

Version:
421 lines 17.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Model = void 0; const serialization_1 = require("./../utils/serialization.cjs"); const validation_1 = require("./validation.cjs"); const hashing_1 = require("./../utils/hashing.cjs"); const constants_1 = require("./../utils/constants.cjs"); const constants_2 = require("./../constants/index.cjs"); const decoration_1 = require("@decaf-ts/decoration"); const equality_1 = require("./../utils/equality.cjs"); const ModelRegistry_1 = require("./ModelRegistry.cjs"); /** * @summary Abstract class representing a Validatable Model object * @description Meant to be used as a base class for all Model classes * * Model objects must: * - Have all their required properties marked with '!'; * - Have all their optional properties marked as '?': * * @param {ModelArg<Model>} model base object from which to populate properties from * * @class Model * @category Model * @abstract * @implements Validatable * @implements Serializable * * @example * class ClassName { * @required() * requiredPropertyName!: PropertyType; * * optionalPropertyName?: PropertyType; * } */ class Model { // eslint-disable-next-line @typescript-eslint/no-unused-vars constructor(arg = undefined) { } isAsync() { const self = this; return !!(self[constants_2.ASYNC_META_KEY] ?? self?.constructor[constants_2.ASYNC_META_KEY]); } /** * @description Validates the model object against its defined validation rules * @summary Validates the object according to its decorated properties, returning any validation errors * * @param {any[]} [exceptions] - Properties in the object to be ignored for the validation. Marked as 'any' to allow for extension but expects strings * @return {ModelErrorDefinition | undefined} - Returns a ModelErrorDefinition object if validation errors exist, otherwise undefined */ hasErrors(...exceptions) { return (0, validation_1.validate)(this, this.isAsync(), ...exceptions); } /** * @description Determines if this model is equal to another object * @summary Compare object equality recursively, checking all properties unless excluded * * @param {any} obj - Object to compare to * @param {string[]} [exceptions] - Property names to be excluded from the comparison * @return {boolean} - True if objects are equal, false otherwise */ equals(obj, ...exceptions) { return (0, equality_1.isEqual)(this, obj, ...exceptions); } compare(other, ...exceptions) { const props = decoration_1.Metadata.properties(this.constructor); if (!props || !props.length) return undefined; const diff = props.reduce((acc, el) => { const k = el; if (exceptions.includes(k)) return acc; if (typeof this[k] === "undefined" && typeof other[k] !== "undefined") { acc[k] = { other: other[k], current: undefined }; return acc; } if (typeof this[k] !== "undefined" && typeof other[k] === "undefined") { acc[k] = { other: undefined, current: this[k] }; return acc; } if ((0, equality_1.isEqual)(this[k], other[k])) return acc; if (Model.isPropertyModel(this, k)) { const nestedDiff = this[k].compare(other[k]); if (nestedDiff) { acc[k] = nestedDiff; } return acc; } if (Array.isArray(this[k]) && Array.isArray(other[k])) { if (this[k].length !== other[k].length) { acc[k] = { current: this[k], other: other[k] }; return acc; } const listDiff = this[k].map((item, i) => { if ((0, equality_1.isEqual)(item, other[k][i])) return null; if (item instanceof Model && other[k][i] instanceof Model) { return item.compare(other[k][i]); } return { current: item, other: other[k][i] }; }); if (listDiff.some((d) => d !== null)) { acc[k] = listDiff; } return acc; } acc[k] = { other: other[k], current: this[k] }; return acc; }, {}); return Object.keys(diff).length > 0 ? diff : undefined; } /** * @description Converts the model to a serialized string representation * @summary Returns the serialized model according to the currently defined {@link Serializer} * * @return {string} - The serialized string representation of the model */ serialize() { return Model.serialize(this); } /** * @description Provides a human-readable string representation of the model * @summary Override the implementation for js's 'toString()' to provide a more useful representation * * @return {string} - A string representation of the model including its class name and JSON representation * @override */ toString() { return this.constructor.name + ": " + JSON.stringify(this, undefined, 2); } /** * @description Generates a hash string for the model object * @summary Defines a default implementation for object hash, relying on a basic implementation based on Java's string hash * * @return {string} - A hash string representing the model */ hash() { return Model.hash(this); } /** * @description Converts a serialized string back into a model instance * @summary Deserializes a Model from its string representation * * @param {string} str - The serialized string to convert back to a model * @return {any} - The deserialized model instance * @throws {Error} If it fails to parse the string, or if it fails to build the model */ static deserialize(str) { let metadata; try { metadata = decoration_1.Metadata.get(this.constructor, constants_1.ModelKeys.SERIALIZATION); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { metadata = undefined; } if (metadata && metadata.serializer) return serialization_1.Serialization.deserialize(str, metadata.serializer, ...(metadata.args || [])); return serialization_1.Serialization.deserialize(str); } /** * @description Copies properties from a source object to a model instance * @summary Repopulates the Object properties with the ones from the new object * * @template T * @param {T} self - The target model instance to update * @param {T | Record<string, any>} [obj] - The source object containing properties to copy * @return {T} - The updated model instance */ static fromObject(self, obj) { if (!obj) obj = {}; for (const prop of Model.getAttributes(self)) { self[prop] = obj[prop] ?? self[prop] ?? undefined; } return self; } /** * @description Copies and rebuilds properties from a source object to a model instance, handling nested models * @summary Repopulates the instance with properties from the new Model Object, recursively rebuilding nested models * * @template T * @param {T} self - The target model instance to update * @param {T | Record<string, any>} [obj] - The source object containing properties to copy * @return {T} - The updated model instance with rebuilt nested models * * @mermaid * sequenceDiagram * participant C as Client * participant M as Model.fromModel * participant B as Model.build * participant R as Reflection * * C->>M: fromModel(self, obj) * M->>M: Get attributes from self * loop For each property * M->>M: Copy property from obj to self * alt Property is a model * M->>M: Check if property is a model * M->>B: build(property, modelType) * B-->>M: Return built model * else Property is a complex type * M->>R: Get property decorators * R-->>M: Return decorators * M->>M: Filter type decorators * alt Property is Array/Set with list decorator * M->>M: Process each item in collection * loop For each item * M->>B: build(item, itemModelType) * B-->>M: Return built model * end * else Property is another model type * M->>B: build(property, propertyType) * B-->>M: Return built model * end * end * end * M-->>C: Return updated self */ static fromModel(self, obj) { return ModelRegistry_1.ModelRegistryManager.fromModel(self, obj); } /** * @description Configures the global model builder function * @summary Sets the Global {@link ModelBuilderFunction} used for building model instances * * @param {ModelBuilderFunction} [builder] - The builder function to set as the global builder * @return {void} */ static setBuilder(builder) { ModelRegistry_1.ModelRegistryManager.setBuilder(builder); } /** * @description Retrieves the currently configured global model builder function * @summary Returns the current global {@link ModelBuilderFunction} used for building model instances * * @return {ModelBuilderFunction | undefined} - The current global builder function or undefined if not set */ static getBuilder() { return ModelRegistry_1.ModelRegistryManager.getBuilder(); } /** * @description Provides access to the current model registry * @summary Returns the current {@link ModelRegistryManager} instance, creating one if it doesn't exist * * @return {ModelRegistry<any>} - The current model registry, defaults to a new {@link ModelRegistryManager} if not set * @private */ static getRegistry() { return ModelRegistry_1.ModelRegistryManager.getRegistry(); } /** * @description Configures the model registry to be used by the Model system * @summary Sets the current model registry to a custom implementation * * @param {BuilderRegistry<any>} modelRegistry - The new implementation of Registry to use * @return {void} */ static setRegistry(modelRegistry) { ModelRegistry_1.ModelRegistryManager.setRegistry(modelRegistry); } /** * @description Registers a model constructor with the model registry * @summary Registers new model classes to make them available for serialization and deserialization * * @template T * @param {ModelConstructor<T>} constructor - The model constructor to register * @param {string} [name] - Optional name to register the constructor under, defaults to constructor.name * @return {void} * * @see ModelRegistry */ static register(constructor, name) { return ModelRegistry_1.ModelRegistryManager.getRegistry().register(constructor, name); } /** * @description Retrieves a registered model constructor by name * @summary Gets a registered Model {@link ModelConstructor} from the model registry * * @template T * @param {string} name - The name of the model constructor to retrieve * @return {ModelConstructor<T> | undefined} - The model constructor if found, undefined otherwise * * @see ModelRegistry */ static get(name) { return ModelRegistry_1.ModelRegistryManager.getRegistry().get(name); } /** * @description Creates a model instance from a plain object * @summary Builds a model instance using the model registry, optionally specifying the model class * * @template T * @param {Record<string, any>} obj - The source object to build the model from * @param {string} [clazz] - When provided, it will attempt to find the matching constructor by name * @return {T} - The built model instance * @throws {Error} If clazz is not found, or obj is not a {@link Model} meaning it has no {@link ModelKeys.ANCHOR} property * * @see ModelRegistry */ static build(obj = {}, clazz) { return Model.getRegistry().build(obj, clazz); } /** * @description Retrieves all attribute names from a model class or instance * @summary Gets all attributes defined in a model, traversing the prototype chain to include inherited attributes * * @template V * @param {Constructor<V> | V} model - The model class or instance to get attributes from * @return {string[]} - Array of attribute names defined in the model */ static getAttributes(model) { return decoration_1.Metadata.getAttributes(model); } /** * @description Compares two model instances for equality * @summary Determines if two model instances are equal by comparing their properties * * @template M * @param {M} obj1 - First model instance to compare * @param {M} obj2 - Second model instance to compare * @param {any[]} [exceptions] - Property names to exclude from comparison * @return {boolean} - True if the models are equal, false otherwise */ static equals(obj1, obj2, ...exceptions) { return (0, equality_1.isEqual)(obj1, obj2, ...exceptions); } /** * @description Validates a model instance against its validation rules * @summary Checks if a model has validation errors, optionally ignoring specified properties * * @template M * @param {M} model - The model instance to validate * @param {boolean} async - A flag indicating whether validation should be asynchronous. * @param {string[]} [propsToIgnore] - Properties to exclude from validation * @return {ModelErrorDefinition | undefined} - Returns validation errors if any, otherwise undefined */ static hasErrors(model, async, ...propsToIgnore) { return (0, validation_1.validate)(model, async, ...propsToIgnore); } /** * @description Converts a model instance to a serialized string * @summary Serializes a model instance using the configured serializer or the default one * * @template M * @param {M} model - The model instance to serialize * @return {string} - The serialized string representation of the model */ static serialize(model) { let metadata; try { metadata = decoration_1.Metadata.get(model.constructor, constants_1.ModelKeys.SERIALIZATION); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { metadata = undefined; } if (metadata && metadata.serializer) return serialization_1.Serialization.serialize(this, metadata.serializer, ...(metadata.args || [])); return serialization_1.Serialization.serialize(model); } /** * @description Generates a hash string for a model instance * @summary Creates a hash representation of a model using the configured algorithm or the default one * * @template M * @param {M} model - The model instance to hash * @return {string} - The hash string representing the model */ static hash(model) { const metadata = decoration_1.Metadata.get(model.constructor, constants_1.ModelKeys.HASHING); if (metadata && metadata.algorithm) return hashing_1.Hashing.hash(model, metadata.algorithm, ...(metadata.args || [])); return hashing_1.Hashing.hash(model); } /** * @description Determines if an object is a model instance or has model metadata * @summary Checks whether a given object is either an instance of the Model class or * has model metadata attached to it. This function is essential for serialization and * deserialization processes, as it helps identify model objects that need special handling. * It safely handles potential errors during metadata retrieval. * * @param {Record<string, any>} target - The object to check * @return {boolean} True if the object is a model instance or has model metadata, false otherwise * * @example * ```typescript * // Check if an object is a model * const user = new User({ name: "John" }); * const isUserModel = isModel(user); // true * * // Check a plain object * const plainObject = { name: "John" }; * const isPlainObjectModel = isModel(plainObject); // false * ``` */ static isModel(target) { return decoration_1.Metadata.isModel(target); } /** * @description Checks if a property of a model is itself a model or has a model type * @summary Determines whether a specific property of a model instance is either a model instance * or has a type that is registered as a model * * @template M * @param {M} target - The model instance to check * @param {string} attribute - The property name to check * @return {boolean | string | undefined} - Returns true if the property is a model instance, * the model name if the property has a model type, or undefined if not a model */ static isPropertyModel(target, attribute) { return decoration_1.Metadata.isPropertyModel(target, attribute); } static describe(model, key) { return decoration_1.Metadata.description(model, key); } } exports.Model = Model; //# sourceMappingURL=Model.js.map