@decaf-ts/db-decorators
Version:
Agnostic database decorators and repository
478 lines (477 loc) • 22.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BlockOperations = void 0;
exports.getHandlersDecorators = getHandlersDecorators;
exports.groupDecorators = groupDecorators;
exports.sortDecorators = sortDecorators;
exports.onCreateUpdate = onCreateUpdate;
exports.onUpdate = onUpdate;
exports.onCreate = onCreate;
exports.onRead = onRead;
exports.onDelete = onDelete;
exports.onAny = onAny;
exports.on = on;
exports.afterCreateUpdate = afterCreateUpdate;
exports.afterUpdate = afterUpdate;
exports.afterCreate = afterCreate;
exports.afterRead = afterRead;
exports.afterDelete = afterDelete;
exports.afterAny = afterAny;
exports.after = after;
exports.operation = operation;
exports.storeHandlerMetadata = storeHandlerMetadata;
const constants_1 = require("./constants.cjs");
const Operations_1 = require("./Operations.cjs");
const reflection_1 = require("@decaf-ts/reflection");
const decorator_validation_1 = require("@decaf-ts/decorator-validation");
const errors_1 = require("./../repository/errors.cjs");
const utils_1 = require("./../repository/utils.cjs");
const decoration_1 = require("@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_1.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
*/
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_1.Operations.get(model, prop, prefix + key);
if (!handlers || !handlers.length)
throw new errors_1.InternalError(`Could not find registered handler for the operation ${prefix + key} under property ${prop}`);
const handlerArgs = (0, utils_1.getHandlerArgs)(dec, prop, model);
if (!handlerArgs || Object.values(handlerArgs).length !== handlers.length)
throw new errors_1.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
*/
function groupDecorators(decorators) {
const grouped = decorators.reduce((acc, dec) => {
if (!dec || !dec.data || !dec.prop)
throw new errors_1.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
*/
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
*/
function onCreateUpdate(handler, data, groupsort) {
return on(constants_1.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
*/
function onUpdate(handler, data, groupsort) {
return on(constants_1.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
*/
function onCreate(handler, data, groupsort) {
return on(constants_1.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
*/
function onRead(handler, data, groupsort) {
return on(constants_1.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
*/
function onDelete(handler, data, groupsort) {
return on(constants_1.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
*/
function onAny(handler, data, groupsort) {
return on(constants_1.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;
* }
*/
function on(op = constants_1.DBOperations.ALL, handler, data, groupsort) {
return operation(constants_1.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
*/
function afterCreateUpdate(handler, data, groupsort) {
return after(constants_1.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
*/
function afterUpdate(handler, data, groupsort) {
return after(constants_1.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
*/
function afterCreate(handler, data, groupsort) {
return after(constants_1.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
*/
function afterRead(handler, data, groupsort) {
return after(constants_1.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
*/
function afterDelete(handler, data, groupsort) {
return after(constants_1.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
*/
function afterAny(handler, data, groupsort) {
return after(constants_1.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;
* }
*/
function after(op = constants_1.DBOperations.ALL, handler, data, groupsort) {
return operation(constants_1.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
*/
function operation(baseOp, operation = constants_1.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_1.Operations.key(compoundKey), target, propertyKey);
if (!data)
data = {
operation: op,
handlers: {},
};
const handlerKey = Operations_1.Operations.getHandlerName(handler);
let mergeData = groupsort;
if (dataToAdd) {
if (Object.keys(dataToAdd).filter((key) => key in groupsort).length > 0)
throw new errors_1.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), (0, decorator_validation_1.propMetadata)(Operations_1.Operations.key(compoundKey), data));
}
return accum;
}, []);
return (0, reflection_1.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
*
*/
function storeHandlerMetadata(key, handler) {
return (...params) => {
return (0, decoration_1.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
*/
const BlockOperations = (operations) => storeHandlerMetadata(constants_1.OperationKeys.REFLECT + constants_1.OperationKeys.BLOCK, (operations, operation) => {
return operations.includes(operation);
})(operations);
exports.BlockOperations = BlockOperations;
//# sourceMappingURL=decorators.js.map