@decaf-ts/db-decorators
Version:
Agnostic database decorators and repository
1,229 lines (1,215 loc) • 389 kB
JavaScript
import { ModelKeys, validator, Validator, DEFAULT_ERROR_MESSAGES as DEFAULT_ERROR_MESSAGES$1, Validation, Hashing, propMetadata, sf, Decoration, date, required, type, ValidationKeys, Model, ModelErrorDefinition, getValidationDecorators, toConditionalPromise, validate } from '@decaf-ts/decorator-validation';
import { __decorate, __metadata } from 'tslib';
import { isEqual, apply, Reflection, metadata } from '@decaf-ts/reflection';
import { ObjectAccumulator } from 'typed-object-accumulator';
/**
* @description Database reflection keys
* @summary Collection of keys used for reflection metadata in database operations
* @const DBKeys
* @memberOf module:db-decorators
*/
const DBKeys = {
REFLECT: `${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
*/
let ReadOnlyValidator = class ReadOnlyValidator extends 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 isEqual(value, oldValue)
? undefined
: this.getMessage(message || this.message);
}
};
ReadOnlyValidator = __decorate([
validator(UpdateValidationKeys.READONLY),
__metadata("design:paramtypes", [])
], 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
*/
let TimestampValidator = class TimestampValidator extends 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;
}
};
TimestampValidator = __decorate([
validator(UpdateValidationKeys.TIMESTAMP),
__metadata("design:paramtypes", [])
], 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 Validator {
constructor(message = DEFAULT_ERROR_MESSAGES$1.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
*/
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
*/
var OperationKeys;
(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.";
})(OperationKeys || (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
*/
var BulkCrudOperationKeys;
(function (BulkCrudOperationKeys) {
BulkCrudOperationKeys["CREATE_ALL"] = "createAll";
BulkCrudOperationKeys["READ_ALL"] = "readAll";
BulkCrudOperationKeys["UPDATE_ALL"] = "updateAll";
BulkCrudOperationKeys["DELETE_ALL"] = "deleteAll";
})(BulkCrudOperationKeys || (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: [OperationKeys.CREATE],
READ: [OperationKeys.READ],
UPDATE: [OperationKeys.UPDATE],
DELETE: [OperationKeys.DELETE],
CREATE_UPDATE: [OperationKeys.CREATE, OperationKeys.UPDATE],
READ_CREATE: [OperationKeys.READ, OperationKeys.CREATE],
ALL: [
OperationKeys.CREATE,
OperationKeys.READ,
OperationKeys.UPDATE,
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 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 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(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(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), propMetadata(Operations.key(compoundKey), data));
}
return accum;
}, []);
return 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 === OperationKeys.UPDATE && prefix === 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.getAllPropertyDecorators(model,
// undefined,
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 === ModelKeys.TYPE)
return;
const { handlers, operation } = val.props;
if (!operation ||
!operation.match(new RegExp(`^(:?${OperationKeys.ON}|${OperationKeys.AFTER})(:?${OperationKeys.CREATE}|${OperationKeys.READ}|${OperationKeys.UPDATE}|${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.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 DefaultContextFactory = (arg) => {
return new Context().accumulate(Object.assign({}, arg, { timestamp: new Date() }));
};
/**
* @description A context management class for handling repository operations.
* @summary The Context class provides a mechanism for managing repository operations with flags,
* parent-child relationships, and state accumulation. It allows for hierarchical context chains
* and maintains operation-specific configurations while supporting type safety through generics.
*
* @template F - Type extending RepositoryFlags that defines the context configuration
*
* @param {ObjectAccumulator<F>} cache - The internal cache storing accumulated values
*
* @class
*
* @example
* ```typescript
* // Creating a new context with repository flags
* const context = new Context<RepositoryFlags>();
*
* // Accumulating values
* const enrichedContext = context.accumulate({
* writeOperation: true,
* affectedTables: ['users'],
* operation: OperationKeys.CREATE
* });
*
* // Accessing values
* const isWrite = enrichedContext.get('writeOperation'); // true
* const tables = enrichedContext.get('affectedTables'); // ['users']
* ```
*
* @mermaid
* sequenceDiagram
* participant C as Client
* participant Ctx as Context
* participant Cache as ObjectAccumulator
*
* C->>Ctx: new Context()
* Ctx->>Cache: create cache
*
* C->>Ctx: accumulate(value)
* Ctx->>Cache: accumulate(value)
* Cache-->>Ctx: updated cache
* Ctx-->>C: updated context
*
* C->>Ctx: get(key)
* Ctx->>Cache: get(key)
* alt Key exists in cache
* Cache-->>Ctx: value
* else Key not found
* Ctx->>Ctx: check parent context
* alt Parent exists
* Ctx->>Parent: get(key)
* Parent-->>Ctx: value
* else No parent
* Ctx-->>C: throw error
* end
* end
* Ctx-->>C: requested value
*/
class Context {
constructor() {
this.cache = new ObjectAccumulator();
Object.defineProperty(this, "cache", {
value: new ObjectAccumulator(),
writable: false,
enumerable: false,
configurable: true,
});
}
static { this.factory = DefaultContextFactory; }
/**
* @description Accumulates new values into the context.
* @summary Merges the provided value object with the existing context state,
* creating a new immutable cache state.
*
* @template F - current accumulator type
* @template V - Type extending object for the values to accumulate
* @param {V} value - The object containing values to accumulate
* @returns A new context instance with accumulated values
*/
accumulate(value) {
Object.defineProperty(this, "cache", {
value: this.cache.accumulate(value),
writable: false,
enumerable: false,
configurable: true,
});
return this;
}
get timestamp() {
return this.cache.timestamp;
}
/**
* @description Retrieves a value from the context by key.
* @summary Attempts to get a value from the current context's cache.
* If not found, traverses up the parent context chain.
*
* @template K - Type extending keyof F for the key to retrieve
* @template F - Accumulator type
* @param {K} key - The key to retrieve from the context
* @returns The value associated with the key
* @throws {Error} If the key is not found in the context chain
*/
get(key) {
try {
return this.cache.get(key);
}
catch (e) {
if (this.cache.parentContext)
return this.cache.parentContext.get(key);
throw e;
}
}
/**
* @description Creates a child context
* @summary Generates a new context instance with current context as parent
*
* @template M - Type extending Model
* @param {OperationKeys} operation - The operation type
* @param {Constructor<M>} [model] - Optional model constructor
* @returns {C} New child context instance
*/
child(operation, model) {
return Context.childFrom(this, {
operation: operation,
affectedTables: model ? [model] : [],
});
}
/**
* @description Creates a child context from another context
* @summary Generates a new context instance with parent reference
*
* @template F - Type extending Repository Flags
* @template C - Type extending Context<F>
* @param {C} context - The parent context
* @param {Partial<F>} [overrides] - Optional flag overrides
* @returns {C} New child context instance
*/
static childFrom(context, overrides) {
return Context.factory(Object.assign({}, context.cache, overrides || {}));
}
/**
* @description Creates a new context from operation parameters
* @summary Generates a context instance for specific operation
*
* @template F - Type extending Repository Flags
* @template M - Type extending Model
* @param {Opera