Source

repository/injectables.ts

import {
  InjectableRegistryImp,
  Injectables,
} from "@decaf-ts/injectable-decorators";
import { Repository } from "./Repository";
import {
  Constructor,
  Model,
  ModelConstructor,
} from "@decaf-ts/decorator-validation";
import { generateInjectableNameForRepository } from "./utils";
import { PersistenceKeys } from "../persistence/constants";
import { Adapter } from "../persistence/Adapter";
import { Logger, Logging } from "@decaf-ts/logging";

/**
 * @description Registry for injectable repositories with auto-resolution.
 * @summary Provides an InjectableRegistry implementation that resolves repositories by model name or constructor. If a repository
 * is not explicitly registered, it attempts to infer the correct repository using model metadata and the active or specified adapter flavour.
 * @param {void} [constructor] No constructor parameters required; the superclass handles internal state.
 * @class InjectablesRegistry
 * @example
 * // Basic usage: retrieve a repository by model name
 * const registry = new InjectablesRegistry();
 * const userRepo = registry.get<UserRepository>('User');
 * // If UserRepository is registered, it will be returned. Otherwise, a repository will be created if a User model exists.
 *
 * // Retrieve by constructor and specify adapter flavour
 * const repoByCtor = registry.get<UserRepository>(UserModel, 'ram');
 *
 * // Retrieve by symbol (e.g., injectable token)
 * const token = Symbol.for('UserRepository');
 * const byToken = registry.get<UserRepository>(token);
 * @mermaid
 * sequenceDiagram
 *   participant C as Consumer
 *   participant R as InjectablesRegistry
 *   participant B as BaseRegistry
 *   participant M as Model
 *   participant A as Adapter
 *   participant RP as Repository
 *   C->>R: get(name, flavour?)
 *   activate R
 *   R->>B: super.get(name)
 *   alt Found in base registry
 *     B-->>R: injectable
 *     R-->>C: injectable
 *   else Not found
 *     R->>M: Model.get(name)
 *     alt Model found
 *       R->>A: resolve flavour (from arg/metadata/current)
 *       R->>RP: Repository.forModel(modelCtor, alias)
 *       alt Repository instance
 *         RP-->>R: repository instance
 *         R-->>C: repository instance
 *       else Repository ctor
 *         R->>A: Adapter.get(resolvedFlavour) or Adapter.current
 *         A-->>R: adapter instance
 *         R->>RP: new repoCtor(adapter, modelCtor)
 *         R-->>C: repository instance
 *       end
 *     else Model not found
 *       R-->>C: undefined
 *     end
 *   end
 */
export class InjectablesRegistry extends InjectableRegistryImp {
  private logger?: Logger;

  protected get log(): Logger {
    if (!this.logger) this.logger = Logging.for(this as any);
    return this.logger;
  }

  constructor() {
    super();
  }

  /**
   * @description Retrieve an injectable with repository auto-resolution.
   * @summary Attempts to get an injectable from the base registry; if not found and the name refers to a known model, it
   * resolves the appropriate repository using the specified flavour or model metadata, falling back to the current adapter when needed.
   * @template T The injectable type to be returned.
   * @param {string | symbol | Constructor<T>} name Token, model name, or constructor associated with the injectable or model.
   * @param {string} [flavour] Optional adapter flavour (e.g., "ram"). If omitted, derives from metadata or current adapter.
   * @return {T | undefined} The located or auto-created injectable instance; otherwise undefined if it cannot be resolved.
   * @mermaid
   * sequenceDiagram
   *   participant G as get(name, flavour?)
   *   participant BR as BaseRegistry
   *   participant M as Model
   *   participant A as Adapter
   *   participant RP as Repository
   *   G->>BR: super.get(name)
   *   alt Found
   *     BR-->>G: injectable
   *   else Not found
   *     G->>M: derive modelCtor from name
   *     alt modelCtor resolved
   *       G->>A: resolve flavour (arg | metadata | current)
   *       G->>RP: Repository.forModel(modelCtor, alias)
   *       alt returns instance
   *         RP-->>G: Repository instance
   *       else returns ctor
   *         G->>A: Adapter.get(flavour) | Adapter.current
   *         A-->>G: adapter instance
   *         G->>RP: new repoCtor(adapter, modelCtor)
   *       end
   *     else no modelCtor
   *       G-->>G: return undefined
   *     end
   *   end
   */
  override get<T>(
    name: symbol | Constructor<T> | string,
    flavour?: string
  ): T | undefined {
    const log = this.log.for(this.get);
    // First, try base registry, but guard against thrown errors
    let injectable: T | undefined;
    try {
      injectable = super.get(name as any);
    } catch {
      // do nothing. we handle it later
    }

    if (!injectable) {
      let modelCtor: Constructor<any> | undefined;
      if (typeof name === "function") modelCtor = name as Constructor<any>;
      else if (typeof name === "symbol" || typeof name === "string") {
        modelCtor = Model.get(name.toString()) as ModelConstructor<any>;
      }

      if (!modelCtor) return undefined;

      // Resolve flavour from metadata if not provided
      const metaKey = Adapter.key(PersistenceKeys.ADAPTER);
      const resolvedFlavour =
        flavour ||
        (Reflect.getMetadata(metaKey, modelCtor) as string | undefined);

      try {
        // Determine an alias to use: prefer a directly registered adapter; otherwise, if the current adapter
        // has the same flavour, use its alias to satisfy Repository.forModel/Adapter.get lookups.
        let aliasToUse = resolvedFlavour;
        try {
          if (resolvedFlavour) Adapter.get(resolvedFlavour);
        } catch {
          const current = Adapter.current as any;
          if (current && current.flavour === resolvedFlavour)
            aliasToUse = current.alias;
        }

        injectable = Repository.forModel(
          modelCtor as Constructor<any>,
          aliasToUse
        ) as T;
        if (injectable instanceof Repository) return injectable as T;

        // Otherwise, register the resolved injectable name for later retrieval
        const f =
          resolvedFlavour ||
          (Reflect.getMetadata(metaKey, (injectable as any).constructor) as
            | string
            | undefined) ||
          (Reflect.getMetadata(metaKey, modelCtor) as string | undefined);
        Injectables.register(
          injectable,
          generateInjectableNameForRepository(
            modelCtor as ModelConstructor<any>,
            f as string
          )
        );
      } catch (e: unknown) {
        log.debug(
          `No registered repository or adapter found. falling back to default adapter. Error: ${(e as Error)?.message || JSON.stringify(e)}`
        );
        const repoCtor = (Repository as any)["get"](modelCtor, resolvedFlavour);
        if (typeof repoCtor === "function") {
          const adapter = resolvedFlavour
            ? (Adapter.get(resolvedFlavour) as any)
            : (Adapter.current as any);
          if (!adapter) return undefined;
          const instance = new repoCtor(adapter, modelCtor);
          return instance as T;
        }
      }
    }

    return injectable as T | undefined;
  }
}