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

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
@@ -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,62 @@ 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
+ return `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${ctx.pageParam}: pageParam as number }, throwOnError: true } as Options<${dataTypeName}, true>)${thenClause}`;
62
+ }
63
+ /**
64
+ * Format the initialPageParam literal. Emits a numeric literal when possible
65
+ * so the inferred pageParam type matches what getNextPageParam returns.
66
+ */
67
+ export function formatInitialPageParam(ctx) {
68
+ return /^-?\d+$/.test(ctx.initialPageParam)
69
+ ? ctx.initialPageParam
70
+ : JSON.stringify(ctx.initialPageParam);
71
+ }
72
+ /**
73
+ * Build the nested type for getNextPageParam.
74
+ * E.g., "meta.next" becomes "{ meta: { next: number } }"
75
+ */
76
+ export function buildNestedNextPageType(nextPageParam) {
77
+ const segments = nextPageParam.split(".");
78
+ return segments.reduceRight((acc, segment) => {
79
+ return `{ ${segment}: ${acc} }`;
80
+ }, "number");
81
+ }
82
+ /**
83
+ * Build the getNextPageParam expression. The parameter is annotated because
84
+ * not every TanStack entry point contextually types it (prefetchInfiniteQuery
85
+ * does not, which would fail noImplicitAny).
86
+ */
87
+ export function buildGetNextPageParamExpr(ctx) {
88
+ const nestedType = buildNestedNextPageType(ctx.nextPageParam);
89
+ return `(response: unknown) => (response as ${nestedType}).${ctx.nextPageParam}`;
90
+ }
91
+ /**
92
+ * Build an options type where the pagination fields TanStack Query marks as
93
+ * required become optional overrides: the generator supplies them, and
94
+ * callers may replace them for custom pagination schemes (#156, #146).
95
+ */
96
+ export function buildOverridableInfiniteOptionsType(optionsTypeName) {
97
+ const instantiated = `${optionsTypeName}<TData, TError>`;
98
+ return `Omit<${instantiated}, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"> & Partial<Pick<${instantiated}, "initialPageParam" | "getNextPageParam">>`;
99
+ }
65
100
  /**
66
101
  * Build useQuery hook.
67
102
  * Example:
@@ -71,19 +106,16 @@ export function buildClientOptionsParam(op, ctx) {
71
106
  * options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">
72
107
  * ) => useQuery<TData, TError>({
73
108
  * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey),
74
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data as TData) as TData,
109
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data as TData) as TData,
75
110
  * ...options
76
111
  * });
77
112
  */
78
113
  export function buildUseQueryHook(op, ctx) {
79
114
  const hookName = `use${op.capitalizedMethodName}`;
80
115
  const errorType = getErrorType(op, ctx);
81
- const dataTypeDefault = getDataTypeDefault(op, "useQuery");
116
+ const dataTypeDefault = `Common.${op.capitalizedMethodName}DefaultResponse`;
82
117
  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`;
118
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data as TData) as TData`;
87
119
  const body = `useQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`;
88
120
  return {
89
121
  kind: StructureKind.VariableStatement,
@@ -105,11 +137,9 @@ export function buildUseQueryHook(op, ctx) {
105
137
  export function buildUseSuspenseQueryHook(op, ctx) {
106
138
  const hookName = `use${op.capitalizedMethodName}Suspense`;
107
139
  const errorType = getErrorType(op, ctx);
108
- const dataTypeDefault = getDataTypeDefault(op, "useSuspenseQuery");
140
+ const dataTypeDefault = `NonNullable<Common.${op.capitalizedMethodName}DefaultResponse>`;
109
141
  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`;
142
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data as TData) as TData`;
113
143
  const body = `useSuspenseQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`;
114
144
  return {
115
145
  kind: StructureKind.VariableStatement,
@@ -126,38 +156,30 @@ export function buildUseSuspenseQueryHook(op, ctx) {
126
156
  };
127
157
  }
128
158
  /**
129
- * Build the nested type for getNextPageParam.
130
- * E.g., "meta.next" becomes "{ meta: { next: number } }"
159
+ * Build a useInfiniteQuery / useSuspenseInfiniteQuery hook. Both variants
160
+ * share the infinite query key (and therefore the cache); they differ only
161
+ * in the TanStack hook called, the options type, and the NonNullable TData
162
+ * default of the suspense variant.
131
163
  */
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.
140
- */
141
- export function buildUseInfiniteQueryHook(op, ctx) {
164
+ function buildInfiniteHook(op, ctx, suspense) {
142
165
  if (!op.isPaginatable) {
143
166
  return null;
144
167
  }
145
- const hookName = `use${op.capitalizedMethodName}Infinite`;
168
+ const hookCall = suspense ? "useSuspenseInfiniteQuery" : "useInfiniteQuery";
169
+ const optionsTypeName = suspense
170
+ ? "UseSuspenseInfiniteQueryOptions"
171
+ : "UseInfiniteQueryOptions";
172
+ const hookName = suspense
173
+ ? `use${op.capitalizedMethodName}SuspenseInfinite`
174
+ : `use${op.capitalizedMethodName}Infinite`;
146
175
  const errorType = getErrorType(op, ctx);
147
176
  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 })`;
177
+ const dataTypeDefault = suspense
178
+ ? `InfiniteData<NonNullable<${baseDataType}>>`
179
+ : `InfiniteData<${baseDataType}>`;
180
+ const queryFn = buildPagedQueryFn(op, ctx, true);
181
+ const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`;
182
+ const body = `${hookCall}({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`;
161
183
  return {
162
184
  kind: StructureKind.VariableStatement,
163
185
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -167,33 +189,38 @@ export function buildUseInfiniteQueryHook(op, ctx) {
167
189
  declarations: [
168
190
  {
169
191
  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}`,
192
+ initializer: `<TData = ${dataTypeDefault}, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${buildInfiniteClientOptionsParam(op)}, queryKey?: TQueryKey, options?: ${buildOverridableInfiniteOptionsType(optionsTypeName)}) => ${body}`,
171
193
  },
172
194
  ],
173
195
  };
174
196
  }
197
+ /**
198
+ * Build useInfiniteQuery hook.
199
+ */
200
+ export function buildUseInfiniteQueryHook(op, ctx) {
201
+ return buildInfiniteHook(op, ctx, false);
202
+ }
203
+ /**
204
+ * Build useSuspenseInfiniteQuery hook.
205
+ */
206
+ export function buildUseSuspenseInfiniteQueryHook(op, ctx) {
207
+ return buildInfiniteHook(op, ctx, true);
208
+ }
175
209
  /**
176
210
  * Build prefetch function.
177
211
  * Example:
178
- * export const prefetchUseFindPets = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}) =>
212
+ * export const prefetchUseFindPets = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}, options?: Omit<FetchQueryOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) =>
179
213
  * queryClient.prefetchQuery({
180
214
  * queryKey: Common.UseFindPetsKeyFn(clientOptions),
181
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data)
215
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
216
+ * ...options
182
217
  * });
183
218
  */
184
219
  export function buildPrefetchFn(op, ctx) {
185
220
  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} })`;
221
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`;
222
+ const optionsParam = `options?: Omit<FetchQueryOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn">`;
223
+ const body = `queryClient.prefetchQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn}, ...options })`;
197
224
  return {
198
225
  kind: StructureKind.VariableStatement,
199
226
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -203,7 +230,42 @@ export function buildPrefetchFn(op, ctx) {
203
230
  declarations: [
204
231
  {
205
232
  name: fnName,
206
- initializer: `(queryClient: QueryClient, ${clientOptionsParam}) => ${body}`,
233
+ initializer: `(queryClient: QueryClient, ${buildClientOptionsParam(op, ctx)}, ${optionsParam}) => ${body}`,
234
+ },
235
+ ],
236
+ };
237
+ }
238
+ /**
239
+ * Build prefetchInfiniteQuery function for a paginatable operation.
240
+ * Example:
241
+ * export const prefetchUseFindPaginatedPetsInfinite = (queryClient: QueryClient, clientOptions: Common.FindPaginatedPetsInfiniteClientOptions = {}, options?: Omit<FetchInfiniteQueryOptions<Common.FindPaginatedPetsDefaultResponse>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam">) =>
242
+ * queryClient.prefetchInfiniteQuery({
243
+ * queryKey: Common.UseFindPaginatedPetsInfiniteKeyFn(clientOptions),
244
+ * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam as number }, throwOnError: true } as Options<FindPaginatedPetsData, true>).then(response => response.data),
245
+ * initialPageParam: 1,
246
+ * getNextPageParam: (response: unknown) => (response as { nextPage: number }).nextPage,
247
+ * ...options
248
+ * });
249
+ */
250
+ export function buildPrefetchInfiniteQueryFn(op, ctx) {
251
+ if (!op.isPaginatable) {
252
+ return null;
253
+ }
254
+ const fnName = `prefetchUse${op.capitalizedMethodName}Infinite`;
255
+ const queryFn = buildPagedQueryFn(op, ctx, false);
256
+ const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`;
257
+ const optionsParam = `options?: Omit<FetchInfiniteQueryOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam">`;
258
+ const body = `queryClient.prefetchInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`;
259
+ return {
260
+ kind: StructureKind.VariableStatement,
261
+ // Copy the operation's JSDoc (description and @deprecated) from the SDK function
262
+ leadingTrivia: op.jsDoc ? `${op.jsDoc}\n` : undefined,
263
+ isExported: true,
264
+ declarationKind: VariableDeclarationKind.Const,
265
+ declarations: [
266
+ {
267
+ name: fnName,
268
+ initializer: `(queryClient: QueryClient, ${buildInfiniteClientOptionsParam(op)}, ${optionsParam}) => ${body}`,
207
269
  },
208
270
  ],
209
271
  };
@@ -211,25 +273,18 @@ export function buildPrefetchFn(op, ctx) {
211
273
  /**
212
274
  * Build ensureQueryData function.
213
275
  * Example:
214
- * export const ensureUseFindPetsData = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}) =>
276
+ * export const ensureUseFindPetsData = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}, options?: Omit<EnsureQueryDataOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) =>
215
277
  * queryClient.ensureQueryData({
216
278
  * queryKey: Common.UseFindPetsKeyFn(clientOptions),
217
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data)
279
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
280
+ * ...options
218
281
  * });
219
282
  */
220
283
  export function buildEnsureQueryDataFn(op, ctx) {
221
284
  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} })`;
285
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`;
286
+ const optionsParam = `options?: Omit<EnsureQueryDataOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn">`;
287
+ const body = `queryClient.ensureQueryData({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn}, ...options })`;
233
288
  return {
234
289
  kind: StructureKind.VariableStatement,
235
290
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -239,7 +294,7 @@ export function buildEnsureQueryDataFn(op, ctx) {
239
294
  declarations: [
240
295
  {
241
296
  name: fnName,
242
- initializer: `(queryClient: QueryClient, ${clientOptionsParam}) => ${body}`,
297
+ initializer: `(queryClient: QueryClient, ${buildClientOptionsParam(op, ctx)}, ${optionsParam}) => ${body}`,
243
298
  },
244
299
  ],
245
300
  };
@@ -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.3",
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
  },