Source

operations/decorators.ts

import {
  GeneralOperationHandler,
  GeneralUpdateOperationHandler,
  IdOperationHandler,
  OperationHandler,
  StandardOperationHandler,
  UpdateOperationHandler,
} from "./types";
import {
  CrudOperations,
  DBOperations,
  ModelOperations,
  OperationKeys,
} from "./constants";
import { Operations } from "./Operations";
import { Model } from "@decaf-ts/decorator-validation";
import { IRepository } from "../interfaces";
import { InternalError } from "../repository/errors";
import {
  propMetadata,
  apply,
  metadata,
  Metadata,
  Constructor,
} from "@decaf-ts/decoration";

/**
 * @description Represents sorting parameters for grouping decorators
 * @summary Defines the structure for specifying group sorting options
 * @typedef {Object} GroupSort
 * @property {number} priority - The priority of the sorting operation, lower numbers represent higher priority
 * @property {string} [group] - Optional property to group decorators, used for grouping related operations
 * @property {number} [groupPriority] - Optional property to specify the priority within a group, lower numbers represent higher priority within the group
 * @category Type Definitions
 */
export type GroupSort = {
  priority: number;
  group?: string;
  groupPriority?: number;
};

const defaultPriority = 50;

const DefaultGroupSort: GroupSort = { priority: defaultPriority };

/**
 * @description DecoratorObject type definition
 * @summary Defines the structure of an object used to represent a decorator in the context of database operations.
 * @typedef {Object} DecoratorObject
 * @property {OperationHandler<any, any, any>} handler - The handler function to be executed during the operation
 * @property {object} data - Optional metadata to be passed to the handler function
 * @property {string} prop - The property key to which the decorator is applied
 * @category Type Definitions
 */
export type DecoratorObject = {
  handler: OperationHandler<any, any, any>;
  data: Record<string, any>[];
  prop: string[];
};

/**
 * @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>} handler - The handler function to register
 * @return {PropertyDecorator} A decorator that registers the handler
 * @function handle
 * @category Property Decorators
 */
function handle(op: OperationKeys, handler: OperationHandler<any, any, any>) {
  return (target: any, propertyKey: string) => {
    Operations.register(handler, op, target, propertyKey);
  };
}

/**
 * @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
 */
export const getHandlerArgs = function (
  dec: any,
  prop: string,
  m: Constructor<any>,
  accum?: Record<string, { args: string[] }>
): Record<string, { args: string[] }> | void {
  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);
};

/**
 * @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<
  M extends Model<true | false>,
  R extends IRepository<M, any>,
  V extends object = object,
>(
  model: Model,
  decorators: Record<string, DecoratorMetadata[]>,
  prefix: string
): DecoratorObject[] {
  const accum: DecoratorObject[] = [];
  for (const prop in decorators) {
    const decs: DecoratorMetadata[] = decorators[prop];
    for (const dec of decs) {
      const { key } = dec!;
      const handlers: OperationHandler<M, R, V>[] | undefined = Operations.get<
        M,
        R,
        V
      >(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 as any);
      if (!handlerArgs)
        throw new InternalError("Missing handler arguments for decorators");

      for (const handler of handlers) {
        const argsEntry = handlerArgs[handler.name];
        if (!argsEntry)
          throw new InternalError(
            `Missing handler arguments for handler ${handler.name}`
          );
        const data = (argsEntry as Record<string, any>).data;
        accum.push({
          handler,
          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: DecoratorObject[]
): DecoratorObject[] {
  const grouped = decorators.reduce<Map<string | symbol, DecoratorObject>>(
    (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: DecoratorObject[]
): DecoratorObject[] {
  // 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<V = object>(
  handler:
    | GeneralOperationHandler<any, any, V>
    | GeneralUpdateOperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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<V = object>(
  handler: UpdateOperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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>} 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<V = object>(
  handler: GeneralOperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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>} 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<V = object>(
  handler: IdOperationHandler<any, any, V>,
  data: V,
  groupsort?: 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<V = object>(
  handler: OperationHandler<any, any, V>,
  data: V,
  groupsort?: 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<V = object>(
  handler: OperationHandler<any, any, V>,
  data: V,
  groupsort?: 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<V = object>(
  op: OperationKeys[] = DBOperations.ALL,
  handler: OperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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> | 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<V = object>(
  handler:
    | StandardOperationHandler<any, any, V>
    | UpdateOperationHandler<any, any, V>,
  data: V,
  groupsort?: 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>} 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<V = object>(
  handler: UpdateOperationHandler<any, any, V>,
  data: V,
  groupsort?: 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>} 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<V = object>(
  handler: StandardOperationHandler<any, any, V>,
  data: V,
  groupsort?: 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<V = object>(
  handler: StandardOperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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<V = object>(
  handler: StandardOperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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>} 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<V = object>(
  handler: StandardOperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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, Vy>} 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<V = object>(
  op: OperationKeys[] = DBOperations.ALL,
  handler: OperationHandler<any, any, V>,
  data?: V,
  groupsort?: 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>} 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<V = object>(
  baseOp: OperationKeys.ON | OperationKeys.AFTER,
  operation: OperationKeys[] = DBOperations.ALL,
  handler: OperationHandler<any, any, V>,
  dataToAdd?: V,
  groupsort: GroupSort = DefaultGroupSort
) {
  return (target: any, propertyKey?: any) => {
    const name = target.constructor.name;
    const decorators = operation.reduce((accum: any[], op) => {
      const compoundKey = baseOp + op;
      let data = Metadata.readOperation(
        target.constructor,
        propertyKey as any,
        compoundKey
      );
      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 as OperationKeys, handler),
          propMetadata(
            Metadata.key(ModelOperations.OPERATIONS, propertyKey, 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 {function(...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 {function(...P):function(...Partial<P>):void}
 * 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<P extends any[]>(
  key: string,
  handler: (...params: P) => boolean
) {
  return (...params: Partial<P>) => {
    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: CrudOperations[]) =>
  storeHandlerMetadata<[CrudOperations[], CrudOperations]>(
    OperationKeys.REFLECT + OperationKeys.BLOCK,
    (operations: CrudOperations[], operation: CrudOperations) => {
      return operations.includes(operation);
    }
  )(operations);