Skip to content

feat: ez.paginated()#3245

Merged
RobinTail merged 34 commits into
masterfrom
ez-paginated
Mar 8, 2026
Merged

feat: ez.paginated()#3245
RobinTail merged 34 commits into
masterfrom
ez-paginated

Conversation

@RobinTail

@RobinTail RobinTail commented Mar 8, 2026

Copy link
Copy Markdown
Owner
  • featuring proprietary schema ez.paginated() to simplify common tasks on paginating data
  • composable and configurable
  • supports both offset and cursor pagination
  • along with Client::hasMore() helper

Summary by CodeRabbit

  • New Features

    • Added offset-style paginated users list endpoint with role filtering at /v2/users/list
    • Introduced unified pagination support (offset and cursor), exported ez.paginated, a public Pagination type, and Client.hasMore helper
  • Documentation

    • Added OpenAPI docs and README pagination usage and examples for the new endpoint and pagination features
  • Tests

    • Added comprehensive tests for paginated inputs, outputs, limits, and composability

@RobinTail RobinTail added the enhancement New feature or request label Mar 8, 2026
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds two-style pagination support: new paginated schema builder (offset and cursor), exposes ez.paginated, generates a public Pagination type and static Client.hasMore() via codegen, adds tests, documentation, OpenAPI, client types, and an example offset-paginated /v2/users/list endpoint and route.

Changes

Cohort / File(s) Summary
Pagination Schema Core
express-zod-api/src/paginated-schema.ts
New paginated() builder implementing offset and cursor styles, config types, input/output Zod schemas, runtime validation, and exported OffsetPaginatedResult / CursorPaginatedResult interfaces.
Client & Integration Base
express-zod-api/src/integration-base.ts, express-zod-api/src/integration.ts
Adds makePaginationType() to emit public Pagination type and #makeHasMoreMethod() to generate static Client.hasMore(response: Pagination): boolean; wires type generation into integration artifact pipeline.
Proprietary Helpers
express-zod-api/src/proprietary-schemas.ts
Exports paginated from ez (import added and included in ez).
Codegen Modifiers
express-zod-api/src/typescript-api.ts
Adds publicStatic access modifier handling and makePublicMethod(..., isStatic?) option so codegen can emit static public methods.
Example Endpoint & Routing
example/endpoints/list-users-paginated.ts, example/routing.ts
New offset-based /v2/users/list endpoint (zod schemas, in-file users array, handler returning { users, total, limit, offset }); route added to routing.
Client Types & OpenAPI
example/example.client.ts, example/example.documentation.yaml
Adds GET/HEAD types and response variants for /v2/users/list, updates Path/Input/response unions and endpointTags, exports Pagination type and Client.hasMore, and adds OpenAPI path/parameters/responses.
Tests & Specs
express-zod-api/tests/paginated-schema.spec.ts, example/index.spec.ts, example/config.ts
Adds comprehensive unit tests for ez.paginated() (offset and cursor), integration test snapshot for the paginated endpoint, and a minor comment relocation in config.
Docs & README
README.md
Adds Pagination documentation and example usage, updates TOC and duplicates Pagination section in two places.
Example client additions
example/example.client.ts
Introduces Pagination type and static Client.hasMore helper plus input/response variant types for the new endpoint.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GeneratedClient
    participant Router
    participant Handler
    participant DataStore
    Client->>GeneratedClient: GET /v2/users/list?limit&offset&roles
    GeneratedClient->>Router: HTTP request to /v2/users/list
    Router->>Handler: invoke listUsersPaginatedEndpoint with parsed input
    Handler->>DataStore: filter by roles, compute total, slice by offset/limit
    DataStore-->>Handler: return page items + total
    Handler-->>Router: respond { users, total, limit, offset }
    Router-->>GeneratedClient: HTTP 200 JSON
    GeneratedClient->>Client: deliver response
    Client->>Client: Client.hasMore(response) checks nextCursor or total vs offset+limit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

documentation, coverage, refactoring

Poem

🐰 I hop through pages, cursor or offset bright,
I nibble schemas by the dim moonlight,
paginated seeds lists that gently grow,
hasMore tells me whether onward I go,
Hooray — more pages! Time to write.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: ez.paginated()' directly and clearly describes the main feature addition: a new proprietary schema helper for pagination. It matches the primary focus of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ez-paginated

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coveralls-official

coveralls-official Bot commented Mar 8, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 100.0%. remained the same
when pulling 68517bd on ez-paginated
into dc22650 on master.

@RobinTail RobinTail marked this pull request as ready for review March 8, 2026 10:05

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-typed feature. The schema construction, overloads, hasMore logic, example, tests, and OpenAPI output all look correct. Two issues worth addressing — one is a real bug involving unvalidated defaults, the other a minor modifier ordering quirk.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

Comment thread express-zod-api/src/paginated-schema.ts
Comment thread express-zod-api/src/typescript-api.ts Outdated
@pullfrog

pullfrog Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Responded to @RobinTail's question about the Zod 4 .default() behavior. Provided a link to colinhacks/zod#4544 and clarified that the short-circuiting behavior is intentional per the Zod maintainer, not a bug — along with the .predefault() workaround.

Pullfrog  | View workflow run | Using Claude Code | Triggered by Pullfrogpullfrog.com𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
example/index.spec.ts (1)

114-119: Add direct coverage for Client.hasMore().

This new test proves the endpoint works, but it still leaves the new public helper unexercised. A small-page request here with an explicit Client.hasMore(...) assertion would make regressions in the generated client API visible.

Possible test extension
-    test("Should respond with paginated list (ez.paginated)", async () => {
-      const response = await fetch(`http://localhost:${port}/v2/users/list`);
+    test("Should respond with paginated list (ez.paginated)", async () => {
+      const response = await fetch(`http://localhost:${port}/v2/users/list?limit=1`);
       expect(response.status).toBe(200);
-      const json = await response.json();
+      const json = (await response.json()) as {
+        data: Parameters<typeof Client.hasMore>[0];
+      };
       expect(json).toMatchSnapshot();
+      expect(Client.hasMore(json.data)).toBe(true);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/index.spec.ts` around lines 114 - 119, Extend the existing test
"Should respond with paginated list (ez.paginated)" to directly exercise the
generated helper by creating a Client from the response payload and asserting
Client.hasMore(...) for the small-page request to catch pagination regressions;
after parsing json from the fetch to /v2/users/list, call Client.hasMore(json)
(or the appropriate static/helper signature) and add an explicit expect(...)
assertion (true/false depending on the known small-page fixture) so the new
public helper Client.hasMore is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example/endpoints/list-users-paginated.ts`:
- Around line 30-33: Update the JSDoc comment above the endpoint to refer to
"users" instead of "items": clarify that ez.paginated() produces a response
shape of { users, total, limit, offset } (since itemsName is set to "users").
Edit the comment that currently reads "items, total, limit, offset" to the
correct "users, total, limit, offset" so it matches the itemsName configuration
and ez.paginated() behavior.

In `@express-zod-api/src/paginated-schema.ts`:
- Around line 55-69: The OffsetInput and CursorInput type aliases use the
non-existent z.ZodCoercedNumber for the limit and offset fields; update those
field types to z.ZodDefault<z.ZodNumber> (i.e., replace z.ZodCoercedNumber with
z.ZodNumber) for limit in both CursorInput and OffsetInput and for offset in
OffsetInput so the TypeScript types match schemas produced by
z.coerce.number()/z.number().

In `@express-zod-api/tests/paginated-schema.spec.ts`:
- Around line 184-230: The paginated builder allows itemsName that can clash
with fixed pagination keys and should reject reserved names; update the
paginated/schema builder (paginated-schema.ts) to validate the itemsName
parameter in the function that constructs the output schema (the code that
builds the object literal with [itemsName] plus fields like limit, offset,
total, nextCursor) and throw or zod-refine when itemsName equals any reserved
key ("limit", "offset", "total", "nextCursor", and any other fixed output field
such as "items"); then add a regression test in paginated-schema.spec.ts that
attempts to create paginated with itemsName set to a reserved key (e.g.,
"limit") and asserts that creation fails (throws or returns an error) to prevent
silent overwrites.
- Around line 56-70: The paginated builder currently applies
.default(defaultLimit) to limitSchema which allows a default that may exceed
maxLimit; update the ez.paginated implementation to validate the config upfront
(e.g., in the paginated factory function) by checking if defaultLimit > maxLimit
and throw a clear Error if so, and keep the limitSchema creation (limitSchema)
as-is or change to a pattern that ensures defaults are validated; also add a
test case to paginated-schema.spec.ts that constructs ez.paginated with
defaultLimit: 30 and maxLimit: 20 and asserts that it throws/ rejects the
invalid configuration.

---

Nitpick comments:
In `@example/index.spec.ts`:
- Around line 114-119: Extend the existing test "Should respond with paginated
list (ez.paginated)" to directly exercise the generated helper by creating a
Client from the response payload and asserting Client.hasMore(...) for the
small-page request to catch pagination regressions; after parsing json from the
fetch to /v2/users/list, call Client.hasMore(json) (or the appropriate
static/helper signature) and add an explicit expect(...) assertion (true/false
depending on the known small-page fixture) so the new public helper
Client.hasMore is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35273d44-bb9b-48bf-9de0-40a0e82ab97a

📥 Commits

Reviewing files that changed from the base of the PR and between 021f93f and b5a0b83.

⛔ Files ignored due to path filters (3)
  • example/__snapshots__/index.spec.ts.snap is excluded by !**/*.snap
  • express-zod-api/tests/__snapshots__/index.spec.ts.snap is excluded by !**/*.snap
  • express-zod-api/tests/__snapshots__/integration.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (12)
  • example/config.ts
  • example/endpoints/list-users-paginated.ts
  • example/example.client.ts
  • example/example.documentation.yaml
  • example/index.spec.ts
  • example/routing.ts
  • express-zod-api/src/integration-base.ts
  • express-zod-api/src/integration.ts
  • express-zod-api/src/paginated-schema.ts
  • express-zod-api/src/proprietary-schemas.ts
  • express-zod-api/src/typescript-api.ts
  • express-zod-api/tests/paginated-schema.spec.ts

Comment thread example/endpoints/list-users-paginated.ts
Comment thread express-zod-api/src/paginated-schema.ts
Comment thread express-zod-api/tests/paginated-schema.spec.ts
Comment thread express-zod-api/tests/paginated-schema.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
express-zod-api/src/paginated-schema.ts (1)

159-170: ⚠️ Potential issue | 🟠 Major

Reject invalid defaultLimit values before building the schema.

z.default() in v4 short-circuits parsing, so defaultLimit = 0, -1, or 1.5 bypasses .int().min(1) and becomes the returned limit whenever the client omits it. Guard defaultLimit with the same invariants as limitSchema.

Suggested guard
   if (maxLimit <= 0)
     throw new Error("ez.paginated: maxLimit must be greater than 0");
+  if (!Number.isInteger(defaultLimit) || defaultLimit < 1) {
+    throw new Error(
+      "ez.paginated: defaultLimit must be a positive integer",
+    );
+  }
   if (defaultLimit > maxLimit) {
     throw new Error(
       "ez.paginated: defaultLimit must not be greater than maxLimit",
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/src/paginated-schema.ts` around lines 159 - 170, The function
that builds paginated schemas (the factory accepting OffsetPaginatedConfig |
CursorPaginatedConfig and returning OffsetPaginatedResult |
CursorPaginatedResult) must validate the provided defaultLimit before
constructing limitSchema because z.default() short-circuits parsing; add guards
for defaultLimit to ensure it's an integer >= 1 and <= maxLimit (same invariants
as limitSchema) and throw descriptive errors (e.g., "ez.paginated: defaultLimit
must be an integer >= 1 and <= maxLimit") when the checks fail; perform these
checks immediately after reading the maxLimit and defaultLimit parameters so
limitSchema and any z.default(defaultLimit) cannot accept invalid defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@express-zod-api/src/paginated-schema.ts`:
- Around line 112-154: The codegen is collapsing both pagination shapes into a
single Pagination type, causing offset endpoints to show cursor fields and vice
versa; update the generator logic in integration-base so it no longer emits one
shared Pagination = { nextCursor } | { total, limit, offset } for all endpoints
but instead emits a discriminated union or distinct types and references the
correct one per endpoint: detect when an endpoint uses paginated() as
OffsetPaginatedResult vs CursorPaginatedResult (symbols: paginated,
OffsetPaginatedResult, CursorPaginatedResult) and either (A) generate Pagination
= { style: "offset"; total: number; limit: number; offset: number } | { style:
"cursor"; nextCursor?: string } and use the matching branch per endpoint, or (B)
generate separate PaginationOffset and PaginationCursor types and have the
endpoint-specific output reference the appropriate type; change the code that
currently constructs the shared Pagination symbol in integration-base to pick
the proper shape based on the endpoint's paginated input/export type.

---

Duplicate comments:
In `@express-zod-api/src/paginated-schema.ts`:
- Around line 159-170: The function that builds paginated schemas (the factory
accepting OffsetPaginatedConfig | CursorPaginatedConfig and returning
OffsetPaginatedResult | CursorPaginatedResult) must validate the provided
defaultLimit before constructing limitSchema because z.default() short-circuits
parsing; add guards for defaultLimit to ensure it's an integer >= 1 and <=
maxLimit (same invariants as limitSchema) and throw descriptive errors (e.g.,
"ez.paginated: defaultLimit must be an integer >= 1 and <= maxLimit") when the
checks fail; perform these checks immediately after reading the maxLimit and
defaultLimit parameters so limitSchema and any z.default(defaultLimit) cannot
accept invalid defaults.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce75a239-863b-4f88-8b7f-8db459d7420c

📥 Commits

Reviewing files that changed from the base of the PR and between 201ef3b and 009d141.

📒 Files selected for processing (3)
  • example/endpoints/list-users-paginated.ts
  • express-zod-api/src/paginated-schema.ts
  • express-zod-api/tests/paginated-schema.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • express-zod-api/tests/paginated-schema.spec.ts

Comment thread express-zod-api/src/paginated-schema.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 620: Add documentation for the Client.hasMore() method in the "Generating
a Frontend Client" section: describe its purpose (returns whether additional
pages are available), specify the expected inputs (e.g., response and
pagination/cursor info) and return value (boolean), and include a short usage
example showing calling Client.hasMore(response, pagination) to decide whether
to fetch the next page; update the link target or anchor text if needed so the
reference at line 620 points to this new explanation.
- Line 641: The inline comment "// or nextCursor" is ambiguous; update the
README examples to clearly separate offset and cursor pagination instead of
implying they can be mixed: replace or remove the comment next to the return {
users, total, limit, offset } example and add two explicit examples — one
showing an offset-style handler returning { users, total, limit, offset } (use
input: { limit, offset } and db.getUsers) and one showing a cursor-style handler
returning { users, nextCursor, limit } (use input: { limit, cursor } and
db.getUsersCursor); reference the return lines and handler examples in the
README so readers can see both formats clearly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c69e1cd0-5fec-4543-a50a-803c435e7a95

📥 Commits

Reviewing files that changed from the base of the PR and between 009d141 and 2464de6.

📒 Files selected for processing (1)
  • README.md

Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread example/endpoints/list-users-paginated.ts Outdated
Comment thread example/example.client.ts Outdated
Comment thread example/example.client.ts Outdated
Comment thread express-zod-api/src/integration-base.ts Outdated
@RobinTail RobinTail added the documentation Improvements or additions to documentation label Mar 8, 2026

@RobinTail RobinTail left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏁 ready

@RobinTail RobinTail merged commit 20fb87f into master Mar 8, 2026
13 checks passed
@RobinTail RobinTail deleted the ez-paginated branch March 8, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant