@decaf-ts/core
Version:
Core persistence module for the decaf framework
187 lines • 7.03 kB
JavaScript
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