Source

operations/OperationsRegistry.ts

import { OperationHandler } from "./types";
import { OperationKeys } from "./constants";
import { IRepository } from "../interfaces/IRepository";
import { Operations } from "./Operations";
import { Model } from "@decaf-ts/decorator-validation";
import { Constructor, Decoration, Metadata } from "@decaf-ts/decoration";

/**
 * @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)
 *   }
 */
export class OperationsRegistry {
  private readonly cache: Record<
    string,
    Record<
      string | symbol,
      Record<
        string,
        Record<string, Record<string, OperationHandler<any, any, any>>>
      >
    >
  > = {};

  /**
   * @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
   * @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<M extends Model, R extends IRepository<M, any>, V>(
    target: string | Record<string, any>,
    propKey: string,
    operation: string,
    accum?: OperationHandler<M, R, V>[]
  ): OperationHandler<M, R, V>[] | undefined {
    accum = accum || [];
    const owner = this.resolveOwner(target);
    const name = this.resolveTargetName(target, owner);

    if (name) {
      const handlers = this.selectHandlers<M, R, V>(
        name,
        propKey,
        operation,
        owner
      );
      if (handlers?.length) {
        accum.unshift(...handlers);
      }
    } else if (
      typeof target === "string" ||
      target === Object.prototype ||
      Object.getPrototypeOf(target) === Object.prototype
    ) {
      return accum;
    }

    let proto = Object.getPrototypeOf(target);
    if (!proto) return accum;
    if (proto.constructor && proto.constructor.name === name)
      proto = Object.getPrototypeOf(proto);
    if (!proto) return accum;

    return this.get<M, R, V>(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<M extends Model, R extends IRepository<M, any>, V>(
    handler: OperationHandler<M, R, V>,
    operation: OperationKeys,
    target: M,
    propKey: string | symbol
  ): void {
    const name = target.constructor.name;
    const handlerName = Operations.getHandlerName(handler);
    const flavour = this.resolveFlavour(target.constructor as Constructor);

    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][flavour])
      this.cache[name][propKey][operation][flavour] = {};
    if (this.cache[name][propKey][operation][flavour][handlerName]) return;
    this.cache[name][propKey][operation][flavour][handlerName] = handler;
  }

  private resolveOwner(
    target: string | Record<string, any>
  ): Constructor | undefined {
    if (!target || typeof target === "string") return undefined;
    if (typeof target === "function") return target as Constructor;
    return target.constructor as Constructor;
  }

  private resolveTargetName(
    target: string | Record<string, any>,
    owner?: Constructor
  ): string | undefined {
    if (typeof target === "string") return target;
    return owner?.name;
  }

  private resolveFlavour(target?: Constructor): string {
    if (!target) return Decoration.defaultFlavour;
    try {
      return Metadata.flavourOf(target);
    } catch {
      return Decoration.defaultFlavour;
    }
  }

  private selectHandlers<M extends Model, R extends IRepository<M, any>, V>(
    name: string,
    propKey: string,
    operation: string,
    owner?: Constructor
  ): OperationHandler<M, R, V>[] | undefined {
    const byOperation = this.cache[name]?.[propKey]?.[operation];
    if (!byOperation) return undefined;
    const flavour = this.resolveFlavour(owner);
    const bucket =
      byOperation[flavour] ||
      byOperation[Decoration.defaultFlavour] ||
      this.firstBucket(byOperation);
    if (!bucket) return undefined;
    const handlers = Object.values(bucket);
    return handlers.length ? (handlers as OperationHandler<M, R, V>[]) : undefined;
  }

  private firstBucket(
    byOperation: Record<string, Record<string, OperationHandler<any, any, any>>>
  ): Record<string, OperationHandler<any, any, any>> | undefined {
    for (const handlers of Object.values(byOperation)) {
      if (handlers && Object.keys(handlers).length) return handlers;
    }
    return undefined;
  }
}