@7nohe/openapi-react-query-codegen 3.0.0-beta.2 โ†’ 3.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,62 @@
1
1
  # OpenAPI React Query Codegen
2
2
 
3
- > Code generator for creating [React Query (also known as TanStack Query)](https://tanstack.com/query) hooks based on your OpenAPI schema.
3
+ > Code generator for [TanStack Query (React Query)](https://tanstack.com/query) based on your OpenAPI schema โ€” `queryOptions` factories following the official TanStack Query v5 pattern, plus ready-to-use hooks, prefetch, ensure, suspense, and infinite query helpers.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen.svg)](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen)
6
6
 
7
+ ๐Ÿ“– **[Documentation](https://openapi-react-query-codegen.vercel.app)** ยท [Migrating to v3](https://openapi-react-query-codegen.vercel.app/guides/migrating-to-v3/)
8
+
7
9
  ## Features
8
10
 
9
- - Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks
10
- - Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions
11
- - Generates query keys and functions for query caching
12
- - Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
11
+ - **`queryOptions` / `infiniteQueryOptions` factories** for every GET operation โ€” the [TanStack Query v5 recommended pattern](https://tanstack.com/query/latest/docs/framework/react/guides/query-options), composable with `useQuery`, `useQueries`, `useSuspenseQuery`, `prefetchQuery`, `ensureQueryData`, and `setQueryData` with full type safety
12
+ - **Custom hooks**: `useQuery`, `useSuspenseQuery`, `useMutation`, `useInfiniteQuery`, and `useSuspenseInfiniteQuery` variants per operation
13
+ - **SSR helpers**: `prefetchQuery`, `prefetchInfiniteQuery`, and `ensureQueryData` functions per operation โ€” ready for Next.js App Router hydration
14
+ - **Hierarchical query keys** with exported key constants and functions: invalidate one exact query, all infinite pages of an operation, or every cache entry of an operation with a single prefix
15
+ - **Pure TypeScript clients** generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) (fetch and axios)
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ npm install -D @7nohe/openapi-react-query-codegen
21
+ npx openapi-rq -i ./petstore.yaml
22
+ ```
23
+
24
+ ```tsx
25
+ import { useQuery } from "@tanstack/react-query";
26
+ import { findPetsOptions } from "./openapi/queries";
27
+
28
+ function Pets() {
29
+ const { data } = useQuery(findPetsOptions({ query: { limit: 10 } }));
30
+ // ...or use the generated hook directly: useFindPets({ query: { limit: 10 } })
31
+ }
32
+ ```
33
+
34
+ See the [documentation](https://openapi-react-query-codegen.vercel.app) for CLI options, SSR recipes, and infinite query usage.
35
+
36
+ ## How it compares
37
+
38
+ | | This library | @hey-api tanstack-query plugin | Orval |
39
+ |---|---|---|---|
40
+ | `queryOptions` / `infiniteQueryOptions` factories (TanStack v5 pattern) | โœ… | โœ… | โŒ |
41
+ | Ready-to-use hooks (`useQuery` / suspense / infinite variants) | โœ… | โŒ (options only) | โœ… |
42
+ | SSR helpers (`prefetchQuery` / `prefetchInfiniteQuery` / `ensureQueryData`) | โœ… | โŒ | Partial (`usePrefetch`) |
43
+ | Hierarchical query keys for granular invalidation | โœ… | โœ… (tags) | Partial |
44
+ | Stable release line | โœ… SemVer | pre-1.0, frequent breaking changes | โœ… |
45
+ | MSW mock generation | โŒ (out of scope) | โŒ | โœ… |
46
+ | Vue / Solid / Svelte / Angular | โŒ React-focused | โœ… | โœ… |
47
+
48
+ **Scope**: this library is deliberately React-focused and does not generate API mocks โ€” use Orval if MSW mocks are your priority, or hey-api's own plugin if you need non-React frameworks.
49
+
50
+ ## Stability policy
51
+
52
+ This library builds on [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts), which is pre-1.0 and moves fast. We **pin the exact hey-api version** and absorb its breaking changes for you: hey-api upgrades land here only after our full snapshot-test suite passes, and are released as minor versions. Your generated API surface follows SemVer โ€” breaking output changes only happen in major versions, with a migration guide.
53
+
54
+ ## Requirements
55
+
56
+ - Node.js 22.18+
57
+ - `@tanstack/react-query` 5.x (peer dependency)
58
+ - `typescript` 5.x or 6.x, `ts-morph` 28.x, `commander` 12โ€“15 (peer dependencies)
59
+
60
+ ## License
61
+
62
+ MIT
package/dist/cli.mjs CHANGED
@@ -32,6 +32,7 @@ async function setupProgram() {
32
32
  .option("--pageParam <value>", "Name of the query parameter used for pagination", "page")
33
33
  .option("--nextPageParam <value>", "Name of the response parameter used for next page", "nextPage")
34
34
  .option("--initialPageParam <value>", "Initial page value to query", "1")
35
+ .option("--omitInitialPageParam", "Send no initial page parameter at all (overrides --initialPageParam)")
35
36
  .parse();
36
37
  const options = program.opts();
37
38
  await generate(options, version);
package/dist/common.mjs CHANGED
@@ -52,6 +52,12 @@ export function BuildCommonTypeName(name) {
52
52
  * @returns The parsed number or NaN if the value is not a valid number.
53
53
  */
54
54
  export function safeParseNumber(value) {
55
+ // `Number("")` is 0, which would silently turn a blank option such as
56
+ // `--initialPageParam ""` into a numeric 0. Treat blank strings as NaN so
57
+ // callers keep the original value.
58
+ if (typeof value === "string" && value.trim() === "") {
59
+ return Number.NaN;
60
+ }
55
61
  const parsed = Number(value);
56
62
  if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
57
63
  return parsed;
@@ -5,7 +5,7 @@ import { generateAllFiles } from "./tsmorph/index.mjs";
5
5
  /**
6
6
  * Create source files using ts-morph based generation.
7
7
  */
8
- export const createSource = async ({ outputPath, client, version, pageParam, nextPageParam, initialPageParam, }) => {
8
+ export const createSource = async ({ outputPath, client, version, pageParam, nextPageParam, initialPageParam, omitInitialPageParam, }) => {
9
9
  // Initialize ts-morph project to read the generated OpenAPI client
10
10
  const project = new Project({
11
11
  skipAddingFilesFromTsConfig: true,
@@ -15,7 +15,7 @@ export const createSource = async ({ outputPath, client, version, pageParam, nex
15
15
  // Parse operations from the service file
16
16
  const operations = await parseOperations(project, pageParam);
17
17
  // Build generation context
18
- const ctx = buildGenerationContext(project, client, pageParam, nextPageParam, initialPageParam, version);
18
+ const ctx = buildGenerationContext(project, client, pageParam, nextPageParam, initialPageParam, omitInitialPageParam, version);
19
19
  // Generate all files using ts-morph
20
20
  return generateAllFiles(operations, ctx);
21
21
  };
package/dist/generate.mjs CHANGED
@@ -59,6 +59,7 @@ export async function generate(options, version) {
59
59
  pageParam: formattedOptions.pageParam,
60
60
  nextPageParam: formattedOptions.nextPageParam,
61
61
  initialPageParam: formattedOptions.initialPageParam.toString(),
62
+ omitInitialPageParam: formattedOptions.omitInitialPageParam ?? false,
62
63
  });
63
64
  await print(source, formattedOptions);
64
65
  const queriesOutputPath = buildQueriesOutputPath(options.output);
@@ -96,7 +96,7 @@ export async function parseOperations(project, pageParam) {
96
96
  /**
97
97
  * Build generation context from project configuration.
98
98
  */
99
- export function buildGenerationContext(project, client, pageParam, nextPageParam, initialPageParam, version) {
99
+ export function buildGenerationContext(project, client, pageParam, nextPageParam, initialPageParam, omitInitialPageParam, version) {
100
100
  const modelsFile = project
101
101
  .getSourceFiles()
102
102
  .find((sf) => sf.getFilePath().includes(modelsFileName));
@@ -117,6 +117,7 @@ export function buildGenerationContext(project, client, pageParam, nextPageParam
117
117
  pageParam,
118
118
  nextPageParam,
119
119
  initialPageParam,
120
+ omitInitialPageParam,
120
121
  version,
121
122
  };
122
123
  }
@@ -141,9 +141,11 @@ export function buildInfiniteClientOptionsType(op, ctx) {
141
141
  }
142
142
  /**
143
143
  * Build the infinite query key constant.
144
- * Kept distinct from the plain query key so cached InfiniteData never
145
- * collides with plain query data for the same operation (#140).
146
- * Example: export const useFindPaginatedPetsInfiniteKey = "FindPaginatedPetsInfinite";
144
+ * Shares the plain query key as its first segment so a single
145
+ * `invalidateQueries({ queryKey: [useXKey] })` matches both the plain and the
146
+ * infinite cache entries of an operation (#174), while the extra "infinite"
147
+ * segment keeps cached InfiniteData from colliding with plain query data (#140).
148
+ * Example: export const useFindPaginatedPetsInfiniteKey = [useFindPaginatedPetsKey, "infinite"] as const;
147
149
  */
148
150
  export function buildInfiniteQueryKeyConst(op) {
149
151
  return {
@@ -153,15 +155,18 @@ export function buildInfiniteQueryKeyConst(op) {
153
155
  declarations: [
154
156
  {
155
157
  name: `use${op.capitalizedMethodName}InfiniteKey`,
156
- initializer: `"${op.capitalizedMethodName}Infinite"`,
158
+ initializer: `[use${op.capitalizedMethodName}Key, "infinite"] as const`,
157
159
  },
158
160
  ],
159
161
  };
160
162
  }
161
163
  /**
162
164
  * Build the infinite query key function.
165
+ * The custom queryKey argument only replaces the params segment โ€” the
166
+ * hierarchical [opKey, "infinite"] prefix is always preserved so
167
+ * prefix-based invalidation keeps working.
163
168
  * Example: export const UseFindPaginatedPetsInfiniteKeyFn = (clientOptions: FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) =>
164
- * [useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])];
169
+ * [...useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])];
165
170
  */
166
171
  export function buildInfiniteQueryKeyFn(op) {
167
172
  const defaultValue = op.allParamsOptional ? " = {}" : "";
@@ -176,7 +181,7 @@ export function buildInfiniteQueryKeyFn(op) {
176
181
  declarations: [
177
182
  {
178
183
  name: `Use${op.capitalizedMethodName}InfiniteKeyFn`,
179
- initializer: `(${params.join(", ")}) => [use${op.capitalizedMethodName}InfiniteKey, ...(queryKey ?? [clientOptions])]`,
184
+ initializer: `(${params.join(", ")}) => [...use${op.capitalizedMethodName}InfiniteKey, ...(queryKey ?? [clientOptions])]`,
180
185
  },
181
186
  ],
182
187
  };
@@ -1,4 +1,5 @@
1
1
  import { StructureKind, VariableDeclarationKind, } from "ts-morph";
2
+ import { SDK_CALL_ARGS } from "./buildQueryHooks.mjs";
2
3
  /**
3
4
  * Get the error type string based on client type.
4
5
  */
@@ -33,7 +34,7 @@ export function buildUseMutationHook(op, ctx) {
33
34
  ? `${op.capitalizedMethodName}Data`
34
35
  : "unknown";
35
36
  const optionsType = `Options<${dataTypeName}, true>`;
36
- const mutationFn = `clientOptions => ${op.methodName}(clientOptions) as unknown as Promise<TData>`;
37
+ const mutationFn = `clientOptions => ${op.methodName}(${SDK_CALL_ARGS}) as unknown as Promise<TData>`;
37
38
  const body = `useMutation<TData, TError, ${optionsType}, TContext>({ mutationKey: Common.Use${op.capitalizedMethodName}KeyFn(mutationKey), mutationFn: ${mutationFn}, ...options })`;
38
39
  return {
39
40
  kind: StructureKind.VariableStatement,
@@ -13,32 +13,6 @@ function getErrorType(op, ctx) {
13
13
  }
14
14
  return errorType;
15
15
  }
16
- /**
17
- * Get the data type based on hook type.
18
- */
19
- function getDataTypeDefault(op, hookType) {
20
- const baseType = `Common.${op.capitalizedMethodName}DefaultResponse`;
21
- if (hookType === "useSuspenseQuery") {
22
- return `NonNullable<${baseType}>`;
23
- }
24
- if (hookType === "useInfiniteQuery") {
25
- return `InfiniteData<${baseType}>`;
26
- }
27
- return baseType;
28
- }
29
- /**
30
- * Get the options type name.
31
- */
32
- function getOptionsTypeName(hookType) {
33
- switch (hookType) {
34
- case "useSuspenseQuery":
35
- return "UseSuspenseQueryOptions";
36
- case "useInfiniteQuery":
37
- return "UseInfiniteQueryOptions";
38
- default:
39
- return "UseQueryOptions";
40
- }
41
- }
42
16
  /**
43
17
  * Resolve the generated Data type name for an operation, falling back to
44
18
  * unknown when the operation has no generated Data type.
@@ -48,13 +22,18 @@ export function getDataTypeName(op, ctx) {
48
22
  ? `${op.capitalizedMethodName}Data`
49
23
  : "unknown";
50
24
  }
25
+ /**
26
+ * SDK call arguments shared by every generated queryFn/mutationFn.
27
+ * throwOnError: true forces the SDK call to reject on error responses; the
28
+ * hey-api runtime default is false, which would resolve undefined data and
29
+ * swallow the error instead of surfacing it to TanStack Query (#172).
30
+ */
31
+ export const SDK_CALL_ARGS = "{ ...clientOptions, throwOnError: true }";
51
32
  /**
52
33
  * Build the client options parameter string.
53
34
  */
54
35
  export function buildClientOptionsParam(op, ctx) {
55
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
56
- ? `${op.capitalizedMethodName}Data`
57
- : "unknown";
36
+ const dataTypeName = getDataTypeName(op, ctx);
58
37
  const hasParams = op.parameters.length > 0;
59
38
  if (!hasParams) {
60
39
  return `clientOptions: Options<${dataTypeName}, true> = {}`;
@@ -62,6 +41,71 @@ export function buildClientOptionsParam(op, ctx) {
62
41
  const defaultValue = op.allParamsOptional ? " = {}" : "";
63
42
  return `clientOptions: Options<${dataTypeName}, true>${defaultValue}`;
64
43
  }
44
+ /**
45
+ * Build the clientOptions parameter typed with the page-less infinite
46
+ * options type โ€” the page parameter is supplied by TanStack Query's
47
+ * pageParam mechanism.
48
+ */
49
+ export function buildInfiniteClientOptionsParam(op) {
50
+ const defaultValue = op.allParamsOptional ? " = {}" : "";
51
+ return `clientOptions: Common.${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`;
52
+ }
53
+ /**
54
+ * Build the paginated SDK call shared by every infinite query builder.
55
+ */
56
+ export function buildPagedQueryFn(op, ctx, castTData) {
57
+ const dataTypeName = getDataTypeName(op, ctx);
58
+ const thenClause = castTData
59
+ ? ".then(response => response.data as TData) as TData"
60
+ : ".then(response => response.data)";
61
+ // When the initial page param is omitted, the first request must send no
62
+ // page param at all, so spread it in only once TanStack Query provides one.
63
+ const pageQuery = ctx.omitInitialPageParam
64
+ ? `...(pageParam === undefined ? {} : { ${ctx.pageParam}: pageParam as number })`
65
+ : `${ctx.pageParam}: pageParam as number`;
66
+ return `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${pageQuery} }, throwOnError: true } as Options<${dataTypeName}, true>)${thenClause}`;
67
+ }
68
+ /**
69
+ * Format the initialPageParam literal. Emits `undefined` when the caller opted
70
+ * to omit it (#177); otherwise a numeric literal when possible so the inferred
71
+ * pageParam type matches what getNextPageParam returns.
72
+ */
73
+ export function formatInitialPageParam(ctx) {
74
+ if (ctx.omitInitialPageParam) {
75
+ return "undefined";
76
+ }
77
+ return /^-?\d+$/.test(ctx.initialPageParam)
78
+ ? ctx.initialPageParam
79
+ : JSON.stringify(ctx.initialPageParam);
80
+ }
81
+ /**
82
+ * Build the nested type for getNextPageParam.
83
+ * E.g., "meta.next" becomes "{ meta: { next: number } }"
84
+ */
85
+ export function buildNestedNextPageType(nextPageParam) {
86
+ const segments = nextPageParam.split(".");
87
+ return segments.reduceRight((acc, segment) => {
88
+ return `{ ${segment}: ${acc} }`;
89
+ }, "number");
90
+ }
91
+ /**
92
+ * Build the getNextPageParam expression. The parameter is annotated because
93
+ * not every TanStack entry point contextually types it (prefetchInfiniteQuery
94
+ * does not, which would fail noImplicitAny).
95
+ */
96
+ export function buildGetNextPageParamExpr(ctx) {
97
+ const nestedType = buildNestedNextPageType(ctx.nextPageParam);
98
+ return `(response: unknown) => (response as ${nestedType}).${ctx.nextPageParam}`;
99
+ }
100
+ /**
101
+ * Build an options type where the pagination fields TanStack Query marks as
102
+ * required become optional overrides: the generator supplies them, and
103
+ * callers may replace them for custom pagination schemes (#156, #146).
104
+ */
105
+ export function buildOverridableInfiniteOptionsType(optionsTypeName) {
106
+ const instantiated = `${optionsTypeName}<TData, TError>`;
107
+ return `Omit<${instantiated}, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"> & Partial<Pick<${instantiated}, "initialPageParam" | "getNextPageParam">>`;
108
+ }
65
109
  /**
66
110
  * Build useQuery hook.
67
111
  * Example:
@@ -71,19 +115,16 @@ export function buildClientOptionsParam(op, ctx) {
71
115
  * options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">
72
116
  * ) => useQuery<TData, TError>({
73
117
  * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey),
74
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data as TData) as TData,
118
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data as TData) as TData,
75
119
  * ...options
76
120
  * });
77
121
  */
78
122
  export function buildUseQueryHook(op, ctx) {
79
123
  const hookName = `use${op.capitalizedMethodName}`;
80
124
  const errorType = getErrorType(op, ctx);
81
- const dataTypeDefault = getDataTypeDefault(op, "useQuery");
125
+ const dataTypeDefault = `Common.${op.capitalizedMethodName}DefaultResponse`;
82
126
  const clientOptionsParam = buildClientOptionsParam(op, ctx);
83
- const hasParams = op.parameters.length > 0;
84
- // Build the queryFn body
85
- const callArgs = hasParams ? "{ ...clientOptions }" : "{ ...clientOptions }";
86
- const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data as TData) as TData`;
127
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data as TData) as TData`;
87
128
  const body = `useQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`;
88
129
  return {
89
130
  kind: StructureKind.VariableStatement,
@@ -105,11 +146,9 @@ export function buildUseQueryHook(op, ctx) {
105
146
  export function buildUseSuspenseQueryHook(op, ctx) {
106
147
  const hookName = `use${op.capitalizedMethodName}Suspense`;
107
148
  const errorType = getErrorType(op, ctx);
108
- const dataTypeDefault = getDataTypeDefault(op, "useSuspenseQuery");
149
+ const dataTypeDefault = `NonNullable<Common.${op.capitalizedMethodName}DefaultResponse>`;
109
150
  const clientOptionsParam = buildClientOptionsParam(op, ctx);
110
- const hasParams = op.parameters.length > 0;
111
- const callArgs = hasParams ? "{ ...clientOptions }" : "{ ...clientOptions }";
112
- const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data as TData) as TData`;
151
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data as TData) as TData`;
113
152
  const body = `useSuspenseQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`;
114
153
  return {
115
154
  kind: StructureKind.VariableStatement,
@@ -126,38 +165,30 @@ export function buildUseSuspenseQueryHook(op, ctx) {
126
165
  };
127
166
  }
128
167
  /**
129
- * Build the nested type for getNextPageParam.
130
- * E.g., "meta.next" becomes "{ meta: { next: number } }"
131
- */
132
- export function buildNestedNextPageType(nextPageParam) {
133
- const segments = nextPageParam.split(".");
134
- return segments.reduceRight((acc, segment) => {
135
- return `{ ${segment}: ${acc} }`;
136
- }, "number");
137
- }
138
- /**
139
- * Build useInfiniteQuery hook.
168
+ * Build a useInfiniteQuery / useSuspenseInfiniteQuery hook. Both variants
169
+ * share the infinite query key (and therefore the cache); they differ only
170
+ * in the TanStack hook called, the options type, and the NonNullable TData
171
+ * default of the suspense variant.
140
172
  */
141
- export function buildUseInfiniteQueryHook(op, ctx) {
173
+ function buildInfiniteHook(op, ctx, suspense) {
142
174
  if (!op.isPaginatable) {
143
175
  return null;
144
176
  }
145
- const hookName = `use${op.capitalizedMethodName}Infinite`;
177
+ const hookCall = suspense ? "useSuspenseInfiniteQuery" : "useInfiniteQuery";
178
+ const optionsTypeName = suspense
179
+ ? "UseSuspenseInfiniteQueryOptions"
180
+ : "UseInfiniteQueryOptions";
181
+ const hookName = suspense
182
+ ? `use${op.capitalizedMethodName}SuspenseInfinite`
183
+ : `use${op.capitalizedMethodName}Infinite`;
146
184
  const errorType = getErrorType(op, ctx);
147
185
  const baseDataType = `Common.${op.capitalizedMethodName}DefaultResponse`;
148
- const dataTypeName = getDataTypeName(op, ctx);
149
- // Infinite queries take a dedicated options type that excludes the page
150
- // parameter โ€” it is supplied by TanStack Query's pageParam mechanism
151
- const defaultValue = op.allParamsOptional ? " = {}" : "";
152
- const clientOptionsParam = `clientOptions: Common.${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`;
153
- // Build the queryFn with pageParam handling
154
- const queryFn = `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${ctx.pageParam}: pageParam as number } } as Options<${dataTypeName}, true>).then(response => response.data as TData) as TData`;
155
- // Build getNextPageParam with nested type
156
- const nestedType = buildNestedNextPageType(ctx.nextPageParam);
157
- const getNextPageParam = `getNextPageParam: (response) => (response as ${nestedType}).${ctx.nextPageParam}`;
158
- // initialPageParam is a string literal
159
- const infiniteOptions = `initialPageParam: "${ctx.initialPageParam}", ${getNextPageParam}`;
160
- const body = `useInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`;
186
+ const dataTypeDefault = suspense
187
+ ? `InfiniteData<NonNullable<${baseDataType}>>`
188
+ : `InfiniteData<${baseDataType}>`;
189
+ const queryFn = buildPagedQueryFn(op, ctx, true);
190
+ const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`;
191
+ const body = `${hookCall}({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`;
161
192
  return {
162
193
  kind: StructureKind.VariableStatement,
163
194
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -167,33 +198,38 @@ export function buildUseInfiniteQueryHook(op, ctx) {
167
198
  declarations: [
168
199
  {
169
200
  name: hookName,
170
- initializer: `<TData = InfiniteData<${baseDataType}>, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${clientOptionsParam}, queryKey?: TQueryKey, options?: Omit<UseInfiniteQueryOptions<TData, TError>, "queryKey" | "queryFn">) => ${body}`,
201
+ initializer: `<TData = ${dataTypeDefault}, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${buildInfiniteClientOptionsParam(op)}, queryKey?: TQueryKey, options?: ${buildOverridableInfiniteOptionsType(optionsTypeName)}) => ${body}`,
171
202
  },
172
203
  ],
173
204
  };
174
205
  }
206
+ /**
207
+ * Build useInfiniteQuery hook.
208
+ */
209
+ export function buildUseInfiniteQueryHook(op, ctx) {
210
+ return buildInfiniteHook(op, ctx, false);
211
+ }
212
+ /**
213
+ * Build useSuspenseInfiniteQuery hook.
214
+ */
215
+ export function buildUseSuspenseInfiniteQueryHook(op, ctx) {
216
+ return buildInfiniteHook(op, ctx, true);
217
+ }
175
218
  /**
176
219
  * Build prefetch function.
177
220
  * Example:
178
- * export const prefetchUseFindPets = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}) =>
221
+ * export const prefetchUseFindPets = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}, options?: Omit<FetchQueryOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) =>
179
222
  * queryClient.prefetchQuery({
180
223
  * queryKey: Common.UseFindPetsKeyFn(clientOptions),
181
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data)
224
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
225
+ * ...options
182
226
  * });
183
227
  */
184
228
  export function buildPrefetchFn(op, ctx) {
185
229
  const fnName = `prefetchUse${op.capitalizedMethodName}`;
186
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
187
- ? `${op.capitalizedMethodName}Data`
188
- : "unknown";
189
- const hasParams = op.parameters.length > 0;
190
- const defaultValue = op.allParamsOptional ? " = {}" : "";
191
- const clientOptionsParam = hasParams
192
- ? `clientOptions: Options<${dataTypeName}, true>${defaultValue}`
193
- : `clientOptions: Options<${dataTypeName}, true> = {}`;
194
- const callArgs = "{ ...clientOptions }";
195
- const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data)`;
196
- const body = `queryClient.prefetchQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn} })`;
230
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`;
231
+ const optionsParam = `options?: Omit<FetchQueryOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn">`;
232
+ const body = `queryClient.prefetchQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn}, ...options })`;
197
233
  return {
198
234
  kind: StructureKind.VariableStatement,
199
235
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -203,7 +239,42 @@ export function buildPrefetchFn(op, ctx) {
203
239
  declarations: [
204
240
  {
205
241
  name: fnName,
206
- initializer: `(queryClient: QueryClient, ${clientOptionsParam}) => ${body}`,
242
+ initializer: `(queryClient: QueryClient, ${buildClientOptionsParam(op, ctx)}, ${optionsParam}) => ${body}`,
243
+ },
244
+ ],
245
+ };
246
+ }
247
+ /**
248
+ * Build prefetchInfiniteQuery function for a paginatable operation.
249
+ * Example:
250
+ * export const prefetchUseFindPaginatedPetsInfinite = (queryClient: QueryClient, clientOptions: Common.FindPaginatedPetsInfiniteClientOptions = {}, options?: Omit<FetchInfiniteQueryOptions<Common.FindPaginatedPetsDefaultResponse>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam">) =>
251
+ * queryClient.prefetchInfiniteQuery({
252
+ * queryKey: Common.UseFindPaginatedPetsInfiniteKeyFn(clientOptions),
253
+ * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam as number }, throwOnError: true } as Options<FindPaginatedPetsData, true>).then(response => response.data),
254
+ * initialPageParam: 1,
255
+ * getNextPageParam: (response: unknown) => (response as { nextPage: number }).nextPage,
256
+ * ...options
257
+ * });
258
+ */
259
+ export function buildPrefetchInfiniteQueryFn(op, ctx) {
260
+ if (!op.isPaginatable) {
261
+ return null;
262
+ }
263
+ const fnName = `prefetchUse${op.capitalizedMethodName}Infinite`;
264
+ const queryFn = buildPagedQueryFn(op, ctx, false);
265
+ const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`;
266
+ const optionsParam = `options?: Omit<FetchInfiniteQueryOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam">`;
267
+ const body = `queryClient.prefetchInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`;
268
+ return {
269
+ kind: StructureKind.VariableStatement,
270
+ // Copy the operation's JSDoc (description and @deprecated) from the SDK function
271
+ leadingTrivia: op.jsDoc ? `${op.jsDoc}\n` : undefined,
272
+ isExported: true,
273
+ declarationKind: VariableDeclarationKind.Const,
274
+ declarations: [
275
+ {
276
+ name: fnName,
277
+ initializer: `(queryClient: QueryClient, ${buildInfiniteClientOptionsParam(op)}, ${optionsParam}) => ${body}`,
207
278
  },
208
279
  ],
209
280
  };
@@ -211,25 +282,18 @@ export function buildPrefetchFn(op, ctx) {
211
282
  /**
212
283
  * Build ensureQueryData function.
213
284
  * Example:
214
- * export const ensureUseFindPetsData = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}) =>
285
+ * export const ensureUseFindPetsData = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}, options?: Omit<EnsureQueryDataOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) =>
215
286
  * queryClient.ensureQueryData({
216
287
  * queryKey: Common.UseFindPetsKeyFn(clientOptions),
217
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data)
288
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
289
+ * ...options
218
290
  * });
219
291
  */
220
292
  export function buildEnsureQueryDataFn(op, ctx) {
221
293
  const fnName = `ensureUse${op.capitalizedMethodName}Data`;
222
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
223
- ? `${op.capitalizedMethodName}Data`
224
- : "unknown";
225
- const hasParams = op.parameters.length > 0;
226
- const defaultValue = op.allParamsOptional ? " = {}" : "";
227
- const clientOptionsParam = hasParams
228
- ? `clientOptions: Options<${dataTypeName}, true>${defaultValue}`
229
- : `clientOptions: Options<${dataTypeName}, true> = {}`;
230
- const callArgs = "{ ...clientOptions }";
231
- const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data)`;
232
- const body = `queryClient.ensureQueryData({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn} })`;
294
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`;
295
+ const optionsParam = `options?: Omit<EnsureQueryDataOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn">`;
296
+ const body = `queryClient.ensureQueryData({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn}, ...options })`;
233
297
  return {
234
298
  kind: StructureKind.VariableStatement,
235
299
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -239,7 +303,7 @@ export function buildEnsureQueryDataFn(op, ctx) {
239
303
  declarations: [
240
304
  {
241
305
  name: fnName,
242
- initializer: `(queryClient: QueryClient, ${clientOptionsParam}) => ${body}`,
306
+ initializer: `(queryClient: QueryClient, ${buildClientOptionsParam(op, ctx)}, ${optionsParam}) => ${body}`,
243
307
  },
244
308
  ],
245
309
  };
@@ -1,5 +1,5 @@
1
1
  import { StructureKind, VariableDeclarationKind, } from "ts-morph";
2
- import { buildClientOptionsParam, buildNestedNextPageType, getDataTypeName, } from "./buildQueryHooks.mjs";
2
+ import { buildClientOptionsParam, buildGetNextPageParamExpr, buildInfiniteClientOptionsParam, buildPagedQueryFn, formatInitialPageParam, SDK_CALL_ARGS, } from "./buildQueryHooks.mjs";
3
3
  /**
4
4
  * Build a queryOptions factory for a GET operation.
5
5
  * The factory centralizes queryKey and queryFn so they can be reused with
@@ -9,13 +9,13 @@ import { buildClientOptionsParam, buildNestedNextPageType, getDataTypeName, } fr
9
9
  * export const findPetsOptions = (clientOptions: Options<FindPetsData, true> = {}, queryKey?: Array<unknown>) =>
10
10
  * queryOptions({
11
11
  * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey),
12
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data),
12
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
13
13
  * });
14
14
  */
15
15
  export function buildQueryOptionsFn(op, ctx) {
16
16
  const fnName = `${op.methodName}Options`;
17
17
  const clientOptionsParam = buildClientOptionsParam(op, ctx);
18
- const queryFn = `() => ${op.methodName}({ ...clientOptions }).then(response => response.data)`;
18
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`;
19
19
  const body = `queryOptions({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn} })`;
20
20
  return {
21
21
  kind: StructureKind.VariableStatement,
@@ -38,9 +38,9 @@ export function buildQueryOptionsFn(op, ctx) {
38
38
  * export const findPaginatedPetsInfiniteOptions = (clientOptions: Common.FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) =>
39
39
  * infiniteQueryOptions({
40
40
  * queryKey: Common.UseFindPaginatedPetsInfiniteKeyFn(clientOptions, queryKey),
41
- * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam } } as Options<FindPaginatedPetsData, true>).then(response => response.data),
41
+ * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam as number }, throwOnError: true } as Options<FindPaginatedPetsData, true>).then(response => response.data),
42
42
  * initialPageParam: 1,
43
- * getNextPageParam: (response) => (response as { nextPage: number }).nextPage,
43
+ * getNextPageParam: (response: unknown) => (response as { nextPage: number }).nextPage,
44
44
  * });
45
45
  */
46
46
  export function buildInfiniteQueryOptionsFn(op, ctx) {
@@ -48,18 +48,9 @@ export function buildInfiniteQueryOptionsFn(op, ctx) {
48
48
  return null;
49
49
  }
50
50
  const fnName = `${op.methodName}InfiniteOptions`;
51
- const dataTypeName = getDataTypeName(op, ctx);
52
- const defaultValue = op.allParamsOptional ? " = {}" : "";
53
- const clientOptionsParam = `clientOptions: Common.${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`;
54
- const queryFn = `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${ctx.pageParam}: pageParam } } as Options<${dataTypeName}, true>).then(response => response.data)`;
55
- // Emit a numeric literal when possible so the inferred pageParam type
56
- // matches what getNextPageParam returns
57
- const initialPageParam = /^-?\d+$/.test(ctx.initialPageParam)
58
- ? ctx.initialPageParam
59
- : JSON.stringify(ctx.initialPageParam);
60
- const nestedType = buildNestedNextPageType(ctx.nextPageParam);
61
- const getNextPageParam = `(response) => (response as ${nestedType}).${ctx.nextPageParam}`;
62
- const body = `infiniteQueryOptions({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, initialPageParam: ${initialPageParam}, getNextPageParam: ${getNextPageParam} })`;
51
+ const queryFn = buildPagedQueryFn(op, ctx, false);
52
+ const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`;
53
+ const body = `infiniteQueryOptions({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions} })`;
63
54
  return {
64
55
  kind: StructureKind.VariableStatement,
65
56
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -69,7 +60,7 @@ export function buildInfiniteQueryOptionsFn(op, ctx) {
69
60
  declarations: [
70
61
  {
71
62
  name: fnName,
72
- initializer: `(${clientOptionsParam}, queryKey?: Array<unknown>) => ${body}`,
63
+ initializer: `(${buildInfiniteClientOptionsParam(op)}, queryKey?: Array<unknown>) => ${body}`,
73
64
  },
74
65
  ],
75
66
  };
@@ -1,8 +1,7 @@
1
- import { StructureKind, } from "ts-morph";
2
1
  import { OpenApiRqFiles } from "../constants.mjs";
3
2
  import { buildDefaultResponseType, buildInfiniteClientOptionsType, buildInfiniteQueryKeyConst, buildInfiniteQueryKeyFn, buildMutationKeyConst, buildMutationKeyFn, buildMutationResultType, buildQueryKeyConst, buildQueryKeyFn, buildQueryResultType, } from "./buildCommon.mjs";
4
3
  import { buildUseMutationHook } from "./buildMutationHooks.mjs";
5
- import { buildEnsureQueryDataFn, buildPrefetchFn, buildUseInfiniteQueryHook, buildUseQueryHook, buildUseSuspenseQueryHook, } from "./buildQueryHooks.mjs";
4
+ import { buildEnsureQueryDataFn, buildPrefetchFn, buildPrefetchInfiniteQueryFn, buildUseInfiniteQueryHook, buildUseQueryHook, buildUseSuspenseInfiniteQueryHook, buildUseSuspenseQueryHook, } from "./buildQueryHooks.mjs";
6
5
  import { buildInfiniteQueryOptionsFn, buildQueryOptionsFn, } from "./buildQueryOptions.mjs";
7
6
  import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildQueryOptionsImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs";
8
7
  /**
@@ -31,26 +30,10 @@ function buildHookFileImports(ctx) {
31
30
  }
32
31
  /**
33
32
  * Generate the index.ts file content.
33
+ * The content is constant, so no ts-morph project is needed.
34
34
  */
35
- function generateIndexFile(ctx) {
36
- const project = createGenerationProject();
37
- const sourceFile = project.createSourceFile(`${OpenApiRqFiles.index}.ts`, undefined, { overwrite: true });
38
- const exports = [
39
- {
40
- kind: StructureKind.ExportDeclaration,
41
- moduleSpecifier: "./common",
42
- },
43
- {
44
- kind: StructureKind.ExportDeclaration,
45
- moduleSpecifier: "./queries",
46
- },
47
- {
48
- kind: StructureKind.ExportDeclaration,
49
- moduleSpecifier: "./queryOptions",
50
- },
51
- ];
52
- sourceFile.addExportDeclarations(exports);
53
- return sourceFile.getFullText();
35
+ function generateIndexFile() {
36
+ return `export * from "./common";\nexport * from "./queries";\nexport * from "./queryOptions";\n`;
54
37
  }
55
38
  /**
56
39
  * Generate the common.ts file content.
@@ -150,6 +133,13 @@ function generateSuspenseFile(operations, ctx) {
150
133
  for (const op of getOperations) {
151
134
  sourceFile.addVariableStatement(buildUseSuspenseQueryHook(op, ctx));
152
135
  }
136
+ // Add useSuspenseInfiniteQuery hooks for paginatable operations
137
+ for (const op of getOperations.filter((o) => o.isPaginatable)) {
138
+ const hook = buildUseSuspenseInfiniteQueryHook(op, ctx);
139
+ if (hook) {
140
+ sourceFile.addVariableStatement(hook);
141
+ }
142
+ }
153
143
  return sourceFile.getFullText();
154
144
  }
155
145
  /**
@@ -185,6 +175,13 @@ function generatePrefetchFile(operations, ctx) {
185
175
  for (const op of getOperations) {
186
176
  sourceFile.addVariableStatement(buildPrefetchFn(op, ctx));
187
177
  }
178
+ // Add prefetchInfiniteQuery functions for paginatable operations
179
+ for (const op of getOperations.filter((o) => o.isPaginatable)) {
180
+ const fn = buildPrefetchInfiniteQueryFn(op, ctx);
181
+ if (fn) {
182
+ sourceFile.addVariableStatement(fn);
183
+ }
184
+ }
188
185
  return sourceFile.getFullText();
189
186
  }
190
187
  /**
@@ -217,7 +214,7 @@ export function generateAllFiles(operations, ctx) {
217
214
  return [
218
215
  {
219
216
  name: `${OpenApiRqFiles.index}.ts`,
220
- content: addHeaderComment(generateIndexFile(ctx), ctx.version),
217
+ content: addHeaderComment(generateIndexFile(), ctx.version),
221
218
  },
222
219
  {
223
220
  name: `${OpenApiRqFiles.common}.ts`,
@@ -1,5 +1,5 @@
1
- export { generateAllFiles } from "./generateFiles.mjs";
2
- export { createGenerationProject } from "./projectFactory.mjs";
3
1
  export * from "./buildCommon.mjs";
4
- export * from "./buildQueryHooks.mjs";
5
2
  export * from "./buildMutationHooks.mjs";
3
+ export * from "./buildQueryHooks.mjs";
4
+ export { generateAllFiles } from "./generateFiles.mjs";
5
+ export { createGenerationProject } from "./projectFactory.mjs";
@@ -41,14 +41,19 @@ export function buildQueryImport() {
41
41
  { name: "useQuery" },
42
42
  { name: "useSuspenseQuery" },
43
43
  { name: "useInfiniteQuery" },
44
+ { name: "useSuspenseInfiniteQuery" },
44
45
  { name: "useMutation" },
45
46
  { name: "UseQueryResult" },
46
47
  { name: "UseQueryOptions" },
47
48
  { name: "UseInfiniteQueryOptions" },
49
+ { name: "UseSuspenseInfiniteQueryOptions" },
48
50
  { name: "UseMutationOptions" },
49
51
  { name: "UseMutationResult" },
50
52
  { name: "UseSuspenseQueryOptions" },
51
53
  { name: "InfiniteData" },
54
+ { name: "FetchQueryOptions", isTypeOnly: true },
55
+ { name: "FetchInfiniteQueryOptions", isTypeOnly: true },
56
+ { name: "EnsureQueryDataOptions", isTypeOnly: true },
52
57
  ],
53
58
  };
54
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nohe/openapi-react-query-codegen",
3
- "version": "3.0.0-beta.2",
3
+ "version": "3.0.0-beta.4",
4
4
  "description": "OpenAPI React Query Codegen",
5
5
  "bin": {
6
6
  "openapi-rq": "dist/cli.mjs"
@@ -43,20 +43,22 @@
43
43
  "cross-spawn": "^7.0.3"
44
44
  },
45
45
  "devDependencies": {
46
- "@biomejs/biome": "^1.9.3",
46
+ "@biomejs/biome": "^2.5.4",
47
47
  "@types/cross-spawn": "^6.0.6",
48
- "@types/node": "^22.7.4",
48
+ "@types/node": "^22.20.1",
49
49
  "@types/semver": "^7.7.1",
50
- "@vitest/coverage-v8": "^1.5.0",
51
- "commander": "^12.0.0",
52
- "lefthook": "^1.6.10",
53
- "rimraf": "^5.0.5",
50
+ "@vitest/coverage-v8": "^4.1.10",
51
+ "commander": "^15.0.0",
52
+ "lefthook": "^2.1.10",
53
+ "rimraf": "^6.1.3",
54
54
  "ts-morph": "^28.0.0",
55
55
  "typescript": "^6.0.3",
56
- "vitest": "^1.5.0"
56
+ "vite": "^7",
57
+ "vitest": "^4.1.10"
57
58
  },
58
59
  "peerDependencies": {
59
- "commander": "12.x",
60
+ "@tanstack/react-query": "^5.0.0",
61
+ "commander": "12.x || 13.x || 14.x || 15.x",
60
62
  "ts-morph": "28.x",
61
63
  "typescript": "5.x || 6.x"
62
64
  },