Source

repository/utils.ts

import { Operations } from "../operations/Operations";
import { OperationHandler, UpdateOperationHandler } from "../operations/types";
import { IRepository } from "../interfaces/IRepository";
import { OperationKeys } from "../operations/constants";
import { DecoratorMetadata, Reflection } from "@decaf-ts/reflection";
import { InternalError } from "./errors";
import { Constructor, Model, ModelKeys } from "@decaf-ts/decorator-validation";
import { Context } from "./Context";
import { RepositoryFlags } from "./types";

/**
 * @description Context arguments for repository operations.
 * @summary Represents the context and arguments for repository operations.
 * This type is used to pass context and arguments between repository methods.
 * @template F - The repository flags type, defaults to RepositoryFlags
 * @template C - The context type, defaults to Context<F>
 * @typedef {Object} ContextArgs
 * @property {C} context - The operation context
 * @property {any[]} args - The operation arguments
 * @memberOf module:db-decorators
 */
export type ContextArgs<
  F extends RepositoryFlags = RepositoryFlags,
  C extends Context<F> = Context<F>,
> = {
  context: C;
  args: any[];
};

/**
 * @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);
};

/**
 *
 * @param {IRepository<T>} repo
 * @param context
 * @param {T} model
 * @param operation
 * @param prefix
 *
 * @param oldModel
 * @function enforceDBPropertyDecoratorsAsync
 *
 * @memberOf db-decorators.utils
 */
export async function enforceDBDecorators<
  M extends Model,
  R extends IRepository<M, F, C>,
  V extends object = object,
  F extends RepositoryFlags = RepositoryFlags,
  C extends Context<F> = Context<F>,
>(
  repo: R,
  context: C,
  model: M,
  operation: string,
  prefix: string,
  oldModel?: M
): Promise<void> {
  const decorators: Record<string, DecoratorMetadata[]> | undefined =
    getDbDecorators(model, operation, prefix);

  if (!decorators) return;

  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");

      let handler: OperationHandler<M, R, V, F, C>;
      let data: any;
      for (let i = 0; i < handlers.length; i++) {
        handler = handlers[i];
        data = Object.values(handlerArgs)[i];

        const args: any[] = [context, data.data, prop, model];

        if (operation === OperationKeys.UPDATE && prefix === OperationKeys.ON) {
          if (!oldModel)
            throw new InternalError("Missing old model for update operation");
          args.push(oldModel);
        }
        try {
          await (handler as UpdateOperationHandler<M, R, V, F, C>).apply(
            repo,
            args as [C, V, keyof M, M, M]
          );
        } catch (e: unknown) {
          const msg = `Failed to execute handler ${handler.name} for ${prop} on ${model.constructor.name} due to error: ${e}`;
          if (context.get("breakOnHandlerError")) throw new InternalError(msg);
          console.log(msg);
        }
      }
    }
  }
}

/**
 * Specific for DB Decorators
 * @param {T} model
 * @param {string} operation CRUD {@link OperationKeys}
 * @param {string} [extraPrefix]
 *
 * @function getDbPropertyDecorators
 *
 * @memberOf db-decorators.utils
 */
export function getDbDecorators<T extends Model>(
  model: T,
  operation: string,
  extraPrefix?: string
): Record<string, DecoratorMetadata[]> | undefined {
  const decorators: Record<string, DecoratorMetadata[]> | undefined =
    Reflection.getAllPropertyDecorators(
      model,
      // undefined,
      OperationKeys.REFLECT + (extraPrefix ? extraPrefix : "")
    );
  if (!decorators) return;
  return Object.keys(decorators).reduce(
    (accum: Record<string, DecoratorMetadata[]> | undefined, decorator) => {
      const dec = decorators[decorator].filter((d) => d.key === operation);
      if (dec && dec.length) {
        if (!accum) accum = {};
        accum[decorator] = dec;
      }
      return accum;
    },
    undefined
  );
}

/**
 * @summary Retrieves the decorators for an object's properties prefixed by {@param prefixes} recursively
 * @param model
 * @param accum
 * @param prefixes
 *
 * @function getAllPropertyDecoratorsRecursive
 * @memberOf module:db-decorators.Repository
 */
export const getAllPropertyDecoratorsRecursive = function <T extends Model>(
  model: T,
  accum: { [indexer: string]: any[] } | undefined,
  ...prefixes: string[]
): { [indexer: string]: any[] } | undefined {
  const accumulator = accum || {};
  const mergeDecorators = function (decs: { [indexer: string]: any[] }) {
    const pushOrSquash = (key: string, ...values: any[]) => {
      values.forEach((val) => {
        let match: any;
        if (
          !(match = accumulator[key].find((e) => e.key === val.key)) ||
          match.props.operation !== val.props.operation
        ) {
          accumulator[key].push(val);
          return;
        }

        if (val.key === ModelKeys.TYPE) return;

        const { handlers, operation } = val.props;

        if (
          !operation ||
          !operation.match(
            new RegExp(
              `^(:?${OperationKeys.ON}|${OperationKeys.AFTER})(:?${OperationKeys.CREATE}|${OperationKeys.READ}|${OperationKeys.UPDATE}|${OperationKeys.DELETE})$`
            )
          )
        ) {
          accumulator[key].push(val);
          return;
        }

        const accumHandlers = match.props.handlers;

        Object.entries(handlers).forEach(([clazz, handlerDef]) => {
          if (!(clazz in accumHandlers)) {
            accumHandlers[clazz] = handlerDef;
            return;
          }

          Object.entries(handlerDef as object).forEach(
            ([handlerProp, handler]) => {
              if (!(handlerProp in accumHandlers[clazz])) {
                accumHandlers[clazz][handlerProp] = handler;
                return;
              }

              Object.entries(handler as object).forEach(
                ([handlerKey, argsObj]) => {
                  if (!(handlerKey in accumHandlers[clazz][handlerProp])) {
                    accumHandlers[clazz][handlerProp][handlerKey] = argsObj;
                    return;
                  }
                  console.warn(
                    `Skipping handler registration for ${clazz} under prop ${handlerProp} because handler is the same`
                  );
                }
              );
            }
          );
        });
      });
    };

    Object.entries(decs).forEach(([key, value]) => {
      accumulator[key] = accumulator[key] || [];
      pushOrSquash(key, ...value);
    });
  };

  const decs: { [indexer: string]: any[] } | undefined =
    Reflection.getAllPropertyDecorators(model, ...prefixes);
  if (decs) mergeDecorators(decs);

  if (Object.getPrototypeOf(model) === Object.prototype) return accumulator;

  // const name = model.constructor.name;
  const proto = Object.getPrototypeOf(model);
  if (!proto) return accumulator;
  // if (proto.constructor && proto.constructor.name === name)
  //     proto = Object.getPrototypeOf(proto)
  return getAllPropertyDecoratorsRecursive(proto, accumulator, ...prefixes);
};