Source

repository/Repository.ts

import { enforceDBDecorators } from "./utils";
import { OperationKeys } from "../operations/constants";
import { InternalError, ValidationError } from "./errors";
import { BaseRepository } from "./BaseRepository";
import { Constructor, Model } from "@decaf-ts/decorator-validation";
import { DBKeys } from "../model/constants";
import { Context } from "./Context";
import { RepositoryFlags } from "./types";

/**
 * @description Concrete repository implementation with validation support.
 * @summary The Repository class extends BaseRepository to provide additional validation
 * functionality. It overrides prefix methods to perform model validation before database
 * operations and throws ValidationError when validation fails.
 * @template M - The model type extending Model
 * @template F - The repository flags type, defaults to RepositoryFlags
 * @template C - The context type, defaults to Context<F>
 * @class Repository
 * @example
 * class UserModel extends Model {
 *   @id()
 *   id: string;
 *
 *   @required()
 *   @minLength(3)
 *   name: string;
 * }
 *
 * class UserRepository extends Repository<UserModel> {
 *   constructor() {
 *     super(UserModel);
 *   }
 *
 *   async create(model: UserModel): Promise<UserModel> {
 *     // Implementation with automatic validation
 *     return model;
 *   }
 * }
 *
 * // Using the repository
 * const repo = new UserRepository();
 * try {
 *   const user = await repo.create({ name: 'Jo' }); // Will throw ValidationError
 * } catch (error) {
 *   console.error(error); // ValidationError: name must be at least 3 characters
 * }
 */
export abstract class Repository<
  M extends Model,
  F extends RepositoryFlags = RepositoryFlags,
  C extends Context<F> = Context<F>,
> extends BaseRepository<M, F, C> {
  protected constructor(clazz?: Constructor<M>) {
    super(clazz);
  }

  /**
   * @description Prepares a model for creation with validation.
   * @summary Overrides the base createPrefix method to add validation checks.
   * Creates a context, instantiates a new model, enforces decorators, and validates
   * the model before allowing creation to proceed.
   * @param {M} model - The model instance to prepare for creation
   * @param {any[]} args - Additional arguments for the create operation
   * @return A promise that resolves to an array containing the validated model and context arguments
   * @throws {ValidationError} If the model fails validation
   */
  protected override async createPrefix(
    model: M,
    ...args: any[]
  ): Promise<[M, ...any[]]> {
    const contextArgs = await Context.args(
      OperationKeys.CREATE,
      this.class,
      args
    );
    model = new this.class(model);
    await enforceDBDecorators(
      this,
      contextArgs.context,
      model,
      OperationKeys.CREATE,
      OperationKeys.ON
    );

    const errors = model.hasErrors();
    if (errors) throw new ValidationError(errors.toString());

    return [model, ...contextArgs.args];
  }

  /**
   * @description Prepares multiple models for creation with validation.
   * @summary Overrides the base createAllPrefix method to add validation checks for multiple models.
   * Creates a context, instantiates new models, enforces decorators, and validates
   * each model before allowing creation to proceed. Collects validation errors from all models.
   * @param {M[]} models - The array of model instances to prepare for creation
   * @param {any[]} args - Additional arguments for the create operation
   * @return {Promise<any[]>} A promise that resolves to an array containing the validated models and context arguments
   * @throws {ValidationError} If any model fails validation, with details about which models failed
   */
  protected override async createAllPrefix(
    models: M[],
    ...args: any[]
  ): Promise<any[]> {
    const contextArgs = await Context.args(
      OperationKeys.CREATE,
      this.class,
      args
    );
    await Promise.all(
      models.map(async (m) => {
        m = new this.class(m);
        await enforceDBDecorators(
          this,
          contextArgs.context,
          m,
          OperationKeys.CREATE,
          OperationKeys.ON
        );
        return m;
      })
    );
    const errors = models
      .map((m) => m.hasErrors())
      .reduce((accum: string | undefined, e, i) => {
        if (e)
          accum =
            typeof accum === "string"
              ? accum + `\n - ${i}: ${e.toString()}`
              : ` - ${i}: ${e.toString()}`;
        return accum;
      }, undefined);
    if (errors) throw new ValidationError(errors);
    return [models, ...contextArgs.args];
  }

  /**
   * @description Prepares a model for update with validation.
   * @summary Overrides the base updatePrefix method to add validation checks.
   * Creates a context, validates the primary key, retrieves the existing model,
   * merges the old and new models, enforces decorators, and validates the model
   * before allowing the update to proceed.
   * @param {M} model - The model instance to prepare for update
   * @param {any[]} args - Additional arguments for the update operation
   * @return A promise that resolves to an array containing the validated model and context arguments
   * @throws {InternalError} If the model doesn't have a primary key value
   * @throws {ValidationError} If the model fails validation
   */
  protected override async updatePrefix(
    model: M,
    ...args: any[]
  ): Promise<[M, ...args: any[]]> {
    const contextArgs = await Context.args(
      OperationKeys.UPDATE,
      this.class,
      args
    );
    const pk = (model as any)[this.pk];
    if (!pk)
      throw new InternalError(
        `No value for the Id is defined under the property ${this.pk as string}`
      );

    const oldModel: M = await this.read(pk);

    model = this.merge(oldModel, model);

    await enforceDBDecorators(
      this,
      contextArgs.context,
      model,
      OperationKeys.UPDATE,
      OperationKeys.ON,
      oldModel
    );

    const errors = model.hasErrors(oldModel as any);
    if (errors) throw new ValidationError(errors.toString());
    return [model, ...contextArgs.args];
  }

  /**
   * @description Prepares multiple models for update with validation.
   * @summary Overrides the base updateAllPrefix method to add validation checks for multiple models.
   * Creates a context, validates primary keys, retrieves existing models, merges old and new models,
   * enforces decorators, and validates each model before allowing updates to proceed.
   * Collects validation errors from all models.
   * @param {M[]} models - The array of model instances to prepare for update
   * @param {any[]} args - Additional arguments for the update operation
   * @return A promise that resolves to an array containing the validated models and context arguments
   * @throws {InternalError} If any model doesn't have a primary key value
   * @throws {ValidationError} If any model fails validation, with details about which models failed
   */
  protected override async updateAllPrefix(models: M[], ...args: any[]) {
    const contextArgs = await Context.args(
      OperationKeys.UPDATE,
      this.class,
      args
    );
    const ids = models.map((m) => {
      const id = m[this.pk];
      if (typeof id === "undefined")
        throw new InternalError(
          `No value for the Id is defined under the property ${this.pk as string}`
        );
      return id as string;
    });
    const oldModels: M[] = await this.readAll(ids, ...contextArgs.args);
    models = models.map((m, i) => this.merge(oldModels[i], m));
    await Promise.all(
      models.map((m, i) =>
        enforceDBDecorators(
          this,
          contextArgs.context,
          m,
          OperationKeys.UPDATE,
          OperationKeys.ON,
          oldModels[i]
        )
      )
    );

    const errors = models
      .map((m, i) => m.hasErrors(oldModels[i] as any))
      .reduce((accum: string | undefined, e, i) => {
        if (e)
          accum =
            typeof accum === "string"
              ? accum + `\n - ${i}: ${e.toString()}`
              : ` - ${i}: ${e.toString()}`;
        return accum;
      }, undefined);
    if (errors) throw new ValidationError(errors);
    return [models, ...contextArgs.args];
  }

  /**
   * @description Creates a reflection key for database operations.
   * @summary Generates a key for storing metadata in the reflection system by prefixing
   * the provided key with the database reflection prefix.
   * @param {string} key - The base key to prefix
   * @return {string} The prefixed reflection key
   */
  static key(key: string) {
    return DBKeys.REFLECT + key;
  }
}