@decaf-ts/decorator-validation
Version:
simple decorator based validation engine
421 lines • 17.9 kB
JavaScript
"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