UNPKG

@decaf-ts/core

Version:

Core persistence module for the decaf framework

187 lines 7.03 kB
import { PagingError } from "./errors.js"; import { Context, PersistenceKeys, prefixMethod, UnsupportedError, } from "./../persistence/index.js"; import { LoggedClass } from "@decaf-ts/logging"; import { PreparedStatementKeys } from "./constants.js"; import { Repository } from "./../repository/Repository.js"; import { SerializationError } from "@decaf-ts/db-decorators"; /** * @description Handles pagination for database queries * @summary Provides functionality for navigating through paginated query results * * This abstract class manages the state and navigation of paginated database query results. * It tracks the current page, total pages, and record count, and provides methods for * moving between pages. * * @template M - The model type this paginator operates on * @template R - The return type of the paginated query (defaults to M[]) * @template Q - The query type (defaults to any) * @param {Adapter<any, Q, any, any>} adapter - The database adapter to use for executing queries * @param {Q} query - The query to paginate * @param {number} size - The number of records per page * @param {Constructor<M>} clazz - The constructor for the model type * @class Paginator * @example * // Create a paginator for a user query * const userQuery = db.select().from(User); * const paginator = await userQuery.paginate(10); // 10 users per page * * // Get the first page of results * const firstPage = await paginator.page(1); * * // Navigate to the next page * const secondPage = await paginator.next(); * * // Get information about the pagination * console.log(`Page ${paginator.current} of ${paginator.total}, ${paginator.count} total records`); * * @mermaid * sequenceDiagram * participant Client * participant Paginator * participant Adapter * participant Database * * Client->>Paginator: new Paginator(adapter, query, size, clazz) * Client->>Paginator: page(1) * Paginator->>Paginator: validatePage(1) * Paginator->>Paginator: prepare(query) * Paginator->>Adapter: execute query with pagination * Adapter->>Database: execute query * Database-->>Adapter: return results * Adapter-->>Paginator: return results * Paginator-->>Client: return page results * * Client->>Paginator: next() * Paginator->>Paginator: page(current + 1) * Paginator->>Paginator: validatePage(current + 1) * Paginator->>Adapter: execute query with pagination * Adapter->>Database: execute query * Database-->>Adapter: return results * Adapter-->>Paginator: return results * Paginator-->>Client: return page results */ export class Paginator extends LoggedClass { get current() { return this._currentPage; } get total() { return this._totalPages; } get count() { return this._recordCount; } get statement() { if (!this._statement) this._statement = this.prepare(this.query); return this._statement; } constructor(adapter, query, size, clazz) { super(); this.adapter = adapter; this.query = query; this.size = size; this.clazz = clazz; prefixMethod(this, this.page, this.pagePrefix, this.page.name); } isPreparedStatement() { const query = this.query; return (query.method && query.method.match(new RegExp(`${PreparedStatementKeys.FIND_BY}|${PreparedStatementKeys.LIST_BY}`, "gi"))); } async pagePrefix(page, ...args) { const contextArgs = await Context.args(PersistenceKeys.QUERY, this.clazz, args, this.adapter); return [page, ...contextArgs.args]; } async pagePrepared(page, ...argz) { const repo = Repository.forModel(this.clazz, this.adapter.alias); const statement = this.query; const { method, args, params } = statement; const regexp = new RegExp(`^${PreparedStatementKeys.FIND_BY}|${PreparedStatementKeys.LIST_BY}`, "gi"); if (!method.match(regexp)) throw new UnsupportedError(`Method ${method} is not supported for pagination`); regexp.lastIndex = 0; const pagedMethod = method.replace(regexp, PreparedStatementKeys.PAGE_BY); const preparedArgs = [pagedMethod, ...args]; let preparedParams = { limit: this.size, offset: page, bookmark: this._bookmark, }; if (pagedMethod === PreparedStatementKeys.PAGE_BY && preparedArgs.length <= 2) { preparedArgs.push(params.direction); } else { preparedParams = { direction: params.direction, limit: this.size, offset: page, bookmark: this._bookmark, }; } preparedArgs.push(preparedParams); const result = await repo.statement(...preparedArgs, ...argz); return this.apply(result); } async next(...args) { return this.page(this.current + 1, ...args); } async previous(...args) { return this.page(this.current - 1, ...args); } validatePage(page) { if (page < 1 || !Number.isInteger(page)) throw new PagingError("Page number cannot be under 1 and must be an integer"); if (typeof this._totalPages !== "undefined" && page > this._totalPages) throw new PagingError(`Only ${this._totalPages} are available. Cannot go to page ${page}`); return page; } async page(page = 1, ...args) { const { ctxArgs } = this.adapter["logCtx"](args, this.page); if (this.isPreparedStatement()) return (await this.pagePrepared(page, ...ctxArgs)); throw new UnsupportedError("Raw support not available without subclassing this"); } serialize(data, toString = false) { const serialization = { data: data, current: this.current, total: this.total, count: this.count, bookmark: this._bookmark, }; try { return toString ? JSON.stringify(serialization) : serialization; } catch (e) { throw new SerializationError(e); } } apply(serialization) { const ser = typeof serialization === "string" ? Paginator.deserialize(serialization) : serialization; this._currentPage = ser.current; this._totalPages = ser.total; this._recordCount = ser.count; this._bookmark = ser.bookmark; return ser.data; } static deserialize(str) { try { return JSON.parse(str); } catch (e) { throw new SerializationError(e); } } static isSerializedPage(obj) { return (obj && typeof obj === "object" && Array.isArray(obj.data) && typeof obj.total === "number" && typeof obj.current === "number" && typeof obj.count === "number"); } } //# sourceMappingURL=Paginator.js.map