Source

query/Paginator.ts

import { Paginator, PagingError, Sequence } from "@decaf-ts/core";
import { findPrimaryKey, InternalError } from "@decaf-ts/db-decorators";
import { MangoQuery, MangoResponse } from "../types";
import { Constructor, Model } from "@decaf-ts/decorator-validation";
import { CouchDBAdapter } from "../adapter";
import { CouchDBKeys } from "../constants";

/**
 * @description Paginator for CouchDB query results
 * @summary Implements pagination for CouchDB queries using bookmarks for efficient navigation through result sets
 * @template M - The model type that extends Model
 * @template R - The result type
 * @param {CouchDBAdapter<any, any, any>} adapter - The CouchDB adapter
 * @param {MangoQuery} query - The Mango query to paginate
 * @param {number} size - The page size
 * @param {Constructor<M>} clazz - The model constructor
 * @class CouchDBPaginator
 * @example
 * // Example of using CouchDBPaginator
 * const adapter = new MyCouchDBAdapter(scope);
 * const query = { selector: { type: "user" } };
 * const paginator = new CouchDBPaginator(adapter, query, 10, User);
 *
 * // Get the first page
 * const page1 = await paginator.page(1);
 *
 * // Get the next page
 * const page2 = await paginator.page(2);
 */
export class CouchDBPaginator<M extends Model, R> extends Paginator<
  M,
  R,
  MangoQuery
> {
  /**
   * @description Bookmark for CouchDB pagination
   * @summary Stores the bookmark returned by CouchDB for continuing pagination
   */
  private bookMark?: string;

  /**
   * @description Gets the total number of pages
   * @summary Not supported in CouchDB - throws an error when accessed
   * @return {number} Never returns as it throws an error
   * @throws {InternalError} Always throws as this functionality is not available in CouchDB
   */
  override get total(): number {
    throw new InternalError(`The total pages api is not available for couchdb`);
  }

  /**
   * @description Gets the total record count
   * @summary Not supported in CouchDB - throws an error when accessed
   * @return {number} Never returns as it throws an error
   * @throws {InternalError} Always throws as this functionality is not available in CouchDB
   */
  override get count(): number {
    throw new InternalError(
      `The record count api is not available for couchdb`
    );
  }

  /**
   * @description Creates a new CouchDBPaginator instance
   * @summary Initializes a paginator for CouchDB query results
   * @param {CouchDBAdapter<any, any, any, any>} adapter - The CouchDB adapter
   * @param {MangoQuery} query - The Mango query to paginate
   * @param {number} size - The page size
   * @param {Constructor<M>} clazz - The model constructor
   */
  constructor(
    adapter: CouchDBAdapter<any, any, any, any>,
    query: MangoQuery,
    size: number,
    clazz: Constructor<M>
  ) {
    super(adapter, query, size, clazz);
  }

  /**
   * @description Prepares a query for pagination
   * @summary Modifies the raw query to include pagination parameters
   * @param {MangoQuery} rawStatement - The original Mango query
   * @return {MangoQuery} The prepared query with pagination parameters
   */
  protected prepare(rawStatement: MangoQuery): MangoQuery {
    const query: MangoQuery = Object.assign({}, rawStatement);
    if (query.limit) this.limit = query.limit;

    query.limit = this.size;

    return query;
  }

  /**
   * @description Retrieves a specific page of results
   * @summary Executes the query with pagination and processes the results
   * @param {number} [page=1] - The page number to retrieve
   * @return {Promise<R[]>} A promise that resolves to an array of results
   * @throws {PagingError} If trying to access a page other than the first without a bookmark, or if no class is defined
   * @mermaid
   * sequenceDiagram
   *   participant Client
   *   participant CouchDBPaginator
   *   participant Adapter
   *   participant CouchDB
   *
   *   Client->>CouchDBPaginator: page(pageNumber)
   *   Note over CouchDBPaginator: Clone statement
   *   CouchDBPaginator->>CouchDBPaginator: validatePage(page)
   *
   *   alt page !== 1
   *     CouchDBPaginator->>CouchDBPaginator: Check bookmark
   *     alt No bookmark
   *       CouchDBPaginator-->>Client: Throw PagingError
   *     else Has bookmark
   *       CouchDBPaginator->>CouchDBPaginator: Add bookmark to statement
   *     end
   *   end
   *
   *   CouchDBPaginator->>Adapter: raw(statement, false)
   *   Adapter->>CouchDB: Execute query
   *   CouchDB-->>Adapter: Return results
   *   Adapter-->>CouchDBPaginator: Return MangoResponse
   *
   *   Note over CouchDBPaginator: Process results
   *
   *   alt Has warning
   *     CouchDBPaginator->>CouchDBPaginator: Log warning
   *   end
   *
   *   CouchDBPaginator->>CouchDBPaginator: Check for clazz
   *
   *   alt No clazz
   *     CouchDBPaginator-->>Client: Throw PagingError
   *   else Has clazz
   *     CouchDBPaginator->>CouchDBPaginator: Find primary key
   *
   *     alt Has fields in statement
   *       CouchDBPaginator->>CouchDBPaginator: Use docs directly
   *     else No fields
   *       CouchDBPaginator->>CouchDBPaginator: Process each document
   *       loop For each document
   *         CouchDBPaginator->>CouchDBPaginator: Extract original ID
   *         CouchDBPaginator->>Adapter: revert(doc, clazz, pkDef.id, parsedId)
   *       end
   *     end
   *
   *     CouchDBPaginator->>CouchDBPaginator: Store bookmark
   *     CouchDBPaginator->>CouchDBPaginator: Update currentPage
   *     CouchDBPaginator-->>Client: Return results
   *   end
   */
  async page(page: number = 1): Promise<R[]> {
    const statement = Object.assign({}, this.statement);

    if (!this._recordCount || !this._totalPages) {
      this._totalPages = this._recordCount = 0;
      const results: R[] =
        (await this.adapter.raw({ ...statement, limit: undefined })) || [];
      this._recordCount = results.length;
      if (this._recordCount > 0) {
        const size = statement?.limit || this.size;
        this._totalPages = Math.ceil(this._recordCount / size);
      }
    }

    this.validatePage(page);

    if (page !== 1) {
      if (!this.bookMark)
        throw new PagingError("No bookmark. Did you start in the first page?");
      statement["bookmark"] = this.bookMark;
    }
    const rawResult: MangoResponse<any> = await this.adapter.raw(
      statement,
      false
    );

    const { docs, bookmark, warning } = rawResult;
    if (warning) console.warn(warning);
    if (!this.clazz) throw new PagingError("No statement target defined");
    const pkDef = findPrimaryKey(new this.clazz());
    const results =
      statement.fields && statement.fields.length
        ? docs // has fields means its not full model
        : docs.map((d: any) => {
            //no fields means we need to revert to saving process
            const originalId = d._id.split(CouchDBKeys.SEPARATOR);
            originalId.splice(0, 1); // remove the table name
            return this.adapter.revert(
              d,
              this.clazz,
              pkDef.id,
              Sequence.parseValue(
                pkDef.props.type,
                originalId.join(CouchDBKeys.SEPARATOR)
              )
            );
          });
    this.bookMark = bookmark;
    this._currentPage = page;
    return results;
  }
}