UNPKG

@decaf-ts/db-decorators

Version:

Agnostic database decorators and repository

455 lines (454 loc) 21 kB
import { DBOperations, OperationKeys } from "./constants.js"; import { Operations } from "./Operations.js"; import { apply } from "@decaf-ts/reflection"; import { propMetadata } from "@decaf-ts/decorator-validation"; import { InternalError } from "./../repository/errors.js"; import { getHandlerArgs } from "./../repository/utils.js"; import { metadata } from "@decaf-ts/decoration"; const defaultPriority = 50; const DefaultGroupSort = { priority: defaultPriority }; /** * @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 Retrieves decorator objects for handling database operations * @summary Retrieves a list of decorator objects representing operation handlers for a given model and decorators * @template M - Type for the model, defaults to Model<true | false> * @template R - Type for the repository, defaults to IRepository<M, F, C> * @template V - Type for metadata, defaults to object * @template F - Type for repository flags, defaults to RepositoryFlags * @template C - Type for context, defaults to Context<F> * @param {Model} model - The model for which to retrieve decorator objects * @param {Record<string, DecoratorMetadata[]>} decorators - The decorators associated with the model properties * @param {string} prefix - The operation prefix (e.g., 'on', 'after') * @return {DecoratorObject[]} An array of decorator objects representing operation handlers * @function getHandlersDecorators * @category Function */ export function getHandlersDecorators(model, decorators, prefix) { const accum = []; 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"); for (let i = 0; i < handlers.length; i++) { const data = handlerArgs[handlers[i].name] .data; accum.push({ handler: handlers[i], data: [data], prop: [prop], }); } } } return accum; } /** * @description Groups decorators based on their group property * @summary Groups decorator objects by their group property, combining data and properties within each group * @param {DecoratorObject[]} decorators - The array of decorator objects to group * @return {DecoratorObject[]} An array of grouped decorator objects * @function groupDecorators * @category Function */ export function groupDecorators(decorators) { const grouped = decorators.reduce((acc, dec) => { if (!dec || !dec.data || !dec.prop) throw new InternalError("Missing decorator properties or data"); // If decorator have no group if (!dec.data[0].group) { acc.set(Symbol(), dec); return acc; } const groupKey = dec.data[0].group; if (!acc.has(groupKey)) { // first handler is saved in the group acc.set(groupKey, { ...dec }); } else { const existing = acc.get(groupKey); acc.set(groupKey, { handler: existing.handler, data: [...existing.data, ...dec.data], prop: [...existing.prop, ...dec.prop], }); } return acc; }, new Map()); const groups = Array.from(grouped.values()); // Sort inside each group by priority groups.forEach((group) => { const combined = group.data.map((d, i) => ({ data: d, prop: group.prop[i], })); combined.sort((a, b) => (a.data.groupPriority ?? 50) - (b.data.groupPriority ?? 50)); group.data = combined.map((c) => c.data); group.prop = combined.map((c) => c.prop); }); return groups; } /** * @description Sorts decorator objects based on their priority * @summary Sorts an array of decorator objects by the priority of their first data element * @param {DecoratorObject[]} decorators - The array of decorator objects to sort * @return {DecoratorObject[]} The sorted array of decorator objects * @function sortDecorators * @category Function */ export function sortDecorators(decorators) { // Sort by groupPriority decorators.sort((a, b) => { const priorityA = a.data[0].priority ?? defaultPriority; const priorityB = b.data[0].priority ?? defaultPriority; return priorityA - priorityB; // lower number = higher priority }); return decorators; } /** * @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 {GeneralOperationHandler<any, any, V, any, any> | GeneralUpdateOperationHandler<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 */ export function onCreateUpdate(handler, data, groupsort) { return on(DBOperations.CREATE_UPDATE, handler, data, groupsort); } /** * @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 */ export function onUpdate(handler, data, groupsort) { return on(DBOperations.UPDATE, handler, data, groupsort); } /** * @description Decorator for handling create operations * @summary Defines a behavior to execute during create operations * @template V - Type for metadata, defaults to object * @param {GeneralOperationHandler<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 */ export function onCreate(handler, data, groupsort) { return on(DBOperations.CREATE, handler, data, groupsort); } /** * @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 */ export function onRead(handler, data, groupsort) { return on(DBOperations.READ, handler, data, groupsort); } /** * @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 */ export function onDelete(handler, data, groupsort) { return on(DBOperations.DELETE, handler, data, groupsort); } /** * @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 */ export function onAny(handler, data, groupsort) { return on(DBOperations.ALL, handler, data, groupsort); } /** * @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; * } */ export function on(op = DBOperations.ALL, handler, data, groupsort) { return operation(OperationKeys.ON, op, handler, data, groupsort); } /** * @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 */ export function afterCreateUpdate(handler, data, groupsort) { return after(DBOperations.CREATE_UPDATE, handler, data, groupsort); } /** * @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 */ export function afterUpdate(handler, data, groupsort) { return after(DBOperations.UPDATE, handler, data, groupsort); } /** * @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 */ export function afterCreate(handler, data, groupsort) { return after(DBOperations.CREATE, handler, data, groupsort); } /** * @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 */ export function afterRead(handler, data, groupsort) { return after(DBOperations.READ, handler, data, groupsort); } /** * @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 */ export function afterDelete(handler, data, groupsort) { return after(DBOperations.DELETE, handler, data, groupsort); } /** * @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 */ export function afterAny(handler, data, groupsort) { return after(DBOperations.ALL, handler, data, groupsort); } /** * @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; * } */ export function after(op = DBOperations.ALL, handler, data, groupsort) { return operation(OperationKeys.AFTER, op, handler, data, groupsort); } /** * @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 */ export function operation(baseOp, operation = DBOperations.ALL, handler, dataToAdd, groupsort = DefaultGroupSort) { 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); let mergeData = groupsort; if (dataToAdd) { if (Object.keys(dataToAdd).filter((key) => key in groupsort).length > 0) throw new InternalError(`Unable to merge groupSort into dataToAdd due to overlaping keys`); mergeData = { ...groupsort, ...dataToAdd }; } 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: mergeData, }; accum.push(handle(compoundKey, handler), propMetadata(Operations.key(compoundKey), data)); } return accum; }, []); return apply(...decorators)(target, propertyKey); }; } /** * @description * Creates a higher-order function that attaches a metadata entry containing a handler * and its execution parameters, to be conditionally evaluated later. * * @summary * The `executeIf` function is a decorator factory designed to wrap a handler function * and associate it with a specific metadata key. When invoked, it stores both the * parameters passed and the handler reference inside the metadata system for deferred * or conditional evaluation. This is particularly useful for dynamically applying logic * or decorators only when certain conditions are met. * * @template P - Represents a tuple of any parameter types that the handler function accepts. * * @param {string} key - The metadata key used to store and later retrieve the handler and its parameters. * @param {(...params: P) => boolean} handler - A predicate or handler function that receives the same parameters as the decorator * and determines whether the associated logic should execute. * * @return {(...params: P) => ReturnType<typeof metadata>} * Returns a function that, when invoked with the given parameters, stores a metadata object containing * both the parameters and the handler reference under the provided key. * * @function storeHandlerMetadata * * @mermaid * sequenceDiagram * participant Dev as Developer * participant executeIf as executeIf() * participant ReturnedFn as Returned Function * participant Metadata as metadata() * * Dev->>executeIf: Calls executeIf(key, handler) * executeIf->>ReturnedFn: Returns function(...params) * Dev->>ReturnedFn: Invokes returned function with (...params) * ReturnedFn->>Metadata: Calls metadata(key, { args: params, handler }) * Metadata-->>ReturnedFn: Returns stored metadata reference * ReturnedFn-->>Dev: Returns metadata response * */ export function storeHandlerMetadata(key, handler) { return (...params) => { return metadata(key, { args: params, handler }); }; } /** * @description * Decorator factory that conditionally blocks specific CRUD operations * from being executed on a model or controller. * * @summary * The `BlockOperations` decorator integrates with the `executeIf` mechanism to * associate metadata that defines which CRUD operations should be restricted. * When applied, it registers a conditional handler that evaluates whether a given * operation is included in the list of blocked operations. This enables dynamic, * metadata-driven control over allowed operations in CRUD-based systems. * * @template CrudOperations - Enum or type representing valid CRUD operations. * * @param {CrudOperations[]} operations - An array of CRUD operations that should be blocked. * The handler will later check if the requested operation is part of this list. * * Returns a decorator that stores metadata indicating which operations are blocked. * The metadata can be inspected or enforced later within the application's lifecycle. * * @function BlockOperations * @category decorators */ export const BlockOperations = (operations) => storeHandlerMetadata(OperationKeys.REFLECT + OperationKeys.BLOCK, (operations, operation) => { return operations.includes(operation); })(operations); //# sourceMappingURL=decorators.js.map