2025-07-28 22:37:36 +00:00
|
|
|
import { type CoreResponse } from '../../core/index.js';
|
|
|
|
import type { ICoreResponse } from '../../core_base/types.js';
|
2025-08-18 00:21:14 +00:00
|
|
|
import {
|
|
|
|
type TPaginationConfig,
|
|
|
|
PaginationStrategy,
|
|
|
|
type TPaginatedResponse,
|
|
|
|
} from '../types/pagination.js';
|
2025-04-03 06:36:48 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a paginated response from a regular response
|
|
|
|
*/
|
2025-07-27 21:23:20 +00:00
|
|
|
export async function createPaginatedResponse<T>(
|
2025-07-28 22:37:36 +00:00
|
|
|
response: ICoreResponse<any>,
|
2025-04-03 06:36:48 +00:00
|
|
|
paginationConfig: TPaginationConfig,
|
|
|
|
queryParams: Record<string, string>,
|
2025-08-18 00:21:14 +00:00
|
|
|
fetchNextPage: (
|
|
|
|
params: Record<string, string>,
|
|
|
|
) => Promise<TPaginatedResponse<T>>,
|
2025-07-27 21:23:20 +00:00
|
|
|
): Promise<TPaginatedResponse<T>> {
|
|
|
|
// Parse response body first
|
2025-08-18 00:21:14 +00:00
|
|
|
const body = (await response.json()) as any;
|
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
// Default to response.body for items if response is JSON
|
2025-07-27 21:23:20 +00:00
|
|
|
let items: T[] = Array.isArray(body)
|
|
|
|
? body
|
2025-08-18 00:21:14 +00:00
|
|
|
: body?.items || body?.data || body?.results || [];
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
let hasNextPage = false;
|
|
|
|
let nextPageParams: Record<string, string> = {};
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
// Determine if there's a next page based on pagination strategy
|
|
|
|
switch (paginationConfig.strategy) {
|
|
|
|
case PaginationStrategy.OFFSET: {
|
|
|
|
const config = paginationConfig;
|
2025-08-18 00:21:14 +00:00
|
|
|
const currentPage = parseInt(
|
|
|
|
queryParams[config.pageParam || 'page'] ||
|
|
|
|
String(config.startPage || 1),
|
|
|
|
);
|
|
|
|
const limit = parseInt(
|
|
|
|
queryParams[config.limitParam || 'limit'] ||
|
|
|
|
String(config.pageSize || 20),
|
|
|
|
);
|
2025-07-27 21:23:20 +00:00
|
|
|
const total = getValueByPath(body, config.totalPath || 'total') || 0;
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
hasNextPage = currentPage * limit < total;
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
if (hasNextPage) {
|
|
|
|
nextPageParams = {
|
|
|
|
...queryParams,
|
2025-08-18 00:21:14 +00:00
|
|
|
[config.pageParam || 'page']: String(currentPage + 1),
|
2025-04-03 06:36:48 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
case PaginationStrategy.CURSOR: {
|
|
|
|
const config = paginationConfig;
|
2025-08-18 00:21:14 +00:00
|
|
|
const nextCursor = getValueByPath(
|
|
|
|
body,
|
|
|
|
config.cursorPath || 'nextCursor',
|
|
|
|
);
|
2025-07-27 21:23:20 +00:00
|
|
|
const hasMore = getValueByPath(body, config.hasMorePath || 'hasMore');
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
hasNextPage = !!nextCursor || !!hasMore;
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
if (hasNextPage && nextCursor) {
|
|
|
|
nextPageParams = {
|
|
|
|
...queryParams,
|
2025-08-18 00:21:14 +00:00
|
|
|
[config.cursorParam || 'cursor']: nextCursor,
|
2025-04-03 06:36:48 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
case PaginationStrategy.LINK_HEADER: {
|
|
|
|
const linkHeader = response.headers['link'] || '';
|
2025-04-03 06:52:58 +00:00
|
|
|
// Handle both string and string[] types for the link header
|
2025-08-18 00:21:14 +00:00
|
|
|
const headerValue = Array.isArray(linkHeader)
|
|
|
|
? linkHeader[0]
|
|
|
|
: linkHeader;
|
2025-04-03 06:52:58 +00:00
|
|
|
const links = parseLinkHeader(headerValue);
|
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
hasNextPage = !!links.next;
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
if (hasNextPage && links.next) {
|
|
|
|
// Extract query parameters from next link URL
|
|
|
|
const url = new URL(links.next);
|
|
|
|
nextPageParams = {};
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
url.searchParams.forEach((value, key) => {
|
|
|
|
nextPageParams[key] = value;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
case PaginationStrategy.CUSTOM: {
|
|
|
|
const config = paginationConfig;
|
|
|
|
hasNextPage = config.hasNextPage(response);
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
if (hasNextPage) {
|
|
|
|
nextPageParams = config.getNextPageParams(response, queryParams);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
// Create a function to fetch the next page
|
|
|
|
const getNextPage = async (): Promise<TPaginatedResponse<T>> => {
|
|
|
|
if (!hasNextPage) {
|
|
|
|
throw new Error('No more pages available');
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
return fetchNextPage(nextPageParams);
|
|
|
|
};
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
// Create a function to fetch all remaining pages
|
|
|
|
const getAllPages = async (): Promise<T[]> => {
|
|
|
|
const allItems = [...items];
|
2025-08-18 00:21:14 +00:00
|
|
|
let currentPage: TPaginatedResponse<T> = {
|
|
|
|
items,
|
|
|
|
hasNextPage,
|
|
|
|
getNextPage,
|
|
|
|
getAllPages,
|
|
|
|
response,
|
|
|
|
};
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
while (currentPage.hasNextPage) {
|
|
|
|
try {
|
|
|
|
currentPage = await currentPage.getNextPage();
|
|
|
|
allItems.push(...currentPage.items);
|
|
|
|
} catch (error) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
return allItems;
|
|
|
|
};
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
return {
|
|
|
|
items,
|
|
|
|
hasNextPage,
|
|
|
|
getNextPage,
|
|
|
|
getAllPages,
|
2025-08-18 00:21:14 +00:00
|
|
|
response,
|
2025-04-03 06:36:48 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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> = {};
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
if (!header) {
|
|
|
|
return links;
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
// Split parts by comma
|
|
|
|
const parts = header.split(',');
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
// Parse each part into a name:value pair
|
|
|
|
for (const part of parts) {
|
|
|
|
const section = part.split(';');
|
|
|
|
if (section.length < 2) {
|
|
|
|
continue;
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
const url = section[0].replace(/<(.*)>/, '$1').trim();
|
|
|
|
const name = section[1].replace(/rel="(.*)"/, '$1').trim();
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
links[name] = url;
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
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;
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
const keys = path.split('.');
|
|
|
|
let current = obj;
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
for (const key of keys) {
|
2025-08-18 00:21:14 +00:00
|
|
|
if (
|
|
|
|
current === null ||
|
|
|
|
current === undefined ||
|
|
|
|
typeof current !== 'object'
|
|
|
|
) {
|
2025-04-03 06:36:48 +00:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
current = current[key];
|
|
|
|
}
|
2025-04-03 06:52:58 +00:00
|
|
|
|
2025-04-03 06:36:48 +00:00
|
|
|
return current;
|
2025-08-18 00:21:14 +00:00
|
|
|
}
|