UNPKG

@decaf-ts/db-decorators

Version:

Agnostic database decorators and repository

1,081 lines (1,067 loc) 405 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@decaf-ts/decorator-validation'), require('tslib'), require('@decaf-ts/reflection'), require('typed-object-accumulator')) : typeof define === 'function' && define.amd ? define(['exports', '@decaf-ts/decorator-validation', 'tslib', '@decaf-ts/reflection', 'typed-object-accumulator'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["db-decorators"] = {}, global.decoratorValidation, global.tslib, global.reflection, global.typedObjectAccumulator)); })(this, (function (exports, decoratorValidation, tslib, reflection, typedObjectAccumulator) { 'use strict'; /** * @description Database reflection keys * @summary Collection of keys used for reflection metadata in database operations * @const DBKeys * @memberOf module:db-decorators */ const DBKeys = { REFLECT: `${decoratorValidation.ModelKeys.REFLECT}persistence.`, REPOSITORY: "repository", CLASS: "_class", ID: "id", INDEX: "index", UNIQUE: "unique", SERIALIZE: "serialize", READONLY: "readonly", TIMESTAMP: "timestamp", TRANSIENT: "transient", HASH: "hash", COMPOSED: "composed", VERSION: "version", ORIGINAL: "__originalObj", }; /** * @description Default separator character for composite indexes * @summary The default separator character used when concatenating multiple fields into a single index * @const DefaultSeparator * @memberOf module:db-decorators */ const DefaultSeparator = "_"; /** * @description Default format for timestamp fields * @summary Standard date format string used for timestamp fields in database models * @const DEFAULT_TIMESTAMP_FORMAT * @memberOf module:db-decorators */ const DEFAULT_TIMESTAMP_FORMAT = "dd/MM/yyyy HH:mm:ss:S"; /** * @description Collection of default error messages used by validators. * @summary Holds the default error messages for various validation scenarios including ID validation, readonly properties, and timestamps. * @typedef {Object} ErrorMessages * @property {Object} ID - Error messages for ID validation * @property {string} ID.INVALID - Error message when an ID is invalid * @property {string} ID.REQUIRED - Error message when an ID is missing * @property {Object} READONLY - Error messages for readonly properties * @property {string} READONLY.INVALID - Error message when attempting to update a readonly property * @property {Object} TIMESTAMP - Error messages for timestamp validation * @property {string} TIMESTAMP.REQUIRED - Error message when a timestamp is missing * @property {string} TIMESTAMP.DATE - Error message when a timestamp is not a valid date * @property {string} TIMESTAMP.INVALID - Error message when a timestamp is not increasing * @const DEFAULT_ERROR_MESSAGES * @memberOf module:validation */ const DEFAULT_ERROR_MESSAGES = { ID: { INVALID: "This Id is invalid", REQUIRED: "The Id is mandatory", }, READONLY: { INVALID: "This cannot be updated", }, TIMESTAMP: { REQUIRED: "Timestamp is Mandatory", DATE: "The Timestamp must the a valid date", INVALID: "This value must always increase", }, }; /** * @description Constants used for reflection-based validation during update operations. * @summary Keys used for storing and retrieving validation metadata on model properties during update operations. * @typedef {Object} ValidationKeys * @property {string} REFLECT - Base reflection key prefix for update validation * @property {string} TIMESTAMP - Key for timestamp validation * @property {string} READONLY - Key for readonly property validation * @const UpdateValidationKeys * @memberOf module:validation */ const UpdateValidationKeys = { REFLECT: "db.update.validation.", TIMESTAMP: DBKeys.TIMESTAMP, READONLY: DBKeys.READONLY, }; /** * @description A validator that ensures properties marked as readonly cannot be modified during updates. * @summary Validator for the {@link readonly} decorator that checks if a value has been changed during an update operation. It compares the new value with the old value and returns an error message if they are not equal. * @param {any} value - The value to be validated * @param {any} oldValue - The previous value to compare against * @param {string} [message] - Optional custom error message * @class ReadOnlyValidator * @example * // Using ReadOnlyValidator with a readonly property * class User { * @readonly() * id: string; * * name: string; * * constructor(id: string, name: string) { * this.id = id; * this.name = name; * } * } * * // This will trigger validation error when trying to update * const user = new User('123', 'John'); * user.id = '456'; // Will be prevented by ReadOnlyValidator * @category Validators */ exports.ReadOnlyValidator = class ReadOnlyValidator extends decoratorValidation.Validator { constructor() { super(DEFAULT_ERROR_MESSAGES.READONLY.INVALID); } /** * @description Implementation of the base validator's hasErrors method. * @summary This method is required by the Validator interface but not used in this validator as validation only happens during updates. * @param {any} value - The value to validate * @param {any[]} args - Additional arguments * @return {string | undefined} Always returns undefined as this validator only works during updates */ // eslint-disable-next-line @typescript-eslint/no-unused-vars hasErrors(value, ...args) { return undefined; } /** * @description Checks if a value has been modified during an update operation. * @summary Validates a value has not changed by comparing it with the previous value using deep equality. * @param {any} value - The new value to validate * @param {any} oldValue - The original value to compare against * @param {string} [message] - Optional custom error message to override the default * @return {string | undefined} An error message if validation fails, undefined otherwise */ updateHasErrors(value, oldValue, message) { if (value === undefined) return; return reflection.isEqual(value, oldValue) ? undefined : this.getMessage(message || this.message); } }; exports.ReadOnlyValidator = tslib.__decorate([ decoratorValidation.validator(UpdateValidationKeys.READONLY), tslib.__metadata("design:paramtypes", []) ], exports.ReadOnlyValidator); /** * @description A validator that ensures timestamp values are only updated with newer timestamps. * @summary Validates the update of a timestamp by comparing the new timestamp with the old one, ensuring the new timestamp is more recent. * @param {Date|string|number} value - The timestamp value to validate * @param {Date|string|number} oldValue - The previous timestamp to compare against * @param {string} [message] - Optional custom error message * @class TimestampValidator * @example * // Using TimestampValidator with a timestamp property * class Document { * @timestamp() * updatedAt: Date; * * title: string; * * constructor(title: string) { * this.title = title; * this.updatedAt = new Date(); * } * } * * // This will trigger validation error when trying to update with an older timestamp * const doc = new Document('My Document'); * const oldDate = new Date(2020, 0, 1); * doc.updatedAt = oldDate; // Will be prevented by TimestampValidator * @category Validators */ exports.TimestampValidator = class TimestampValidator extends decoratorValidation.Validator { constructor() { super(DEFAULT_ERROR_MESSAGES.TIMESTAMP.INVALID); } /** * @description Implementation of the base validator's hasErrors method. * @summary This method is required by the Validator interface but not used in this validator as validation only happens during updates. * @param {any} value - The timestamp value to validate * @param {any[]} args - Additional arguments * @return {string | undefined} Always returns undefined as this validator only works during updates */ // eslint-disable-next-line @typescript-eslint/no-unused-vars hasErrors(value, ...args) { return undefined; } /** * @description Validates that a timestamp is newer than its previous value. * @summary Checks if a timestamp has been updated with a more recent value by converting both values to Date objects and comparing them. * @param {Date|string|number} value - The new timestamp value to validate * @param {Date|string|number} oldValue - The original timestamp to compare against * @param {string} [message] - Optional custom error message to override the default * @return {string | undefined} An error message if validation fails (new timestamp is not newer), undefined otherwise */ updateHasErrors(value, oldValue, message) { if (value === undefined) return; message = message || this.getMessage(message || this.message); try { value = new Date(value); oldValue = new Date(oldValue); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return message; } return value <= oldValue ? message : undefined; } }; exports.TimestampValidator = tslib.__decorate([ decoratorValidation.validator(UpdateValidationKeys.TIMESTAMP), tslib.__metadata("design:paramtypes", []) ], exports.TimestampValidator); /** * @description Abstract base class for validators that compare new values with old values during updates. * @summary Base class for an Update validator that provides a framework for implementing validation logic that compares a new value with its previous state. * @param {string} [message] - Error message. Defaults to {@link DecoratorMessages#DEFAULT} * @param {string[]} [acceptedTypes] - The accepted value types by the decorator * @class UpdateValidator * @example * // Extending UpdateValidator to create a custom validator * class MyCustomValidator extends UpdateValidator { * constructor() { * super("Custom validation failed"); * } * * public updateHasErrors(value: any, oldValue: any): string | undefined { * // Custom validation logic * if (value === oldValue) { * return this.message; * } * return undefined; * } * * hasErrors(value: any): string | undefined { * return undefined; // Not used for update validators * } * } * @category Validators */ class UpdateValidator extends decoratorValidation.Validator { constructor(message = decoratorValidation.DEFAULT_ERROR_MESSAGES.DEFAULT, ...acceptedTypes) { super(message, ...acceptedTypes); } } /** * @description Generates a key for update validation metadata. * @summary Builds the key to store as metadata under Reflections for update validation by prefixing the provided key with the update validation prefix. * @param {string} key - The base key to be prefixed * @return {string} The complete metadata key for update validation * @function updateKey * @memberOf module:db-decorators */ decoratorValidation.Validation.updateKey = function (key) { return UpdateValidationKeys.REFLECT + key; }; /** * @description Database operation key constants * @summary Enum defining CRUD operations and their lifecycle phases * @enum {string} * @readonly * @memberOf module:db-decorators */ exports.OperationKeys = void 0; (function (OperationKeys) { OperationKeys["REFLECT"] = "decaf.model.db.operations."; OperationKeys["CREATE"] = "create"; OperationKeys["READ"] = "read"; OperationKeys["UPDATE"] = "update"; OperationKeys["DELETE"] = "delete"; OperationKeys["ON"] = "on."; OperationKeys["AFTER"] = "after."; })(exports.OperationKeys || (exports.OperationKeys = {})); /** * @description Bulk database operation key constants * @summary Enum defining bulk CRUD operations for handling multiple records at once * @enum {string} * @readonly * @memberOf module:db-decorators */ exports.BulkCrudOperationKeys = void 0; (function (BulkCrudOperationKeys) { BulkCrudOperationKeys["CREATE_ALL"] = "createAll"; BulkCrudOperationKeys["READ_ALL"] = "readAll"; BulkCrudOperationKeys["UPDATE_ALL"] = "updateAll"; BulkCrudOperationKeys["DELETE_ALL"] = "deleteAll"; })(exports.BulkCrudOperationKeys || (exports.BulkCrudOperationKeys = {})); /** * @description Grouped CRUD operations for decorator mapping * @summary Maps out groups of CRUD operations for easier mapping of decorators * @const DBOperations * @memberOf module:db-decorators */ const DBOperations = { CREATE: [exports.OperationKeys.CREATE], READ: [exports.OperationKeys.READ], UPDATE: [exports.OperationKeys.UPDATE], DELETE: [exports.OperationKeys.DELETE], CREATE_UPDATE: [exports.OperationKeys.CREATE, exports.OperationKeys.UPDATE], READ_CREATE: [exports.OperationKeys.READ, exports.OperationKeys.CREATE], ALL: [ exports.OperationKeys.CREATE, exports.OperationKeys.READ, exports.OperationKeys.UPDATE, exports.OperationKeys.DELETE, ], }; /** * @description Registry for database operation handlers * @summary Manages and stores operation handlers for different model properties and operations * @class OperationsRegistry * @template M - Model type * @template R - Repository type * @template V - Metadata type * @template F - Repository flags * @template C - Context type * @example * // Create a registry and register a handler * const registry = new OperationsRegistry(); * registry.register(myHandler, OperationKeys.CREATE, targetModel, 'propertyName'); * * // Get handlers for a specific operation * const handlers = registry.get(targetModel.constructor.name, 'propertyName', 'onCreate'); * * @mermaid * classDiagram * class OperationsRegistry { * -cache: Record~string, Record~string|symbol, Record~string, Record~string, OperationHandler~~~~ * +get(target, propKey, operation, accum) * +register(handler, operation, target, propKey) * } */ class OperationsRegistry { constructor() { this.cache = {}; } /** * @description Retrieves operation handlers for a specific target and operation * @summary Finds all registered handlers for a given target, property, and operation, including from parent classes * @template M - Model type extending Model * @template R - Repository type extending IRepository * @template V - Metadata type * @template F - Repository flags extending RepositoryFlags * @template C - Context type extending Context<F> * @param {string | Record<string, any>} target - The target class name or object * @param {string} propKey - The property key to get handlers for * @param {string} operation - The operation key to get handlers for * @param {OperationHandler[]} [accum] - Accumulator for recursive calls * @return {OperationHandler[] | undefined} Array of handlers or undefined if none found */ get(target, propKey, operation, accum) { accum = accum || []; let name; try { name = typeof target === "string" ? target : target.constructor.name; accum.unshift(...Object.values(this.cache[name][propKey][operation] || [])); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { if (typeof target === "string" || target === Object.prototype || Object.getPrototypeOf(target) === Object.prototype) return accum; } let proto = Object.getPrototypeOf(target); if (proto.constructor.name === name) proto = Object.getPrototypeOf(proto); return this.get(proto, propKey, operation, accum); } /** * @description Registers an operation handler for a specific target and operation * @summary Stores a handler in the registry for a given target, property, and operation * @template M - Model type extending Model * @template R - Repository type extending IRepository * @template V - Metadata type * @template F - Repository flags extending RepositoryFlags * @template C - Context type extending Context<F> * @param {OperationHandler} handler - The handler function to register * @param {OperationKeys} operation - The operation key to register the handler for * @param {M} target - The target model instance * @param {string | symbol} propKey - The property key to register the handler for * @return {void} */ register(handler, operation, target, propKey) { const name = target.constructor.name; const handlerName = Operations.getHandlerName(handler); if (!this.cache[name]) this.cache[name] = {}; if (!this.cache[name][propKey]) this.cache[name][propKey] = {}; if (!this.cache[name][propKey][operation]) this.cache[name][propKey][operation] = {}; if (this.cache[name][propKey][operation][handlerName]) return; this.cache[name][propKey][operation][handlerName] = handler; } } /** * @description Static utility class for database operation management * @summary Provides functionality for registering, retrieving, and managing database operation handlers * @class Operations * @template M - Model type * @template R - Repository type * @template V - Metadata type * @template F - Repository flags * @template C - Context type * @example * // Register a handler for a create operation * Operations.register(myHandler, OperationKeys.CREATE, targetModel, 'propertyName'); * * // Get handlers for a specific operation * const handlers = Operations.get(targetModel.constructor.name, 'propertyName', 'onCreate'); * * @mermaid * classDiagram * class Operations { * -registry: OperationsRegistry * +getHandlerName(handler) * +key(str) * +get(targetName, propKey, operation) * -getOpRegistry() * +register(handler, operation, target, propKey) * } * Operations --> OperationsRegistry : uses */ class Operations { constructor() { } /** * @description Gets a unique name for an operation handler * @summary Returns the name of the handler function or generates a hash if name is not available * @param {OperationHandler<any, any, any, any, any>} handler - The handler function to get the name for * @return {string} The name of the handler or a generated hash */ static getHandlerName(handler) { if (handler.name) return handler.name; console.warn("Handler name not defined. A name will be generated, but this is not desirable. please avoid using anonymous functions"); return decoratorValidation.Hashing.hash(handler.toString()); } /** * @description Generates a reflection metadata key * @summary Creates a fully qualified metadata key by prefixing with the reflection namespace * @param {string} str - The operation key string to prefix * @return {string} The fully qualified metadata key */ static key(str) { return exports.OperationKeys.REFLECT + str; } /** * @description Retrieves operation handlers for a specific target and operation * @summary Gets registered handlers from the operations registry for a given target, property, and operation * @template M - Model type extending Model * @template R - Repository type extending IRepository * @template V - Metadata type, defaults to object * @template F - Repository flags extending RepositoryFlags * @template C - Context type extending Context<F> * @param {string | Record<string, any>} targetName - The target class name or object * @param {string} propKey - The property key to get handlers for * @param {string} operation - The operation key to get handlers for * @return {any} The registered handlers for the specified target, property, and operation */ static get(targetName, propKey, operation) { return Operations.registry.get(targetName, propKey, operation); } /** * @description Gets or initializes the operations registry * @summary Returns the existing registry or creates a new one if it doesn't exist * @return {OperationsRegistry} The operations registry instance * @private */ static getOpRegistry() { if (!Operations.registry) Operations.registry = new OperationsRegistry(); return Operations.registry; } /** * @description Registers an operation handler for a specific target and operation * @summary Adds a handler to the operations registry for a given target, property, and operation * @template V - Model type extending Model * @param {OperationHandler<V, any, any>} handler - The handler function to register * @param {OperationKeys} operation - The operation key to register the handler for * @param {V} target - The target model instance * @param {string | symbol} propKey - The property key to register the handler for * @return {void} */ static register(handler, operation, target, propKey) { Operations.getOpRegistry().register(handler, operation, target, propKey); } } /** * @description Internal function to register operation handlers * @summary Registers an operation handler for a specific operation key on a target property * @param {OperationKeys} op - The operation key to handle * @param {OperationHandler<any, any, any, any, any>} handler - The handler function to register * @return {PropertyDecorator} A decorator that registers the handler * @function handle * @category Property Decorators */ function handle(op, handler) { return (target, propertyKey) => { Operations.register(handler, op, target, propertyKey); }; } /** * @description Decorator for handling create and update operations * @summary Defines a behavior to execute during both create and update operations * @template V - Type for metadata, defaults to object * @param {StandardOperationHandler<any, any, V, any, any> | UpdateOperationHandler<any, any, V, any, any>} handler - The method called upon the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function onCreateUpdate * @category Property Decorators */ function onCreateUpdate(handler, data) { return on(DBOperations.CREATE_UPDATE, handler, data); } /** * @description Decorator for handling update operations * @summary Defines a behavior to execute during update operations * @template V - Type for metadata, defaults to object * @param {UpdateOperationHandler<any, any, V, any>} handler - The method called upon the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function onUpdate * @category Property Decorators */ function onUpdate(handler, data) { return on(DBOperations.UPDATE, handler, data); } /** * @description Decorator for handling create operations * @summary Defines a behavior to execute during create operations * @template V - Type for metadata, defaults to object * @param {StandardOperationHandler<any, any, V, any, any>} handler - The method called upon the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function onCreate * @category Property Decorators */ function onCreate(handler, data) { return on(DBOperations.CREATE, handler, data); } /** * @description Decorator for handling read operations * @summary Defines a behavior to execute during read operations * @template V - Type for metadata, defaults to object * @param {IdOperationHandler<any, any, V, any, any>} handler - The method called upon the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function onRead * @category Property Decorators */ function onRead(handler, data) { return on(DBOperations.READ, handler, data); } /** * @description Decorator for handling delete operations * @summary Defines a behavior to execute during delete operations * @template V - Type for metadata, defaults to object * @param {OperationHandler<any, any, V, any, any>} handler - The method called upon the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function onDelete * @category Property Decorators */ function onDelete(handler, data) { return on(DBOperations.DELETE, handler, data); } /** * @description Decorator for handling all operation types * @summary Defines a behavior to execute during any database operation * @template V - Type for metadata, defaults to object * @param {OperationHandler<any, any, V, any, any>} handler - The method called upon the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function onAny * @category Property Decorators */ function onAny(handler, data) { return on(DBOperations.ALL, handler, data); } /** * @description Base decorator for handling database operations * @summary Defines a behavior to execute during specified database operations * @template V - Type for metadata, defaults to object * @param {OperationKeys[] | DBOperations} [op=DBOperations.ALL] - One or more operation types to handle * @param {OperationHandler<any, any, V, any, any>} handler - The method called upon the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function on * @category Property Decorators * @example * // Example usage: * class MyModel { * @on(DBOperations.CREATE, myHandler) * myProperty: string; * } */ function on(op = DBOperations.ALL, handler, data) { return operation(exports.OperationKeys.ON, op, handler, data); } /** * @description Decorator for handling post-create and post-update operations * @summary Defines a behavior to execute after both create and update operations * @template V - Type for metadata, defaults to object * @param {StandardOperationHandler<any, any, V, any, any> | UpdateOperationHandler<any, any, V, any, any>} handler - The method called after the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function afterCreateUpdate * @category Property Decorators */ function afterCreateUpdate(handler, data) { return after(DBOperations.CREATE_UPDATE, handler, data); } /** * @description Decorator for handling post-update operations * @summary Defines a behavior to execute after update operations * @template V - Type for metadata, defaults to object * @param {UpdateOperationHandler<any, any, V, any, any>} handler - The method called after the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function afterUpdate * @category Property Decorators */ function afterUpdate(handler, data) { return after(DBOperations.UPDATE, handler, data); } /** * @description Decorator for handling post-create operations * @summary Defines a behavior to execute after create operations * @template V - Type for metadata, defaults to object * @param {StandardOperationHandler<any, any, V, any, any>} handler - The method called after the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function afterCreate * @category Property Decorators */ function afterCreate(handler, data) { return after(DBOperations.CREATE, handler, data); } /** * @description Decorator for handling post-read operations * @summary Defines a behavior to execute after read operations * @template V - Type for metadata, defaults to object * @param {StandardOperationHandler<any, any, V, any, any>} handler - The method called after the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function afterRead * @category Property Decorators */ function afterRead(handler, data) { return after(DBOperations.READ, handler, data); } /** * @description Decorator for handling post-delete operations * @summary Defines a behavior to execute after delete operations * @template V - Type for metadata, defaults to object * @param {StandardOperationHandler<any, any, V, any, any>} handler - The method called after the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function afterDelete * @category Property Decorators */ function afterDelete(handler, data) { return after(DBOperations.DELETE, handler, data); } /** * @description Decorator for handling post-operation for all operation types * @summary Defines a behavior to execute after any database operation * @template V - Type for metadata, defaults to object * @param {StandardOperationHandler<any, any, V, any, any>} handler - The method called after the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function afterAny * @category Property Decorators */ function afterAny(handler, data) { return after(DBOperations.ALL, handler, data); } /** * @description Base decorator for handling post-operation behaviors * @summary Defines a behavior to execute after specified database operations * @template V - Type for metadata, defaults to object * @param {OperationKeys[] | DBOperations} [op=DBOperations.ALL] - One or more operation types to handle * @param {OperationHandler<any, any, V, any, any>} handler - The method called after the operation * @param {V} [data] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function after * @category Property Decorators * @example * // Example usage: * class MyModel { * @after(DBOperations.CREATE, myHandler) * myProperty: string; * } */ function after(op = DBOperations.ALL, handler, data) { return operation(exports.OperationKeys.AFTER, op, handler, data); } /** * @description Core decorator factory for operation handlers * @summary Creates decorators that register handlers for database operations * @template V - Type for metadata, defaults to object * @param {OperationKeys.ON | OperationKeys.AFTER} baseOp - Whether the handler runs during or after the operation * @param {OperationKeys[]} [operation=DBOperations.ALL] - The specific operations to handle * @param {OperationHandler<any, any, V, any, any>} handler - The handler function to execute * @param {V} [dataToAdd] - Optional metadata to pass to the handler * @return {PropertyDecorator} A decorator that can be applied to class properties * @function operation * @category Property Decorators * @mermaid * sequenceDiagram * participant Client * participant Decorator as @operation * participant Operations as Operations Registry * participant Handler * * Client->>Decorator: Apply to property * Decorator->>Operations: Register handler * Decorator->>Decorator: Store metadata * * Note over Client,Handler: Later, during operation execution * Client->>Operations: Execute operation * Operations->>Handler: Call registered handler * Handler-->>Operations: Return result * Operations-->>Client: Return final result */ function operation(baseOp, operation = DBOperations.ALL, handler, dataToAdd) { return (target, propertyKey) => { const name = target.constructor.name; const decorators = operation.reduce((accum, op) => { const compoundKey = baseOp + op; let data = Reflect.getMetadata(Operations.key(compoundKey), target, propertyKey); if (!data) data = { operation: op, handlers: {}, }; const handlerKey = Operations.getHandlerName(handler); if (!data.handlers[name] || !data.handlers[name][propertyKey] || !(handlerKey in data.handlers[name][propertyKey])) { data.handlers[name] = data.handlers[name] || {}; data.handlers[name][propertyKey] = data.handlers[name][propertyKey] || {}; data.handlers[name][propertyKey][handlerKey] = { data: dataToAdd, }; accum.push(handle(compoundKey, handler), decoratorValidation.propMetadata(Operations.key(compoundKey), data)); } return accum; }, []); return reflection.apply(...decorators)(target, propertyKey); }; } /** * @description Base error class for the repository module * @summary Abstract base error class that all other error types extend from. Provides common error handling functionality. * @param {string} name - The name of the error * @param {string|Error} msg - The error message or Error object * @param {number} code - The HTTP status code associated with this error * @class BaseError * @example * // This is an abstract class and should not be instantiated directly * // Instead, use one of the concrete error classes: * throw new ValidationError('Invalid data provided'); */ class BaseError extends Error { constructor(name, msg, code = 500) { if (msg instanceof BaseError) return msg; const message = `[${name}] ${msg instanceof Error ? msg.message : msg}`; super(message); this.code = code; if (msg instanceof Error) this.stack = msg.stack; } } /** * @description Error thrown when validation fails * @summary Represents a failure in the Model details, typically thrown when data validation fails * @param {string|Error} msg - The error message or Error object * @return {ValidationError} A new ValidationError instance * @class ValidationError * @example * // Throw a validation error when data is invalid * if (!isValid(data)) { * throw new ValidationError('Invalid data format'); * } */ class ValidationError extends BaseError { constructor(msg) { super(ValidationError.name, msg, 422); } } /** * @description Error thrown for internal system failures * @summary Represents an internal failure (should mean an error in code) with HTTP 500 status code * @param {string|Error} msg - The error message or Error object * @return {InternalError} A new InternalError instance * @class InternalError * @example * // Throw an internal error when an unexpected condition occurs * try { * // Some operation * } catch (error) { * throw new InternalError('Unexpected internal error occurred'); * } */ class InternalError extends BaseError { constructor(msg) { super(InternalError.name, msg, 500); } } /** * @description Error thrown when serialization or deserialization fails * @summary Represents a failure in the Model de/serialization, typically when converting between data formats * @param {string|Error} msg - The error message or Error object * @return {SerializationError} A new SerializationError instance * @class SerializationError * @example * // Throw a serialization error when JSON parsing fails * try { * const data = JSON.parse(invalidJson); * } catch (error) { * throw new SerializationError('Failed to parse JSON data'); * } */ class SerializationError extends BaseError { constructor(msg) { super(SerializationError.name, msg, 422); } } /** * @description Error thrown when a requested resource is not found * @summary Represents a failure in finding a model, resulting in a 404 HTTP status code * @param {string|Error} msg - The error message or Error object * @return {NotFoundError} A new NotFoundError instance * @class NotFoundError * @example * // Throw a not found error when a record doesn't exist * const user = await repository.findById(id); * if (!user) { * throw new NotFoundError(`User with ID ${id} not found`); * } */ class NotFoundError extends BaseError { constructor(msg) { super(NotFoundError.name, msg, 404); } } /** * @description Error thrown when a conflict occurs in the storage * @summary Represents a conflict in the storage, typically when trying to create a duplicate resource * @param {string|Error} msg - The error message or Error object * @return {ConflictError} A new ConflictError instance * @class ConflictError * @example * // Throw a conflict error when trying to create a duplicate record * const existingUser = await repository.findByEmail(email); * if (existingUser) { * throw new ConflictError(`User with email ${email} already exists`); * } */ class ConflictError extends BaseError { constructor(msg) { super(ConflictError.name, msg, 409); } } /** * @summary retrieves the arguments for the handler * @param {any} dec the decorator * @param {string} prop the property name * @param {{}} m the model * @param {{}} [accum] accumulator used for internal recursiveness * * @function getHandlerArgs * @memberOf module:db-decorators.Repository */ const getHandlerArgs = function (dec, prop, m, accum) { const name = m.constructor.name; if (!name) throw new InternalError("Could not determine model class"); accum = accum || {}; if (dec.props.handlers[name] && dec.props.handlers[name][prop]) accum = { ...dec.props.handlers[name][prop], ...accum }; let proto = Object.getPrototypeOf(m); if (proto === Object.prototype) return accum; if (proto.constructor.name === name) proto = Object.getPrototypeOf(proto); return getHandlerArgs(dec, prop, proto, accum); }; /** * * @param {IRepository<T>} repo * @param context * @param {T} model * @param operation * @param prefix * * @param oldModel * @function enforceDBPropertyDecoratorsAsync * * @memberOf db-decorators.utils */ async function enforceDBDecorators(repo, context, model, operation, prefix, oldModel) { const decorators = getDbDecorators(model, operation, prefix); if (!decorators) return; for (const prop in decorators) { const decs = decorators[prop]; for (const dec of decs) { const { key } = dec; const handlers = Operations.get(model, prop, prefix + key); if (!handlers || !handlers.length) throw new InternalError(`Could not find registered handler for the operation ${prefix + key} under property ${prop}`); const handlerArgs = getHandlerArgs(dec, prop, model); if (!handlerArgs || Object.values(handlerArgs).length !== handlers.length) throw new InternalError("Args and handlers length do not match"); let handler; let data; for (let i = 0; i < handlers.length; i++) { handler = handlers[i]; data = Object.values(handlerArgs)[i]; const args = [context, data.data, prop, model]; if (operation === exports.OperationKeys.UPDATE && prefix === exports.OperationKeys.ON) { if (!oldModel) throw new InternalError("Missing old model for update operation"); args.push(oldModel); } try { await handler.apply(repo, args); } catch (e) { const msg = `Failed to execute handler ${handler.name} for ${prop} on ${model.constructor.name} due to error: ${e}`; if (context.get("breakOnHandlerError")) throw new InternalError(msg); console.log(msg); } } } } } /** * Specific for DB Decorators * @param {T} model * @param {string} operation CRUD {@link OperationKeys} * @param {string} [extraPrefix] * * @function getDbPropertyDecorators * * @memberOf db-decorators.utils */ function getDbDecorators(model, operation, extraPrefix) { const decorators = reflection.Reflection.getAllPropertyDecorators(model, // undefined, exports.OperationKeys.REFLECT + (extraPrefix ? extraPrefix : "")); if (!decorators) return; return Object.keys(decorators).reduce((accum, decorator) => { const dec = decorators[decorator].filter((d) => d.key === operation); if (dec && dec.length) { if (!accum) accum = {}; accum[decorator] = dec; } return accum; }, undefined); } /** * @summary Retrieves the decorators for an object's properties prefixed by {@param prefixes} recursively * @param model * @param accum * @param prefixes * * @function getAllPropertyDecoratorsRecursive * @memberOf module:db-decorators.Repository */ const getAllPropertyDecoratorsRecursive = function (model, accum, ...prefixes) { const accumulator = accum || {}; const mergeDecorators = function (decs) { const pushOrSquash = (key, ...values) => { values.forEach((val) => { let match; if (!(match = accumulator[key].find((e) => e.key === val.key)) || match.props.operation !== val.props.operation) { accumulator[key].push(val); return; } if (val.key === decoratorValidation.ModelKeys.TYPE) return; const { handlers, operation } = val.props; if (!operation || !operation.match(new RegExp(`^(:?${exports.OperationKeys.ON}|${exports.OperationKeys.AFTER})(:?${exports.OperationKeys.CREATE}|${exports.OperationKeys.READ}|${exports.OperationKeys.UPDATE}|${exports.OperationKeys.DELETE})$`))) { accumulator[key].push(val); return; } const accumHandlers = match.props.handlers; Object.entries(handlers).forEach(([clazz, handlerDef]) => { if (!(clazz in accumHandlers)) { accumHandlers[clazz] = handlerDef; return; } Object.entries(handlerDef).forEach(([handlerProp, handler]) => { if (!(handlerProp in accumHandlers[clazz])) { accumHandlers[clazz][handlerProp] = handler; return; } Object.entries(handler).forEach(([handlerKey, argsObj]) => { if (!(handlerKey in accumHandlers[clazz][handlerProp])) { accumHandlers[clazz][handlerProp][handlerKey] = argsObj; return; } console.warn(`Skipping handler registration for ${clazz} under prop ${handlerProp} because handler is the same`); }); }); }); }); }; Object.entries(decs).forEach(([key, value]) => { accumulator[key] = accumulator[key] || []; pushOrSquash(key, ...value); }); }; const decs = reflection.Reflection.getAllPropertyDecorators(model, ...prefixes); if (decs) mergeDecorators(decs); if (Object.getPrototypeOf(model) === Object.prototype) return accumulator; // const name = model.constructor.name; const proto = Object.getPrototypeOf(model); if (!proto) return accumulator; // if (proto.constructor && proto.constructor.name === name) // proto = Object.getPrototypeOf(proto) return getAllPropertyDecoratorsRecursive(proto, accumulator, ...prefixes); }; /** * @description Default configuration flags for repository operations. * @summary Provides default values for repository operation flags, excluding the timestamp property. * These flags control behavior such as context handling, validation, error handling, and more. * @const DefaultRepositoryFlags * @memberOf module:db-decorators */ const DefaultRepositoryFlags = { parentContext: undefined, childContexts: [], ignoredValidationProperties: [], callArgs: [], writeOperation: false, affectedTables: [], operation: undefined, breakOnHandlerError: true, rebuildWithTransient: true, }; /** * @description Default factory for creating context instances. * @summary A factory function that creates new Context instances with the provided repository flags. * It automatically adds a timestamp to the context and returns a properly typed context instance. * @const DefaultContextFactory * @memberOf module:db-decorators */ const DefaultContextFacto