@decaf-ts/db-decorators
Version:
Agnostic database decorators and repository
1,081 lines (1,067 loc) • 405 kB
JavaScript
(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