@decaf-ts/decorator-validation
Version:
simple decorator based validation engine
296 lines • 12.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModelRegistryManager = void 0;
exports.bulkModelRegister = bulkModelRegister;
const decoration_1 = require("@decaf-ts/decoration");
const constants_1 = require("./constants.cjs");
const index_1 = require("./../validation/index.cjs");
let modelBuilderFunction;
let actingModelRegistry;
/**
* @description Registry manager for model constructors that enables serialization and rebuilding
* @summary The ModelRegistryManager implements the ModelRegistry interface and provides
* functionality for registering, retrieving, and building model instances. It maintains
* a cache of model constructors indexed by name, allowing for efficient lookup and instantiation.
* This class is essential for the serialization and deserialization of model objects.
*
* @param {function(Record<string, any>): boolean} [testFunction] - Function to test if an object is a model, defaults to {@link Model#isModel}
*
* @class ModelRegistryManager
* @template M Type of model that can be registered, must extend Model
* @implements ModelRegistry<M>
* @category Model
*
* @example
* ```typescript
* // Create a model registry
* const registry = new ModelRegistryManager();
*
* // Register a model class
* registry.register(User);
*
* // Retrieve a model constructor by name
* const UserClass = registry.get("User");
*
* // Build a model instance from a plain object
* const userData = { name: "John", age: 30 };
* const user = registry.build(userData, "User");
* ```
*
* @mermaid
* sequenceDiagram
* participant C as Client
* participant R as ModelRegistryManager
* participant M as Model Class
*
* C->>R: new ModelRegistryManager(testFunction)
* C->>R: register(ModelClass)
* R->>R: Store in cache
* C->>R: get("ModelName")
* R-->>C: ModelClass constructor
* C->>R: build(data, "ModelName")
* R->>R: Get constructor from cache
* R->>M: new ModelClass(data)
* M-->>R: Model instance
* R-->>C: Model instance
*/
class ModelRegistryManager {
constructor(testFunction = decoration_1.Metadata.isModel) {
this.cache = {};
this.testFunction = testFunction;
}
/**
* @description Registers a model constructor with the registry
* @summary Adds a model constructor to the registry cache, making it available for
* later retrieval and instantiation. If no name is provided, the constructor's name
* property is used as the key in the registry.
*
* @param {ModelConstructor<M>} constructor - The model constructor to register
* @param {string} [name] - Optional name to register the constructor under, defaults to constructor.name
* @return {void}
* @throws {Error} If the constructor is not a function
*/
register(constructor, name) {
if (typeof constructor !== "function")
throw new Error("Model registering failed. Missing Class name or constructor");
name = name || constructor.name;
this.cache[name] = constructor;
}
/**
* @summary Gets a registered Model {@link ModelConstructor}
* @param {string} name
*/
get(name) {
try {
return this.cache[name];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
return undefined;
}
}
/**
* @param {Record<string, any>} obj
* @param {string} [clazz] when provided, it will attempt to find the matching constructor
*
* @throws Error If clazz is not found, or obj is not a {@link Model} meaning it has no {@link ModelKeys.ANCHOR} property
*/
build(obj = {}, clazz) {
if (!clazz && !this.testFunction(obj))
throw new Error("Provided obj is not a Model object");
const name = clazz || decoration_1.Metadata.modelName(obj.constructor);
if (!(name in this.cache))
throw new Error(`Provided class ${name} is not a registered Model object`);
return new this.cache[name](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) {
modelBuilderFunction = 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 modelBuilderFunction || ModelRegistryManager.fromModel;
}
/**
* @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) {
if (!obj)
obj = {};
let decorators;
const props = decoration_1.Metadata.getAttributes(self);
const proto = Object.getPrototypeOf(self);
let descriptor;
for (const prop of props) {
try {
self[prop] =
obj[prop] ??
self[prop] ??
undefined;
}
catch (e) {
descriptor = Object.getOwnPropertyDescriptor(proto, prop);
if (!descriptor || descriptor.writable)
throw new Error(`Unable to write property ${prop} to model: ${e}`);
}
if (typeof self[prop] !== "object")
continue;
const propM = decoration_1.Metadata.isPropertyModel(self, prop);
if (propM) {
try {
self[prop] =
ModelRegistryManager.getRegistry().build(self[prop], typeof propM === "string" ? propM : undefined);
}
catch (e) {
console.log(e);
}
continue;
}
decorators = decoration_1.Metadata.allowedTypes(self.constructor, prop);
if (!decorators || !decorators.length)
throw new Error(`failed to find decorators for property ${prop}`);
const clazz = decorators.map((t) => typeof t === "function" && !t.name ? t() : t);
const reserved = Object.values(constants_1.ReservedModels);
clazz.forEach((c) => {
if (!reserved.includes(c))
try {
switch (c.name) {
case "Array":
case "Set": {
const validation = decoration_1.Metadata.validationFor(self.constructor, prop);
if (!validation || !validation[index_1.ValidationKeys.LIST])
break;
const listDec = validation[index_1.ValidationKeys.LIST];
const clazzName = listDec.clazz
.map((t) => typeof t === "function" && !t.name
? t()
: t)
.find((t) => !constants_1.jsTypes.includes(t.name));
if (c.name === "Array")
self[prop] = self[prop].map((el) => {
return ["object", "function"].includes(typeof el) &&
clazzName
? ModelRegistryManager.getRegistry().build(el, clazzName.name)
: el;
});
if (c.name === "Set") {
const s = new Set();
for (const v of self[prop]) {
if (["object", "function"].includes(typeof v) &&
clazzName) {
s.add(ModelRegistryManager.getRegistry().build(v, clazzName.name));
}
else {
s.add(v);
}
}
self[prop] = s;
}
break;
}
default:
if (typeof self[prop] !== "undefined" &&
ModelRegistryManager.getRegistry().get(c.name))
self[prop] =
ModelRegistryManager.getRegistry().build(self[prop], c.name);
}
}
catch (e) {
console.log(e);
// do nothing. we have no registry of this class
}
});
}
return self;
}
/**
* @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() {
if (!actingModelRegistry)
actingModelRegistry = new ModelRegistryManager();
return actingModelRegistry;
}
/**
* @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) {
actingModelRegistry = modelRegistry;
}
}
exports.ModelRegistryManager = ModelRegistryManager;
/**
* @summary Bulk Registers Models
* @description Useful when using bundlers that might not evaluate all the code at once
*
* @template M extends Model
* @param {Array<Constructor<M>> | Array<{name: string, constructor: Constructor<M>}>} [models]
*
* @memberOf module:decorator-validation
* @category Model
*/
function bulkModelRegister(...models) {
models.forEach((m) => {
const constructor = (m.constructor ? m.constructor : m);
ModelRegistryManager.getRegistry().register(constructor, m.name);
});
}
//# sourceMappingURL=ModelRegistry.js.map