Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/lib/response-validation.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,25 +52,30 @@ export interface Page<T> {
hasPrev: boolean
totalElements: number | null
totalPages: number | null
nextCursor: string | null
}

export function parsePage<T>(schema: ZodType<T>, data: unknown, context = 'Response'): Page<T> {
// nextCursor is additive on TableValueResult. Passthrough so a new
// envelope field cannot break list commands (Postel's Law).
const envelope = z
.object({
data: z.array(schema),
hasNext: z.boolean(),
hasPrev: z.boolean(),
totalElements: z.number().int().nullable().optional(),
totalPages: z.number().int().nullable().optional(),
nextCursor: z.string().nullable().optional(),
})
.strict()
.passthrough()
const out = parse(envelope, data, `${context}: invalid TableValueResult envelope`)
return {
data: out.data,
hasNext: out.hasNext,
hasPrev: out.hasPrev,
totalElements: out.totalElements ?? null,
totalPages: out.totalPages ?? null,
nextCursor: out.nextCursor ?? null,
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/typed-api.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,8 +38,8 @@ export async function fetchPaginated<TItem>(
/**
* Schema-validated variant of `fetchPaginated`.
*
* Each page is parsed through `parsePage` (envelope `.strict()` — P1) so an
* unknown top-level field on the `TableValueResult` envelope, or a per-item
* Each page is parsed through `parsePage` (envelope `.passthrough()` so
* additive fields like `nextCursor` cannot break list commands; per-item
* shape mismatch, raises a typed `ValidationError` rather than flowing
* silently into `display()`. Continues paginating until `hasNext === false`.
*/
Expand Down
Loading