Source

query/Statement.ts

import { type Constructor, Model } from "@decaf-ts/decorator-validation";
import type { Executor, RawExecutor } from "../interfaces";
import type {
  FromSelector,
  GroupBySelector,
  OrderBySelector,
  SelectSelector,
} from "./selectors";
import { Condition } from "./Condition";
import { findPrimaryKey, InternalError } from "@decaf-ts/db-decorators";
import { final } from "../utils/decorators";
import type {
  CountOption,
  DistinctOption,
  LimitOption,
  MaxOption,
  MinOption,
  OffsetOption,
  OrderAndGroupOption,
  SelectOption,
  WhereOption,
} from "./options";
import { Paginatable } from "../interfaces/Paginatable";
import { Paginator } from "./Paginator";
import { Adapter } from "../persistence";
import { QueryError } from "./errors";

/**
 * @description Base class for database query statements
 * @summary Provides a foundation for building and executing database queries
 *
 * This abstract class implements the query builder pattern for constructing
 * database queries. It supports various query operations like select, from,
 * where, orderBy, groupBy, limit, and offset. It also provides methods for
 * executing queries and handling pagination.
 *
 * @template Q - The query type specific to the database adapter
 * @template M - The model type this statement operates on
 * @template R - The return type of the query
 * @param {Adapter<any, Q, any, any>} adapter - The database adapter to use for executing queries
 * @class Statement
 * @example
 * // Create a statement to query users
 * const statement = new SQLStatement(adapter);
 * const users = await statement
 *   .select()
 *   .from(User)
 *   .where(Condition.attribute("status").eq("active"))
 *   .orderBy(["createdAt", "DESC"])
 *   .limit(10)
 *   .execute();
 *
 * // Use pagination
 * const paginator = await statement
 *   .select()
 *   .from(User)
 *   .paginate(20); // 20 users per page
 *
 * @mermaid
 * sequenceDiagram
 *   participant Client
 *   participant Statement
 *   participant Adapter
 *   participant Database
 *
 *   Client->>Statement: select()
 *   Client->>Statement: from(Model)
 *   Client->>Statement: where(condition)
 *   Client->>Statement: orderBy([field, direction])
 *   Client->>Statement: limit(value)
 *   Client->>Statement: execute()
 *   Statement->>Statement: build()
 *   Statement->>Adapter: raw(query)
 *   Adapter->>Database: execute query
 *   Database-->>Adapter: return results
 *   Adapter-->>Statement: return processed results
 *   Statement-->>Client: return final results
 */
export abstract class Statement<Q, M extends Model, R>
  implements Executor<R>, RawExecutor<Q>, Paginatable<M, R, Q>
{
  protected readonly selectSelector?: SelectSelector<M>[];
  protected distinctSelector?: SelectSelector<M>;
  protected maxSelector?: SelectSelector<M>;
  protected minSelector?: SelectSelector<M>;
  protected countSelector?: SelectSelector<M>;
  protected fromSelector!: Constructor<M>;
  protected whereCondition?: Condition<M>;
  protected orderBySelector?: OrderBySelector<M>;
  protected groupBySelector?: GroupBySelector<M>;
  protected limitSelector?: number;
  protected offsetSelector?: number;

  protected constructor(protected adapter: Adapter<any, Q, any, any>) {}

  select<
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    S extends readonly SelectSelector<M>[],
  >(): SelectOption<M, M[]>;
  select<S extends readonly SelectSelector<M>[]>(
    selector: readonly [...S]
  ): SelectOption<M, Pick<M, S[number]>[]>;

  @final()
  select<S extends readonly SelectSelector<M>[]>(
    selector?: readonly [...S]
  ): SelectOption<M, M[]> | SelectOption<M, Pick<M, S[number]>[]> {
    Object.defineProperty(this, "selectSelector", {
      value: selector,
      writable: false,
    });
    return this as SelectOption<M, M[]> | SelectOption<M, Pick<M, S[number]>[]>;
  }

  @final()
  distinct<S extends SelectSelector<M>>(
    selector: S
  ): DistinctOption<M, M[S][]> {
    this.distinctSelector = selector;
    return this as DistinctOption<M, M[S][]>;
  }

  @final()
  max<S extends SelectSelector<M>>(selector: S): MaxOption<M, M[S]> {
    this.maxSelector = selector;
    return this as MaxOption<M, M[S]>;
  }

  @final()
  min<S extends SelectSelector<M>>(selector: S): MinOption<M, M[S]> {
    this.minSelector = selector;
    return this as MinOption<M, M[S]>;
  }

  @final()
  count<S extends SelectSelector<M>>(selector?: S): CountOption<M, number> {
    this.countSelector = selector;
    return this as CountOption<M, number>;
  }

  @final()
  public from(selector: FromSelector<M>): WhereOption<M, R> {
    this.fromSelector = (
      typeof selector === "string" ? Model.get(selector) : selector
    ) as Constructor<M>;
    if (!this.fromSelector)
      throw new QueryError(`Could not find selector model: ${selector}`);
    return this;
  }

  @final()
  public where(condition: Condition<M>): OrderAndGroupOption<M, R> {
    this.whereCondition = condition;
    return this;
  }

  @final()
  public orderBy(
    selector: OrderBySelector<M>
  ): LimitOption<M, R> & OffsetOption<R> {
    this.orderBySelector = selector;
    return this;
  }

  @final()
  public groupBy(selector: GroupBySelector<M>): LimitOption<M, R> {
    this.groupBySelector = selector;
    return this;
  }

  @final()
  public limit(value: number): OffsetOption<R> {
    this.limitSelector = value;
    return this;
  }

  @final()
  public offset(value: number): Executor<R> {
    this.offsetSelector = value;
    return this;
  }

  @final()
  async execute(): Promise<R> {
    try {
      const query: Q = this.build();
      return (await this.raw(query)) as R;
    } catch (e: unknown) {
      throw new InternalError(e as Error);
    }
  }

  async raw<R>(rawInput: Q): Promise<R> {
    const results = await this.adapter.raw<R>(rawInput);
    if (!this.selectSelector) return results;
    const pkAttr = findPrimaryKey(
      new (this.fromSelector as Constructor<M>)()
    ).id;

    const processor = function recordProcessor(
      this: Statement<Q, M, R>,
      r: any
    ) {
      const id = r[pkAttr];
      return this.adapter.revert(
        r,
        this.fromSelector as Constructor<any>,
        pkAttr,
        id
      ) as any;
    }.bind(this as any);

    if (Array.isArray(results)) return results.map(processor) as R;
    return processor(results) as R;
  }

  protected abstract build(): Q;
  protected abstract parseCondition(condition: Condition<M>, ...args: any[]): Q;
  abstract paginate(size: number): Promise<Paginator<M, R, Q>>;
}