Source

repository/Context.ts

import { ContextArgs } from "./utils";
import { Contextual } from "../interfaces/Contextual";
import { OperationKeys } from "../operations/constants";
import { Constructor, Model } from "@decaf-ts/decorator-validation";
import { DefaultRepositoryFlags } from "./constants";
import { ObjectAccumulator } from "typed-object-accumulator";
import { RepositoryFlags } from "./types";

/**
 * @description Factory type for creating context instances.
 * @summary Defines a function type that creates context instances with specific repository flags.
 * @template F - The repository flags type extending RepositoryFlags
 * @typedef {Function} ContextFactory
 * @memberOf module:db-decorators
 */
export type ContextFactory<F extends RepositoryFlags> = <C extends Context<F>>(
  arg: Omit<F, "timestamp">
) => C;

/**
 * @description Default factory for creating context instances.
 * @summary A factory function that creates new Context instances with the provided repository flags.
 * It automatically adds a timestamp to the context and returns a properly typed context instance.
 * @const DefaultContextFactory
 * @memberOf module:db-decorators
 */
export const DefaultContextFactory: ContextFactory<any> = <
  F extends RepositoryFlags,
  C extends Context<F>,
>(
  arg: Omit<F, "timestamp">
) => {
  return new Context<F>().accumulate(
    Object.assign({}, arg, { timestamp: new Date() }) as F
  ) as C;
};

/**
 * @description A context management class for handling repository operations.
 * @summary The Context class provides a mechanism for managing repository operations with flags,
 * parent-child relationships, and state accumulation. It allows for hierarchical context chains
 * and maintains operation-specific configurations while supporting type safety through generics.
 *
 * @template F - Type extending RepositoryFlags that defines the context configuration
 *
 * @param {ObjectAccumulator<F>} cache - The internal cache storing accumulated values
 *
 * @class
 *
 * @example
 * ```typescript
 * // Creating a new context with repository flags
 * const context = new Context<RepositoryFlags>();
 *
 * // Accumulating values
 * const enrichedContext = context.accumulate({
 *   writeOperation: true,
 *   affectedTables: ['users'],
 *   operation: OperationKeys.CREATE
 * });
 *
 * // Accessing values
 * const isWrite = enrichedContext.get('writeOperation'); // true
 * const tables = enrichedContext.get('affectedTables'); // ['users']
 * ```
 *
 * @mermaid
 * sequenceDiagram
 *   participant C as Client
 *   participant Ctx as Context
 *   participant Cache as ObjectAccumulator
 *
 *   C->>Ctx: new Context()
 *   Ctx->>Cache: create cache
 *
 *   C->>Ctx: accumulate(value)
 *   Ctx->>Cache: accumulate(value)
 *   Cache-->>Ctx: updated cache
 *   Ctx-->>C: updated context
 *
 *   C->>Ctx: get(key)
 *   Ctx->>Cache: get(key)
 *   alt Key exists in cache
 *     Cache-->>Ctx: value
 *   else Key not found
 *     Ctx->>Ctx: check parent context
 *     alt Parent exists
 *       Ctx->>Parent: get(key)
 *       Parent-->>Ctx: value
 *     else No parent
 *       Ctx-->>C: throw error
 *     end
 *   end
 *   Ctx-->>C: requested value
 */
export class Context<F extends RepositoryFlags> {
  constructor() {
    Object.defineProperty(this, "cache", {
      value: new ObjectAccumulator<F>(),
      writable: false,
      enumerable: false,
      configurable: true,
    });
  }

  static factory: ContextFactory<any> = DefaultContextFactory;

  private readonly cache: F & ObjectAccumulator<F> =
    new ObjectAccumulator() as F & ObjectAccumulator<F>;

  /**
   * @description Accumulates new values into the context.
   * @summary Merges the provided value object with the existing context state,
   * creating a new immutable cache state.
   *
   * @template F - current accumulator type
   * @template V - Type extending object for the values to accumulate
   * @param {V} value - The object containing values to accumulate
   * @returns A new context instance with accumulated values
   */
  accumulate<V extends object>(value: V) {
    Object.defineProperty(this, "cache", {
      value: this.cache.accumulate(value),
      writable: false,
      enumerable: false,
      configurable: true,
    });
    return this as unknown as Context<F & V>;
  }

  get timestamp() {
    return this.cache.timestamp;
  }

  /**
   * @description Retrieves a value from the context by key.
   * @summary Attempts to get a value from the current context's cache.
   * If not found, traverses up the parent context chain.
   *
   * @template K - Type extending keyof F for the key to retrieve
   * @template F - Accumulator type
   * @param {K} key - The key to retrieve from the context
   * @returns The value associated with the key
   * @throws {Error} If the key is not found in the context chain
   */
  get<K extends keyof F>(key: K): F[K] {
    try {
      return this.cache.get(key);
    } catch (e: unknown) {
      if (this.cache.parentContext) return this.cache.parentContext.get(key);
      throw e;
    }
  }

  /**
   * @description Creates a child context
   * @summary Generates a new context instance with current context as parent
   *
   * @template M - Type extending Model
   * @param {OperationKeys} operation - The operation type
   * @param {Constructor<M>} [model] - Optional model constructor
   * @returns {C} New child context instance
   */
  child<M extends Model, C extends Context<F>>(
    operation: OperationKeys,
    model?: Constructor<M>
  ): C {
    return Context.childFrom<F, C>(
      this as unknown as C,
      {
        operation: operation,
        affectedTables: model ? [model] : [],
      } as unknown as Partial<F>
    );
  }

  /**
   * @description Creates a child context from another context
   * @summary Generates a new context instance with parent reference
   *
   * @template F - Type extending Repository Flags
   * @template C - Type extending Context<F>
   * @param {C} context - The parent context
   * @param {Partial<F>} [overrides] - Optional flag overrides
   * @returns {C} New child context instance
   */
  static childFrom<F extends RepositoryFlags, C extends Context<F>>(
    context: C,
    overrides?: Partial<F>
  ): C {
    return Context.factory(
      Object.assign({}, context.cache, overrides || {})
    ) as unknown as C;
  }

  /**
   * @description Creates a new context from operation parameters
   * @summary Generates a context instance for specific operation
   *
   * @template F - Type extending Repository Flags
   * @template M - Type extending Model
   * @param {OperationKeys.DELETE} operation - The operation type
   * @param {Partial<F>} overrides - Flag overrides
   * @param {Constructor<M>} model - The model constructor
   * @param {any} args - Operation arguments
   * @returns {Promise<C>} Promise resolving to new context
   */
  static async from<
    M extends Model,
    F extends RepositoryFlags,
    C extends Context<F>,
  >(
    operation:
      | OperationKeys.CREATE
      | OperationKeys.READ
      | OperationKeys.UPDATE
      | OperationKeys.DELETE,
    overrides: Partial<F>,
    model: Constructor<M>,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ...args: any[]
  ): Promise<C> {
    return Context.factory(
      Object.assign({}, DefaultRepositoryFlags, overrides, {
        operation: operation,
        model: model,
      })
    ) as C;
  }

  /**
   * @description Prepares arguments for context operations
   * @summary Creates a context args object with the specified operation parameters
   *
   * @template F - Type extending {@link RepositoryFlags}
   * @template M - Type extending {@link Model}
   * @param {OperationKeys.DELETE} operation - The operation type
   * @param {Constructor<M>} model - The model constructor
   * @param {any[]} args - Operation arguments
   * @param {Contextual<F>} [contextual] - Optional contextual object
   * @param {Partial<F>} [overrides] - Optional flag overrides
   * @returns {Promise<ContextArgs>} Promise resolving to context arguments
   *
   * @mermaid
   * sequenceDiagram
   *   participant C as Context
   *   participant M as Model
   *   participant A as Args
   *
   *   C->>C: Receive operation request
   *   C->>M: Validate model constructor
   *   C->>C: Create child context
   *   C->>A: Process operation args
   *   A->>C: Return context args
   *   C->>C: Apply overrides
   *   C->>C: Return final context
   */
  static async args<
    M extends Model,
    C extends Context<F>,
    F extends RepositoryFlags,
  >(
    operation:
      | OperationKeys.CREATE
      | OperationKeys.READ
      | OperationKeys.UPDATE
      | OperationKeys.DELETE,
    model: Constructor<M>,
    args: any[],
    contextual?: Contextual<F>,
    overrides?: Partial<F>
  ): Promise<ContextArgs<F, C>> {
    const last = args.pop();

    async function getContext() {
      if (contextual)
        return contextual.context(operation, overrides || {}, model, ...args);
      return Context.from(operation, overrides || {}, model, ...args);
    }

    let c: C;
    if (last) {
      if (last instanceof Context) {
        c = last as C;
        args.push(last);
      } else {
        c = (await getContext()) as C;
        args.push(last, c);
      }
    } else {
      c = (await getContext()) as C;
      args.push(c);
    }

    return { context: c, args: args };
  }
}