204 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			204 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { type CoreResponse } from '../../core/index.js';
 | |
| import type { ICoreResponse } from '../../core_base/types.js';
 | |
| import {
 | |
|   type TPaginationConfig,
 | |
|   PaginationStrategy,
 | |
|   type TPaginatedResponse,
 | |
| } from '../types/pagination.js';
 | |
| 
 | |
| /**
 | |
|  * Creates a paginated response from a regular response
 | |
|  */
 | |
| export async function createPaginatedResponse<T>(
 | |
|   response: ICoreResponse<any>,
 | |
|   paginationConfig: TPaginationConfig,
 | |
|   queryParams: Record<string, string>,
 | |
|   fetchNextPage: (
 | |
|     params: Record<string, string>,
 | |
|   ) => Promise<TPaginatedResponse<T>>,
 | |
| ): Promise<TPaginatedResponse<T>> {
 | |
|   // Parse response body first
 | |
|   const body = (await response.json()) as any;
 | |
| 
 | |
|   // Default to response.body for items if response is JSON
 | |
|   let items: T[] = Array.isArray(body)
 | |
|     ? body
 | |
|     : body?.items || body?.data || 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(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(
 | |
|         body,
 | |
|         config.cursorPath || 'nextCursor',
 | |
|       );
 | |
|       const hasMore = getValueByPath(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'] || '';
 | |
|       // Handle both string and string[] types for the link header
 | |
|       const headerValue = Array.isArray(linkHeader)
 | |
|         ? linkHeader[0]
 | |
|         : linkHeader;
 | |
|       const links = parseLinkHeader(headerValue);
 | |
| 
 | |
|       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;
 | |
| }
 |