Skip to content

feat(@pinia/colada): generate InfiniteQuery factory helpers for paginated operations#3846

Open
50Bytes-dev wants to merge 5 commits intohey-api:mainfrom
50Bytes-dev:feat/pinia-colada-infinite-query-options
Open

feat(@pinia/colada): generate InfiniteQuery factory helpers for paginated operations#3846
50Bytes-dev wants to merge 5 commits intohey-api:mainfrom
50Bytes-dev:feat/pinia-colada-infinite-query-options

Conversation

@50Bytes-dev
Copy link
Copy Markdown

Closes #3377.

Summary

Mirrors the TanStack v5 infiniteQueryOptions generator inside the @pinia/colada plugin so paginated OpenAPI operations emit ready-to-use Pinia Colada infinite-query helpers. Closes the parity gap with the TanStack family of plugins.

Per paginated operation the generator emits:

  • {{name}}InfiniteQueryKey — typed key factory built on the extended createQueryKey('id', options, true) discriminator.
  • {{name}}InfiniteQuery — Option-A factory (options, init) => DefineInfiniteQueryOptionsTagged<...>. init is a typed Pick<DefineInfiniteQueryOptions<TData, TError, TPageParam, undefined>, 'initialPageParam' | 'getNextPageParam' | 'maxPages' | 'getPreviousPageParam'> — required upstream fields are enforced at compile time, optional ones surface in IDE completion. No @ts-ignore anywhere in the emitted output.

Shared once per output (deduped via plugin.querySymbol):

  • createInfiniteParams<K extends Pick<QueryKey<Options>[0], 'body' | 'path' | 'query'>> merges body / path / query from the queryKey with the per-page object. No headers slot — Pinia QueryKey excludes headers by design (packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts).
  • Extended createQueryKey runtime helper with infinite?: boolean parameter and _infinite?: boolean slot on the QueryKey type alias to prevent cache collisions between regular and infinite entries.

Plugin config

defineConfig({
  plugins: [
    {
      name: '@pinia/colada',
      infiniteQueryOptions: {
        meta: (op) => ({ id: op.id }),
      },
      infiniteQueryKeys: { tags: true },
    },
  ],
});

Both blocks accept boolean / string / function shorthands matching the existing queryOptions / queryKeys conventions. Defaults: enabled: true, naming {{name}}InfiniteQuery / {{name}}InfiniteQueryKey.

Use-site

const { data, loadNextPage } = useInfiniteQuery(() =>
  getFooInfiniteQuery(options, {
    initialPageParam: 0,
    getNextPageParam: (last) => last.nextCursor,
    // maxPages, getPreviousPageParam are optional and surface in IDE completion
  }),
);

The two values OpenAPI cannot derive (initialPageParam, getNextPageParam) are enforced at compile time via the init Pick. Auto-generation of these values from response schema is documented as a follow-up under "Future Enhancements" in the plan (industry prior-art: Speakeasy x-speakeasy-pagination, Stainless x-stainless-pagination-property, APIMatic x-pagination).

Workspace bumps

@pinia/colada 0.x^1.2.1 in:

  • dev/package.json
  • examples/openapi-ts-pinia-colada/package.json
  • packages/openapi-ts-tests/main/package.json

Required for defineInfiniteQueryOptions (added in v1.2.0, April 17 2026) and the DefineInfiniteQueryOptions type. defineQueryOptions shape is stable v0.17.6 → v1.2.1, so existing snapshot output is unchanged except for the additive _infinite? / infinite? additions.

Snapshot impact

Existing @pinia/colada/fetch and @pinia/colada/asClass snapshots gain:

  • _infinite?: boolean optional prop on the QueryKey type alias.
  • infinite?: boolean optional parameter on the createQueryKey runtime helper.
  • New infinite output for paginated operations already present in full.yaml.

Two new scenarios target the existing specs/3.1.x/pagination-ref.yaml fixture:

  • pagination — verifies factory shape, externals, createInfiniteParams dedup, and absence of @ts-ignore.
  • pagination-disabled — verifies that infiniteQueryOptions: false omits all infinite output while regular query/mutation output is untouched.

Test plan

  • pnpm install clean.
  • pnpm ty -- @hey-api/openapi-ts clean.
  • pnpm tt (vitest projects @hey-api/openapi-ts + @test/openapi-ts): 880 tests pass, 1 skipped.
  • pnpm lint clean (oxfmt + eslint, husky lint-staged ran on commit).
  • Snapshot diff inspected by hand — matches the High-Level Technical Design sketch exactly: closure-captured key, no headers slot, no @ts-ignore, static defineInfiniteQueryOptions({...}) call.

Post-Deploy Monitoring & Validation

No additional operational monitoring required — this is build-time codegen only, no runtime/server impact. Generated code uses throwOnError: true to propagate SDK errors via Pinia Colada's normal error pipeline. Consumers on @pinia/colada < 1.2.0 will hit a missing-export error at typecheck/build time; the changeset documents the version requirement and infiniteQueryOptions: false provides an opt-out for users not yet ready to upgrade.

Verification at user-side: monitor for issue reports referencing missing defineInfiniteQueryOptions import (indicates user is on @pinia/colada < 1.2.0) — direct them to the changeset or the opt-out flag.

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 2, 2026

@50Bytes-dev is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label May 2, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 2, 2026

🦋 Changeset detected

Latest commit: 151ac76

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hey-api/openapi-ts Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dosubot dosubot Bot added the feature 🚀 Feature request. label May 2, 2026
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented May 2, 2026

TL;DR — Adds InfiniteQuery factory helper generation to the @pinia/colada plugin, closing the parity gap with the TanStack family. Paginated OpenAPI operations now emit typed {{name}}InfiniteQuery and {{name}}InfiniteQueryKey helpers that integrate with Pinia Colada's useInfiniteQuery / defineInfiniteQueryOptions (requires @pinia/colada >= 1.2.0, pinned to exact 1.2.1).

Key changes

  • New infiniteQueryOptions.ts generator — emits per-operation InfiniteQuery factory and InfiniteQueryKey key factory for paginated operations, using a typed init parameter (Pick<DefineInfiniteQueryOptions, ...>) to enforce initialPageParam and getNextPageParam at compile time.
  • Shared createInfiniteParams utility — merges body/path/query from the query key with the per-page object; now generic over TData extends Options so its return type stays Pick<TData, 'body' | 'path' | 'query'> while the page input is permissive.
  • Extended createQueryKey with infinite discriminator — adds an _infinite?: boolean slot to QueryKey and an infinite?: boolean parameter to createQueryKey to prevent cache collisions between regular and infinite entries.
  • Plugin config surfaceinfiniteQueryKeys and infiniteQueryOptions options with the same boolean | string | function | object shorthands as existing queryOptions/queryKeys.
  • @pinia/colada pinned to exact 1.2.1 — required for defineInfiniteQueryOptions (added in v1.2.0); updated in dev/, examples/, and test packages.
  • Deep-partial override type (latest commit) — page-object type is now an inline literal { body?: Partial<Options<T>['body']>; path?: Partial<...>; query?: Partial<...> } instead of the shallow Partial<Pick<Options<T>, ...>>, fixing TS2322 when an operation's query (or body/path) carries a required sibling next to the pagination param.
  • Earlier strict-mode fixes — default-value parameter pattern for optional options (TS1016), structural narrowing for pageParam, and nested pagination param emission as nested object literals.
  • Coverage test suite infiniteQueryOptions.test.ts — six createClient scenarios: numeric cursor, string cursor, nested-ref pagination, mixed paginated + regular operations, infiniteQueryOptions: false opt-out, and deep-partial override for a required-sibling query field (tenantId next to offset).
  • Expanded test fixturespecs/3.1.x/pagination-ref.yaml now includes /albums (numeric cursor), /products (string cursor), and /orders (paginated with required sibling tenantId). Snapshot scenarios pagination and pagination-disabled exercise multi-operation generation and opt-out behavior.

Summary | 60 files | 5 commits | base: mainfeat/pinia-colada-infinite-query-options


Infinite query factory generation

Before: The @pinia/colada plugin only generated queryOptions and mutationOptions helpers; paginated operations had no infinite-query support.
After: Paginated operations automatically emit {{name}}InfiniteQuery(options, init) and {{name}}InfiniteQueryKey(options) — fully typed, no @ts-ignore.

The generator detects pagination via operationPagination(), then emits a factory that wraps defineInfiniteQueryOptions with a query function that converts the pageParam (either a primitive or a structured object) into merged request parameters via the shared createInfiniteParams helper. The init spread passes through initialPageParam, getNextPageParam, maxPages, and getPreviousPageParam.

How does cache isolation between regular and infinite queries work? The `createQueryKey` helper now accepts an optional third `infinite` boolean. When `true`, the resulting key object includes `_infinite: true`, making infinite-query cache entries structurally distinct from regular query entries for the same operation ID.

infiniteQueryOptions.ts · queryKey.ts · types.ts · v0/plugin.ts


Deep-partial override for required-sibling query fields

Before: The page-override type was Partial<Pick<Options<T>, 'body' | 'path' | 'query'>>. Shallow Partial kept the inner query shape strict — so getNextPageParam callbacks returning { query: { offset } } for an operation with a required sibling (e.g. tenantId) failed with TS2322.
After: The override type is an inline deep-partial literal keyed off the operation data type. createInfiniteParams gains a TData extends Options generic so its return stays the strict shape the SDK call expects.

Form Shape
Previous (shallow) Partial<Pick<Options<T>, 'body' | 'path' | 'query'>>
Current (deep) { body?: Partial<Options<T>['body']>; path?: Partial<Options<T>['path']>; query?: Partial<Options<T>['query']> }

For nested pagination paths (e.g. foo.page), the runtime literal still casts through unknown because only the leaf key is filled and deep partial stops at the first level.

infiniteQueryOptions.ts · config.ts


Strict-mode type safety for generated helpers

Before: Generated infinite-query factories had TS errors under --strict — TS1016 (required param after optional), TS2345 (page spread unassignable), and TS2322 (nullable cursors routing into the page-object branch).
After: All generated code compiles cleanly under tsc --strict for numeric, string, nullable, and nested pagination parameter shapes.

Issue Fix
TS1016 (required param follows optional) Optional options uses = {} default instead of ? marker
TS2345 (page spread unassignable) createInfiniteParams return is Pick<TData, 'body' | 'path' | 'query'> keyed off the operation data type
Nested pagination params (foo.page) Emit nested object literal { query: { foo: { page: pageParam } } } instead of dotted key
TS2322 (nullable cursor into object branch) Structural narrowing (typeof === 'object' && !== null && 'body' in ...) plus explicit cast of pageParam to the pagination schema type

infiniteQueryOptions.ts · pagination-ref.yaml


Review feedback coverage tests

Before: Infinite-query generation was exercised only through end-to-end snapshot scenarios in pagination / pagination-disabled.
After: A dedicated unit-style test file runs createClient against six minimal in-memory OpenAPI docs covering each strict-mode fix path, including the new required-sibling case.

The tests use dryRun: true where possible (the required-sibling case writes to a temp dir with HEYAPI_CODEGEN_ENV=development stubbed so the bundled client-fetch sources resolve). Scenarios include nullable numeric cursors (integer | null), nullable string cursors (date-time | null), ref-based nested pagination params (Filter.page), coexisting paginated + non-paginated operations, the infiniteQueryOptions: false opt-out, and a pagination param next to a required sibling query field.

infiniteQueryOptions.test.ts

Pullfrog  | View workflow run | via Pullfrog𝕏

@codecov
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

Codecov Report

❌ Patch coverage is 95.60440% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.35%. Comparing base (1721aa6) to head (151ac76).

Files with missing lines Patch % Lines
.../src/plugins/@pinia/colada/infiniteQueryOptions.ts 94.66% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3846      +/-   ##
==========================================
+ Coverage   39.58%   42.35%   +2.76%     
==========================================
  Files         532      533       +1     
  Lines       19581    19670      +89     
  Branches     5835     5854      +19     
==========================================
+ Hits         7751     8331     +580     
+ Misses       9582     9175     -407     
+ Partials     2248     2164      -84     
Flag Coverage Δ
unittests 42.35% <95.60%> (+2.76%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ated operations

Closes hey-api#3377.

Mirrors the TanStack v5 `infiniteQueryOptions` generator inside the `@pinia/colada` plugin so paginated OpenAPI operations emit ready-to-use Pinia Colada infinite-query helpers.

Per paginated operation, the generator emits:

- `{{name}}InfiniteQueryKey` — typed key factory built on the extended `createQueryKey('id', options, true)` discriminator.
- `{{name}}InfiniteQuery` — Option-A factory `(options, init) => DefineInfiniteQueryOptionsTagged<...>`. `init` is a typed `Pick<DefineInfiniteQueryOptions<TData, TError, TPageParam, undefined>, 'initialPageParam' | 'getNextPageParam' | 'maxPages' | 'getPreviousPageParam'>` — required upstream fields are enforced at compile time, optional ones surface in IDE completion. No `@ts-ignore`.

Shared helpers:

- `createInfiniteParams<K extends Pick<QueryKey<Options>[0], 'body' | 'path' | 'query'>>` — deduped via `plugin.querySymbol`. No `headers` slot (Pinia `QueryKey` excludes headers by design).
- Extended `createQueryKey` runtime helper with `infinite?: boolean` parameter and `_infinite?: boolean` slot on the QueryKey type alias.

Plugin config:

- `infiniteQueryOptions` — `{ enabled: true, name: '{{name}}InfiniteQuery', case, meta? }` (independent from `queryOptions.meta`).
- `infiniteQueryKeys` — `{ enabled: true, name: '{{name}}InfiniteQueryKey', case, tags: false }`.

Workspace bumps `@pinia/colada` from `0.x` to `^1.2.1` to pick up `defineInfiniteQueryOptions` (added in v1.2.0) and the `DefineInfiniteQueryOptions` type. `defineQueryOptions` shape is stable v0.17.6 → v1.2.1, so existing snapshot output is unchanged except for the additive `_infinite?` / `infinite?` additions.

Use-site:

```ts
const { data, loadNextPage } = useInfiniteQuery(() =>
  getFooInfiniteQuery(options, {
    initialPageParam: 0,
    getNextPageParam: (last) => last.nextCursor,
  }),
);
```
@50Bytes-dev 50Bytes-dev force-pushed the feat/pinia-colada-infinite-query-options branch from 2d5757d to 709c565 Compare May 2, 2026 10:09
@dosubot dosubot Bot added size:XS This PR changes 0-9 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels May 2, 2026
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

Two issues to address before merge: (1) planning docs committed to the repo, (2) version specifiers violate save-exact=true.

The core implementation is solid — it cleanly mirrors the TanStack pattern, the createInfiniteParams helper is well-structured, and the dedup via plugin.querySymbol is correct. The test coverage with both pagination and pagination-disabled scenarios is good. The _infinite discriminator on the shared QueryKey type (always emitted regardless of config) matches TanStack's existing approach for cache-collision prevention.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏

@@ -0,0 +1,126 @@
---
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.

This file and docs/plans/2026-05-02-001-feat-pinia-colada-infinite-query-options-plan.md are AI planning artifacts, not user-facing documentation. They create novel docs/brainstorms/ and docs/plans/ directories that don't exist anywhere else in the repo. Remove both files before merge — the PR description and changeset already capture the design decisions.

Comment thread dev/package.json Outdated
"@opencode-ai/sdk": "1.3.13",
"@orpc/contract": "1.13.14",
"@pinia/colada": "0.19.1",
"@pinia/colada": "^1.2.1",
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.

The repo's .npmrc has save-exact=true, so all versions should be exact. This applies to all three package.json files changed in this PR (dev/, examples/openapi-ts-pinia-colada/, packages/openapi-ts-tests/main/).

Suggested change
"@pinia/colada": "^1.2.1",
"@pinia/colada": "1.2.1",

Resolves three classes of strict-tsc errors that surfaced in consumer
projects after PR hey-api#3846 introduced @pinia/colada InfiniteQuery factories:

- TS1016: when options is logically optional, emit `options: Type = {}`
  instead of `options?:`, so the required `init` parameter no longer
  follows an optional one.
- TS2345: type the per-request page shape as
  `Partial<Pick<Options<TData>, 'body' | 'path' | 'query'>>` (operation
  data type instead of the broad QueryKey shape) and loosen the
  `createInfiniteParams` K bound to `Pick<Options, ...>`. Spread of
  `...params` into the SDK call now typechecks even when body/path are
  `never`, while Partial allows the wrap literal to omit fields already
  filled in by `options`.
- TS2322: replace the `typeof pageParam === 'object'` narrowing with a
  structural guard (`'body' in pageParam || 'path' in pageParam ||
  'query' in pageParam`), and cast the leaf pageParam to the pagination
  schema's emitted type so unions like `Date | null` and `null` no
  longer leak into the page-object branch.

Also fixes a runtime correctness bug for nested pagination params: a
parameter like `foo` whose pagination keyword lives on its inner schema
(`foo.page`) now generates `{ query: { foo: { page: pageParam } } }`
instead of a literal dotted key `'foo.page'`.

Extend `specs/3.1.x/pagination-ref.yaml` with `getAlbums` (optional
integer cursor) and `getProducts` (optional date-time cursor) so the
snapshot tests cover both flat and primitive-with-null pagination
shapes alongside the pre-existing nested `foo.page` case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels May 2, 2026
50Bytes-dev and others added 2 commits May 5, 2026 12:39
…nfinite-query-options

# Conflicts:
#	pnpm-lock.yaml
- Pin @pinia/colada to "1.2.1" (drop caret) in dev/, example, and test packages to match repo-wide save-exact policy in .npmrc
- Regenerate examples/openapi-ts-pinia-colada client to keep examples:check in sync after the merge from main
- Add unit tests covering @pinia/colada infiniteQueryOptions generation paths (numeric/string cursor, nested ref pagination, mixed ops, disabled flag) — lifts patch coverage from ~3% to ~91%

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3846

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3846

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3846

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3846

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3846

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3846

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3846

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3846

commit: 151ac76

…deep-partial inline literal

Generated InfiniteQuery helpers used `Partial<Pick<Options<TData>, 'body' | 'path' | 'query'>>`
as the page-override type, which only opt-out the top level. When `query` (or `body`/`path`)
carried a required sibling alongside the pagination param (e.g. `tenantId` next to `offset`,
`client_subscription_id` next to `offset`), TS rejected the override literal with TS2322
because the inner shape stayed strict. The previous fix worked around this with an
`as unknown as Partial<Pick<...>>` cast — semantically a lie that also failed to help users
returning partial overrides from `getNextPageParam` callbacks.

Switch to an inline deep-partial literal — `{ body?: Partial<O['body']>; path?: Partial<O['path']>;
query?: Partial<O['query']> }` — at all three use sites (typePageObjectParam, page const annotation,
DefineInfiniteQueryOptions TPageParam generic). The inline form keeps the override-type private to
each generated file (no new exported helper). `createInfiniteParams` gains a `TData extends Options`
generic and an explicit `Pick<TData, 'body' | 'path' | 'query'>` return type so its output stays
strict for the SDK call while inputs remain permissive. The cast survives only for nested
pagination paths (e.g. `foo.page` where `foo` is a required object) where deep-partial alone can't
reach the inner level.

Adds in-source regression test using tmpdir + fs.readFileSync to assert the deep-partial form is
present and no `as unknown as Partial<Pick<` / `@ts-ignore` slips into pinia output. Adds /orders
operation to specs/3.1.x/pagination-ref.yaml exercising the required-sibling case end-to-end via
the existing snapshot tests. Extends the existing changeset for this branch with a 6th bullet
covering the change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 Feature request. size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useInfiniteQuery() support for Pinia Colada?

1 participant