smartrequest/ts/modern/features/pagination.ts

170 lines
4.8 KiB
TypeScript

import { type IExtendedIncomingMessage } from '../../legacy/smartrequest.request.js';
import { type TPaginationConfig, PaginationStrategy, type TPaginatedResponse } from '../types/pagination.js';
/**
* Creates a paginated response from a regular response
*/
export function createPaginatedResponse<T>(
response: IExtendedIncomingMessage<any>,
paginationConfig: TPaginationConfig,
queryParams: Record<string, string>,
fetchNextPage: (params: Record<string, string>) => Promise<TPaginatedResponse<T>>
): TPaginatedResponse<T> {
// Default to response.body for items if response is JSON
let items: T[] = Array.isArray(response.body)
? response.body
: (response.body?.items || response.body?.data || response.body?.results || []);
let hasNextPage = false;
let nextPageParams: Record<string, string> = {};
// Determine if there's a next page based on pagination strategy
switch (paginationConfig.strategy) {
case PaginationStrategy.OFFSET: {
const config = paginationConfig;
const currentPage = parseInt(queryParams[config.pageParam || 'page'] || String(config.startPage || 1));
const limit = parseInt(queryParams[config.limitParam || 'limit'] || String(config.pageSize || 20));
const total = getValueByPath(response.body, config.totalPath || 'total') || 0;
hasNextPage = currentPage * limit < total;
if (hasNextPage) {
nextPageParams = {
...queryParams,
[config.pageParam || 'page']: String(currentPage + 1)
};
}
break;
}
case PaginationStrategy.CURSOR: {
const config = paginationConfig;
const nextCursor = getValueByPath(response.body, config.cursorPath || 'nextCursor');
const hasMore = getValueByPath(response.body, config.hasMorePath || 'hasMore');
hasNextPage = !!nextCursor || !!hasMore;
if (hasNextPage && nextCursor) {
nextPageParams = {
...queryParams,
[config.cursorParam || 'cursor']: nextCursor
};
}
break;
}
case PaginationStrategy.LINK_HEADER: {
const linkHeader = response.headers['link'] || '';
const links = parseLinkHeader(linkHeader);
hasNextPage = !!links.next;
if (hasNextPage && links.next) {
// Extract query parameters from next link URL
const url = new URL(links.next);
nextPageParams = {};
url.searchParams.forEach((value, key) => {
nextPageParams[key] = value;
});
}
break;
}
case PaginationStrategy.CUSTOM: {
const config = paginationConfig;
hasNextPage = config.hasNextPage(response);
if (hasNextPage) {
nextPageParams = config.getNextPageParams(response, queryParams);
}
break;
}
}
// Create a function to fetch the next page
const getNextPage = async (): Promise<TPaginatedResponse<T>> => {
if (!hasNextPage) {
throw new Error('No more pages available');
}
return fetchNextPage(nextPageParams);
};
// Create a function to fetch all remaining pages
const getAllPages = async (): Promise<T[]> => {
const allItems = [...items];
let currentPage: TPaginatedResponse<T> = { items, hasNextPage, getNextPage, getAllPages, response };
while (currentPage.hasNextPage) {
try {
currentPage = await currentPage.getNextPage();
allItems.push(...currentPage.items);
} catch (error) {
break;
}
}
return allItems;
};
return {
items,
hasNextPage,
getNextPage,
getAllPages,
response
};
}
/**
* Parse Link header for pagination
* Link: <https://api.example.com/users?page=2>; rel="next", <https://api.example.com/users?page=5>; rel="last"
*/
export function parseLinkHeader(header: string): Record<string, string> {
const links: Record<string, string> = {};
if (!header) {
return links;
}
// Split parts by comma
const parts = header.split(',');
// Parse each part into a name:value pair
for (const part of parts) {
const section = part.split(';');
if (section.length < 2) {
continue;
}
const url = section[0].replace(/<(.*)>/, '$1').trim();
const name = section[1].replace(/rel="(.*)"/, '$1').trim();
links[name] = url;
}
return links;
}
/**
* Get a nested value from an object using dot notation path
* e.g., getValueByPath(obj, "data.pagination.nextCursor")
*/
export function getValueByPath(obj: any, path?: string): any {
if (!path || !obj) {
return undefined;
}
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current === null || current === undefined || typeof current !== 'object') {
return undefined;
}
current = current[key];
}
return current;
}