Skip to main content
Glama
pagination.ts5.06 kB
/** * Pagination utilities for TeamCity API */ import { info } from '@/utils/logger'; export interface PaginationParams { count?: number; start?: number; } export interface PaginatedResponse<T> { items: T[]; count: number; total?: number; nextHref?: string; prevHref?: string; } export interface PaginationOptions { pageSize?: number; maxPages?: number; fetchAll?: boolean; } interface ApiResponse<T = unknown> { data: T & { nextHref?: string; prevHref?: string; }; } /** * Async iterator for paginated results */ export class PaginatedIterator<T> { private currentPage = 0; private hasMore = true; private readonly pageSize: number; private readonly maxPages?: number; private readonly fetchFn: (params: PaginationParams) => Promise<PaginatedResponse<T>>; constructor( fetchFn: (params: PaginationParams) => Promise<PaginatedResponse<T>>, options: PaginationOptions = {} ) { this.fetchFn = fetchFn; this.pageSize = options.pageSize ?? 100; this.maxPages = options.maxPages; } /** * Async iterator implementation */ async *[Symbol.asyncIterator](): AsyncIterator<T> { while (this.hasMore && (!this.maxPages || this.currentPage < this.maxPages)) { const start = this.currentPage * this.pageSize; // Sequential page fetch; preserves order and avoids overfetching // eslint-disable-next-line no-await-in-loop const response = await this.fetchFn({ count: this.pageSize, start, }); for (const item of response.items) { yield item; } this.currentPage++; this.hasMore = response.nextHref !== undefined || response.items.length === this.pageSize; if (response.items.length < this.pageSize) { this.hasMore = false; } } } /** * Collect all items into array */ async toArray(): Promise<T[]> { const items: T[] = []; for await (const item of this) { items.push(item); } return items; } /** * Get a single page */ async getPage(pageNumber: number): Promise<T[]> { const start = pageNumber * this.pageSize; const response = await this.fetchFn({ count: this.pageSize, start, }); return response.items; } } /** * Create paginated fetcher function */ export function createPaginatedFetcher<T>( baseFetch: (params: PaginationParams) => Promise<unknown>, extractItems: (response: unknown) => T[], extractTotal?: (response: unknown) => number | undefined ): (params: PaginationParams) => Promise<PaginatedResponse<T>> { return async (params: PaginationParams) => { const response = (await baseFetch({ count: params.count, start: params.start, })) as ApiResponse; const items = extractItems(response.data); const total = extractTotal?.(response.data); return { items, count: items.length, total, nextHref: response.data?.nextHref, prevHref: response.data?.prevHref, }; }; } /** * Fetch all pages automatically */ export async function fetchAllPages<T>( fetchFn: (params: PaginationParams) => Promise<PaginatedResponse<T>>, options: PaginationOptions = {} ): Promise<T[]> { const allItems: T[] = []; let hasMore = true; let currentPage = 0; const pageSize = options.pageSize ?? 100; const maxPages = options.maxPages; info('Starting paginated fetch', { pageSize, maxPages }); while (hasMore && (!maxPages || currentPage < maxPages)) { const start = currentPage * pageSize; // Sequential page fetch; keeps memory and requests bounded // eslint-disable-next-line no-await-in-loop const response = await fetchFn({ count: pageSize, start, }); allItems.push(...response.items); currentPage++; hasMore = response.nextHref !== undefined || response.items.length === pageSize; if (response.items.length < pageSize) { hasMore = false; } info(`Fetched page ${currentPage}`, { itemsInPage: response.items.length, totalItems: allItems.length, hasMore, }); } info('Paginated fetch complete', { totalPages: currentPage, totalItems: allItems.length, }); return allItems; } /** * Parse TeamCity locator string for pagination */ export function parseLocator(locator: string): PaginationParams { const params: PaginationParams = {}; const parts = locator.split(','); for (const part of parts) { const [key, value] = part.split(':'); if (key === 'start' && value !== undefined) { params.start = parseInt(value, 10); } else if (key === 'count' && value !== undefined) { params.count = parseInt(value, 10); } } return params; } /** * Build TeamCity locator string for pagination */ export function buildLocator(params: PaginationParams): string { const parts: string[] = []; if (params.start !== undefined) { parts.push(`start:${params.start}`); } if (params.count !== undefined) { parts.push(`count:${params.count}`); } return parts.join(','); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Daghis/teamcity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server