/** * Cursor-based Pagination Helper * High-performance pagination for large datasets */ export interface CursorPaginationParams { cursor?: string; limit?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; } export interface CursorPaginationResult { data: T[]; pagination: { limit: number; hasMore: boolean; nextCursor?: string; prevCursor?: string; sortBy: string; sortOrder: 'asc' | 'desc'; }; } export class CursorPagination { /** * Encode cursor from item data */ static encodeCursor(item: any, sortBy: string = 'id'): string { const cursorData = { value: item[sortBy], timestamp: Date.now() }; return Buffer.from(JSON.stringify(cursorData)).toString('base64'); } /** * Decode cursor to get item data */ static decodeCursor(cursor: string): any { try { const decoded = Buffer.from(cursor, 'base64').toString('utf-8'); return JSON.parse(decoded); } catch (error) { return null; } } /** * Generate SQL WHERE clause for cursor pagination * Example: WHERE id > lastId */ static generateWhereClause( cursor?: string, sortBy: string = 'id', sortOrder: 'asc' | 'desc' = 'asc' ): { where: string; values: any[] } { if (!cursor) { return { where: '', values: [] }; } const decoded = this.decodeCursor(cursor); if (!decoded) { return { where: '', values: [] }; } const operator = sortOrder === 'asc' ? '>' : '<'; return { where: `WHERE ${sortBy} ${operator} $1`, values: [decoded.value] }; } /** * Generate MongoDB query for cursor pagination */ static generateMongoQuery( cursor?: string, sortBy: string = '_id', sortOrder: 'asc' | 'desc' = 'asc' ): Record { if (!cursor) { return {}; } const decoded = this.decodeCursor(cursor); if (!decoded) { return {}; } const operator = sortOrder === 'asc' ? '$gt' : '$lt'; return { [sortBy]: { [operator]: decoded.value } }; } /** * Create pagination result with cursors */ static createResult( data: T[], limit: number, sortBy: string, sortOrder: 'asc' | 'desc', getCursorValue?: (item: T) => any ): CursorPaginationResult { const hasMore = data.length > limit; const limitedData = hasMore ? data.slice(0, limit) : data; const nextCursor = hasMore ? this.encodeCursor( getCursorValue ? getCursorValue(limitedData[limitedData.length - 1]) : limitedData[limitedData.length - 1], sortBy ) : undefined; const prevCursor = limitedData.length > 0 ? this.encodeCursor( getCursorValue ? getCursorValue(limitedData[0]) : limitedData[0], sortBy ) : undefined; return { data: limitedData, pagination: { limit, hasMore, nextCursor, prevCursor, sortBy, sortOrder } }; } } export default CursorPagination;