Source

operations/decorators.ts

import {
  GeneralOperationHandler,
  GeneralUpdateOperationHandler,
  IdOperationHandler,
  OperationHandler,
  StandardOperationHandler,
  UpdateOperationHandler,
} from "./types";
import { DBOperations, OperationKeys } from "./constants";
import { Operations } from "./Operations";
import { apply } from "@decaf-ts/reflection";
import { Model, propMetadata } from "@decaf-ts/decorator-validation";
import { IRepository } from "../interfaces";
import { RepositoryFlags } from "../repository/types";
import { Context } from "../repository/Context";
import { InternalError } from "../repository/errors";
import { getHandlerArgs } from "../repository/utils";

/**
 * @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, 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, 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, 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, any, any>
) {
  return (target: any, propertyKey: string) => {
    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<
  M extends Model<true | false>,
  R extends IRepository<M, F, C>,
  V extends object = object,
  F extends RepositoryFlags = RepositoryFlags,
  C extends Context<F> = Context<F>,
>(
  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, F, C>[] | undefined =
        Operations.get<M, R, V, F, C>(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 || 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] as Record<string, any>)
          .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: 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, any, any>
    | GeneralUpdateOperationHandler<any, any, V, any, any>,
  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, any>,
  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, 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<V = object>(
  handler: GeneralOperationHandler<any, any, V, any, any>,
  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, 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<V = object>(
  handler: IdOperationHandler<any, any, V, any, any>,
  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, any, any>,
  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, any, any>,
  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, any, any>,
  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, 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<V = object>(
  handler:
    | StandardOperationHandler<any, any, V, any, any>
    | UpdateOperationHandler<any, any, V, any, any>,
  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, 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<V = object>(
  handler: UpdateOperationHandler<any, any, V, any, any>,
  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, 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<V = object>(
  handler: StandardOperationHandler<any, any, V, any, any>,
  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, any, any>,
  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, any, any>,
  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, 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<V = object>(
  handler: StandardOperationHandler<any, any, V, any, any>,
  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, 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<V = object>(
  op: OperationKeys[] = DBOperations.ALL,
  handler: OperationHandler<any, any, V, any, any>,
  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, 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<V = object>(
  baseOp: OperationKeys.ON | OperationKeys.AFTER,
  operation: OperationKeys[] = DBOperations.ALL,
  handler: OperationHandler<any, any, V, any, any>,
  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 = 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 as OperationKeys, handler),
          propMetadata(Operations.key(compoundKey), data)
        );
      }
      return accum;
    }, []);
    return apply(...decorators)(target, propertyKey);
  };
}