import { z } from "zod"; // --------------------------------------------------------------------------- // Input schema — merge into any tRPC procedure input with `.merge()` or spread // --------------------------------------------------------------------------- export const PaginationInputSchema = z.object({ page: z.number().int().min(1).default(1), limit: z.number().int().min(1).max(500).default(50), cursor: z.string().optional(), }); export type PaginationInput = z.infer; // --------------------------------------------------------------------------- // Result envelope // --------------------------------------------------------------------------- export interface PaginatedResult { items: T[]; total: number; page: number; limit: number; nextCursor: string | null; } // --------------------------------------------------------------------------- // Core paginate helper // --------------------------------------------------------------------------- /** * Run a Prisma-style `findMany` + `count` with the project's standard * offset+cursor hybrid pagination and the `take: limit + 1` sentinel trick. * * @param findMany Prisma `findMany` (or compatible) that receives `{ skip, take }`. * The caller is responsible for applying `where`/`orderBy`/`include` * and for injecting the cursor condition into `where` when * `cursorWhere` is provided. * @param count Prisma `count` for the *base* where clause (without cursor). * @param input The validated pagination input fields. * @param getId Optional — extract the cursor id from the last item. * Defaults to `item.id` for items that have an `id: string`. */ export async function paginate( findMany: (args: { skip: number; take: number }) => Promise, count: () => Promise, input: PaginationInput, getId: (item: T) => string = (item) => (item as unknown as { id: string }).id, ): Promise> { const page = input.page ?? 1; const limit = input.limit ?? 50; const skip = input.cursor ? 0 : (page - 1) * limit; const [rawItems, total] = await Promise.all([ findMany({ skip, take: limit + 1 }), count(), ]); const hasMore = rawItems.length > limit; const items = hasMore ? rawItems.slice(0, limit) : rawItems; const nextCursor = hasMore && items.length > 0 ? getId(items[items.length - 1]!) : null; return { items, total, page, limit, nextCursor }; } // --------------------------------------------------------------------------- // Cursor-only variant (no total count, lighter) // --------------------------------------------------------------------------- export interface CursorResult { items: T[]; nextCursor: string | null; } export const CursorInputSchema = z.object({ limit: z.number().int().min(1).max(500).default(50), cursor: z.string().optional(), }); export type CursorInput = z.infer; /** * Cursor-only pagination without a total count query. * Useful for large tables or infinite-scroll UIs that don't need a total. */ export async function paginateCursor( findMany: (args: { take: number }) => Promise, input: CursorInput, getId: (item: T) => string = (item) => (item as unknown as { id: string }).id, ): Promise> { const limit = input.limit ?? 50; const rawItems = await findMany({ take: limit + 1 }); const hasMore = rawItems.length > limit; const items = hasMore ? rawItems.slice(0, limit) : rawItems; const nextCursor = hasMore && items.length > 0 ? getId(items[items.length - 1]!) : null; return { items, nextCursor }; }