@7nohe/openapi-react-query-codegen 3.0.0-beta.1 โ†’ 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
@@ -5,6 +5,7 @@ export const serviceFileName = "sdk.gen";
5
5
  export const modelsFileName = "types.gen";
6
6
  export const OpenApiRqFiles = {
7
7
  queries: "queries",
8
+ queryOptions: "queryOptions",
8
9
  infiniteQueries: "infiniteQueries",
9
10
  common: "common",
10
11
  suspense: "suspense",
@@ -117,3 +117,72 @@ export function buildMutationKeyFn(op) {
117
117
  ],
118
118
  };
119
119
  }
120
+ /**
121
+ * Build the client options type for infinite queries.
122
+ * The page parameter is excluded because TanStack Query supplies it via the
123
+ * pageParam mechanism (#140).
124
+ * Example:
125
+ * export type FindPaginatedPetsInfiniteClientOptions = Omit<Options<FindPaginatedPetsData, true>, "query"> &
126
+ * { query?: Omit<NonNullable<FindPaginatedPetsData["query"]>, "page"> };
127
+ */
128
+ export function buildInfiniteClientOptionsType(op, ctx) {
129
+ const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
130
+ ? `${op.capitalizedMethodName}Data`
131
+ : "unknown";
132
+ const type = dataTypeName === "unknown"
133
+ ? "Options<unknown, true>"
134
+ : `Omit<Options<${dataTypeName}, true>, "query"> & { query?: Omit<NonNullable<${dataTypeName}["query"]>, "${ctx.pageParam}"> }`;
135
+ return {
136
+ kind: StructureKind.TypeAlias,
137
+ isExported: true,
138
+ name: `${op.capitalizedMethodName}InfiniteClientOptions`,
139
+ type,
140
+ };
141
+ }
142
+ /**
143
+ * Build the infinite query key constant.
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;
149
+ */
150
+ export function buildInfiniteQueryKeyConst(op) {
151
+ return {
152
+ kind: StructureKind.VariableStatement,
153
+ isExported: true,
154
+ declarationKind: VariableDeclarationKind.Const,
155
+ declarations: [
156
+ {
157
+ name: `use${op.capitalizedMethodName}InfiniteKey`,
158
+ initializer: `[use${op.capitalizedMethodName}Key, "infinite"] as const`,
159
+ },
160
+ ],
161
+ };
162
+ }
163
+ /**
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.
168
+ * Example: export const UseFindPaginatedPetsInfiniteKeyFn = (clientOptions: FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) =>
169
+ * [...useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])];
170
+ */
171
+ export function buildInfiniteQueryKeyFn(op) {
172
+ const defaultValue = op.allParamsOptional ? " = {}" : "";
173
+ const params = [
174
+ `clientOptions: ${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`,
175
+ "queryKey?: Array<unknown>",
176
+ ];
177
+ return {
178
+ kind: StructureKind.VariableStatement,
179
+ isExported: true,
180
+ declarationKind: VariableDeclarationKind.Const,
181
+ declarations: [
182
+ {
183
+ name: `Use${op.capitalizedMethodName}InfiniteKeyFn`,
184
+ initializer: `(${params.join(", ")}) => [...use${op.capitalizedMethodName}InfiniteKey, ...(queryKey ?? [clientOptions])]`,
185
+ },
186
+ ],
187
+ };
188
+ }
@@ -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,
@@ -14,38 +14,26 @@ function getErrorType(op, ctx) {
14
14
  return errorType;
15
15
  }
16
16
  /**
17
- * Get the data type based on hook type.
17
+ * Resolve the generated Data type name for an operation, falling back to
18
+ * unknown when the operation has no generated Data type.
18
19
  */
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;
20
+ export function getDataTypeName(op, ctx) {
21
+ return ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
22
+ ? `${op.capitalizedMethodName}Data`
23
+ : "unknown";
28
24
  }
29
25
  /**
30
- * Get the options type name.
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).
31
30
  */
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
- }
31
+ export const SDK_CALL_ARGS = "{ ...clientOptions, throwOnError: true }";
42
32
  /**
43
33
  * Build the client options parameter string.
44
34
  */
45
- function buildClientOptionsParam(op, ctx) {
46
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
47
- ? `${op.capitalizedMethodName}Data`
48
- : "unknown";
35
+ export function buildClientOptionsParam(op, ctx) {
36
+ const dataTypeName = getDataTypeName(op, ctx);
49
37
  const hasParams = op.parameters.length > 0;
50
38
  if (!hasParams) {
51
39
  return `clientOptions: Options<${dataTypeName}, true> = {}`;
@@ -53,6 +41,62 @@ function buildClientOptionsParam(op, ctx) {
53
41
  const defaultValue = op.allParamsOptional ? " = {}" : "";
54
42
  return `clientOptions: Options<${dataTypeName}, true>${defaultValue}`;
55
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
+ }
56
100
  /**
57
101
  * Build useQuery hook.
58
102
  * Example:
@@ -62,19 +106,16 @@ function buildClientOptionsParam(op, ctx) {
62
106
  * options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">
63
107
  * ) => useQuery<TData, TError>({
64
108
  * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey),
65
- * 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,
66
110
  * ...options
67
111
  * });
68
112
  */
69
113
  export function buildUseQueryHook(op, ctx) {
70
114
  const hookName = `use${op.capitalizedMethodName}`;
71
115
  const errorType = getErrorType(op, ctx);
72
- const dataTypeDefault = getDataTypeDefault(op, "useQuery");
116
+ const dataTypeDefault = `Common.${op.capitalizedMethodName}DefaultResponse`;
73
117
  const clientOptionsParam = buildClientOptionsParam(op, ctx);
74
- const hasParams = op.parameters.length > 0;
75
- // Build the queryFn body
76
- const callArgs = hasParams ? "{ ...clientOptions }" : "{ ...clientOptions }";
77
- 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`;
78
119
  const body = `useQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`;
79
120
  return {
80
121
  kind: StructureKind.VariableStatement,
@@ -96,11 +137,9 @@ export function buildUseQueryHook(op, ctx) {
96
137
  export function buildUseSuspenseQueryHook(op, ctx) {
97
138
  const hookName = `use${op.capitalizedMethodName}Suspense`;
98
139
  const errorType = getErrorType(op, ctx);
99
- const dataTypeDefault = getDataTypeDefault(op, "useSuspenseQuery");
140
+ const dataTypeDefault = `NonNullable<Common.${op.capitalizedMethodName}DefaultResponse>`;
100
141
  const clientOptionsParam = buildClientOptionsParam(op, ctx);
101
- const hasParams = op.parameters.length > 0;
102
- const callArgs = hasParams ? "{ ...clientOptions }" : "{ ...clientOptions }";
103
- 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`;
104
143
  const body = `useSuspenseQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`;
105
144
  return {
106
145
  kind: StructureKind.VariableStatement,
@@ -117,38 +156,30 @@ export function buildUseSuspenseQueryHook(op, ctx) {
117
156
  };
118
157
  }
119
158
  /**
120
- * Build the nested type for getNextPageParam.
121
- * 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.
122
163
  */
123
- function buildNestedNextPageType(nextPageParam) {
124
- const segments = nextPageParam.split(".");
125
- return segments.reduceRight((acc, segment) => {
126
- return `{ ${segment}: ${acc} }`;
127
- }, "number");
128
- }
129
- /**
130
- * Build useInfiniteQuery hook.
131
- */
132
- export function buildUseInfiniteQueryHook(op, ctx) {
164
+ function buildInfiniteHook(op, ctx, suspense) {
133
165
  if (!op.isPaginatable) {
134
166
  return null;
135
167
  }
136
- 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`;
137
175
  const errorType = getErrorType(op, ctx);
138
176
  const baseDataType = `Common.${op.capitalizedMethodName}DefaultResponse`;
139
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
140
- ? `${op.capitalizedMethodName}Data`
141
- : "unknown";
142
- const defaultValue = op.allParamsOptional ? " = {}" : "";
143
- const clientOptionsParam = `clientOptions: Options<${dataTypeName}, true>${defaultValue}`;
144
- // Build the queryFn with pageParam handling
145
- const queryFn = `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${ctx.pageParam}: pageParam as number } }).then(response => response.data as TData) as TData`;
146
- // Build getNextPageParam with nested type
147
- const nestedType = buildNestedNextPageType(ctx.nextPageParam);
148
- const getNextPageParam = `getNextPageParam: (response) => (response as ${nestedType}).${ctx.nextPageParam}`;
149
- // initialPageParam is a string literal
150
- const infiniteOptions = `initialPageParam: "${ctx.initialPageParam}", ${getNextPageParam}`;
151
- const body = `useInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(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 })`;
152
183
  return {
153
184
  kind: StructureKind.VariableStatement,
154
185
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -158,33 +189,38 @@ export function buildUseInfiniteQueryHook(op, ctx) {
158
189
  declarations: [
159
190
  {
160
191
  name: hookName,
161
- 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}`,
162
193
  },
163
194
  ],
164
195
  };
165
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
+ }
166
209
  /**
167
210
  * Build prefetch function.
168
211
  * Example:
169
- * 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">) =>
170
213
  * queryClient.prefetchQuery({
171
214
  * queryKey: Common.UseFindPetsKeyFn(clientOptions),
172
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data)
215
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
216
+ * ...options
173
217
  * });
174
218
  */
175
219
  export function buildPrefetchFn(op, ctx) {
176
220
  const fnName = `prefetchUse${op.capitalizedMethodName}`;
177
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
178
- ? `${op.capitalizedMethodName}Data`
179
- : "unknown";
180
- const hasParams = op.parameters.length > 0;
181
- const defaultValue = op.allParamsOptional ? " = {}" : "";
182
- const clientOptionsParam = hasParams
183
- ? `clientOptions: Options<${dataTypeName}, true>${defaultValue}`
184
- : `clientOptions: Options<${dataTypeName}, true> = {}`;
185
- const callArgs = "{ ...clientOptions }";
186
- const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data)`;
187
- 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 })`;
188
224
  return {
189
225
  kind: StructureKind.VariableStatement,
190
226
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -194,7 +230,42 @@ export function buildPrefetchFn(op, ctx) {
194
230
  declarations: [
195
231
  {
196
232
  name: fnName,
197
- 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}`,
198
269
  },
199
270
  ],
200
271
  };
@@ -202,25 +273,18 @@ export function buildPrefetchFn(op, ctx) {
202
273
  /**
203
274
  * Build ensureQueryData function.
204
275
  * Example:
205
- * 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">) =>
206
277
  * queryClient.ensureQueryData({
207
278
  * queryKey: Common.UseFindPetsKeyFn(clientOptions),
208
- * queryFn: () => findPets({ ...clientOptions }).then(response => response.data)
279
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
280
+ * ...options
209
281
  * });
210
282
  */
211
283
  export function buildEnsureQueryDataFn(op, ctx) {
212
284
  const fnName = `ensureUse${op.capitalizedMethodName}Data`;
213
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
214
- ? `${op.capitalizedMethodName}Data`
215
- : "unknown";
216
- const hasParams = op.parameters.length > 0;
217
- const defaultValue = op.allParamsOptional ? " = {}" : "";
218
- const clientOptionsParam = hasParams
219
- ? `clientOptions: Options<${dataTypeName}, true>${defaultValue}`
220
- : `clientOptions: Options<${dataTypeName}, true> = {}`;
221
- const callArgs = "{ ...clientOptions }";
222
- const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data)`;
223
- 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 })`;
224
288
  return {
225
289
  kind: StructureKind.VariableStatement,
226
290
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -230,7 +294,7 @@ export function buildEnsureQueryDataFn(op, ctx) {
230
294
  declarations: [
231
295
  {
232
296
  name: fnName,
233
- initializer: `(queryClient: QueryClient, ${clientOptionsParam}) => ${body}`,
297
+ initializer: `(queryClient: QueryClient, ${buildClientOptionsParam(op, ctx)}, ${optionsParam}) => ${body}`,
234
298
  },
235
299
  ],
236
300
  };
@@ -0,0 +1,67 @@
1
+ import { StructureKind, VariableDeclarationKind, } from "ts-morph";
2
+ import { buildClientOptionsParam, buildGetNextPageParamExpr, buildInfiniteClientOptionsParam, buildPagedQueryFn, formatInitialPageParam, SDK_CALL_ARGS, } from "./buildQueryHooks.mjs";
3
+ /**
4
+ * Build a queryOptions factory for a GET operation.
5
+ * The factory centralizes queryKey and queryFn so they can be reused with
6
+ * every TanStack Query utility (useQuery, useQueries, prefetchQuery,
7
+ * ensureQueryData, setQueryData, ...) with full type safety.
8
+ * Example:
9
+ * export const findPetsOptions = (clientOptions: Options<FindPetsData, true> = {}, queryKey?: Array<unknown>) =>
10
+ * queryOptions({
11
+ * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey),
12
+ * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data),
13
+ * });
14
+ */
15
+ export function buildQueryOptionsFn(op, ctx) {
16
+ const fnName = `${op.methodName}Options`;
17
+ const clientOptionsParam = buildClientOptionsParam(op, ctx);
18
+ const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`;
19
+ const body = `queryOptions({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn} })`;
20
+ return {
21
+ kind: StructureKind.VariableStatement,
22
+ // Copy the operation's JSDoc (description and @deprecated) from the SDK function
23
+ leadingTrivia: op.jsDoc ? `${op.jsDoc}\n` : undefined,
24
+ isExported: true,
25
+ declarationKind: VariableDeclarationKind.Const,
26
+ declarations: [
27
+ {
28
+ name: fnName,
29
+ initializer: `(${clientOptionsParam}, queryKey?: Array<unknown>) => ${body}`,
30
+ },
31
+ ],
32
+ };
33
+ }
34
+ /**
35
+ * Build an infiniteQueryOptions factory for a paginatable GET operation.
36
+ * Uses the dedicated infinite query key and page-less options type.
37
+ * Example:
38
+ * export const findPaginatedPetsInfiniteOptions = (clientOptions: Common.FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) =>
39
+ * infiniteQueryOptions({
40
+ * queryKey: Common.UseFindPaginatedPetsInfiniteKeyFn(clientOptions, queryKey),
41
+ * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam as number }, throwOnError: true } as Options<FindPaginatedPetsData, true>).then(response => response.data),
42
+ * initialPageParam: 1,
43
+ * getNextPageParam: (response: unknown) => (response as { nextPage: number }).nextPage,
44
+ * });
45
+ */
46
+ export function buildInfiniteQueryOptionsFn(op, ctx) {
47
+ if (!op.isPaginatable) {
48
+ return null;
49
+ }
50
+ const fnName = `${op.methodName}InfiniteOptions`;
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} })`;
54
+ return {
55
+ kind: StructureKind.VariableStatement,
56
+ // Copy the operation's JSDoc (description and @deprecated) from the SDK function
57
+ leadingTrivia: op.jsDoc ? `${op.jsDoc}\n` : undefined,
58
+ isExported: true,
59
+ declarationKind: VariableDeclarationKind.Const,
60
+ declarations: [
61
+ {
62
+ name: fnName,
63
+ initializer: `(${buildInfiniteClientOptionsParam(op)}, queryKey?: Array<unknown>) => ${body}`,
64
+ },
65
+ ],
66
+ };
67
+ }
@@ -1,9 +1,9 @@
1
- import { StructureKind, } from "ts-morph";
2
1
  import { OpenApiRqFiles } from "../constants.mjs";
3
- import { buildDefaultResponseType, buildMutationKeyConst, buildMutationKeyFn, buildMutationResultType, buildQueryKeyConst, buildQueryKeyFn, buildQueryResultType, } from "./buildCommon.mjs";
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";
6
- import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs";
4
+ import { buildEnsureQueryDataFn, buildPrefetchFn, buildPrefetchInfiniteQueryFn, buildUseInfiniteQueryHook, buildUseQueryHook, buildUseSuspenseInfiniteQueryHook, buildUseSuspenseQueryHook, } from "./buildQueryHooks.mjs";
5
+ import { buildInfiniteQueryOptionsFn, buildQueryOptionsFn, } from "./buildQueryOptions.mjs";
6
+ import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildQueryOptionsImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs";
7
7
  /**
8
8
  * Build imports for common.ts file.
9
9
  */
@@ -30,22 +30,10 @@ function buildHookFileImports(ctx) {
30
30
  }
31
31
  /**
32
32
  * Generate the index.ts file content.
33
+ * The content is constant, so no ts-morph project is needed.
33
34
  */
34
- function generateIndexFile(ctx) {
35
- const project = createGenerationProject();
36
- const sourceFile = project.createSourceFile(`${OpenApiRqFiles.index}.ts`, undefined, { overwrite: true });
37
- const exports = [
38
- {
39
- kind: StructureKind.ExportDeclaration,
40
- moduleSpecifier: "./common",
41
- },
42
- {
43
- kind: StructureKind.ExportDeclaration,
44
- moduleSpecifier: "./queries",
45
- },
46
- ];
47
- sourceFile.addExportDeclarations(exports);
48
- return sourceFile.getFullText();
35
+ function generateIndexFile() {
36
+ return `export * from "./common";\nexport * from "./queries";\nexport * from "./queryOptions";\n`;
49
37
  }
50
38
  /**
51
39
  * Generate the common.ts file content.
@@ -65,6 +53,12 @@ function generateCommonFile(operations, ctx) {
65
53
  sourceFile.addVariableStatement(buildQueryKeyConst(op));
66
54
  sourceFile.addVariableStatement(buildQueryKeyFn(op, ctx));
67
55
  }
56
+ // Add dedicated infinite query types and keys for paginatable operations
57
+ for (const op of getOperations.filter((o) => o.isPaginatable)) {
58
+ sourceFile.addTypeAlias(buildInfiniteClientOptionsType(op, ctx));
59
+ sourceFile.addVariableStatement(buildInfiniteQueryKeyConst(op));
60
+ sourceFile.addVariableStatement(buildInfiniteQueryKeyFn(op));
61
+ }
68
62
  // Add mutation types and keys
69
63
  for (const op of mutationOperations) {
70
64
  sourceFile.addTypeAlias(buildMutationResultType(op));
@@ -94,6 +88,37 @@ function generateQueriesFile(operations, ctx) {
94
88
  }
95
89
  return sourceFile.getFullText();
96
90
  }
91
+ /**
92
+ * Generate the queryOptions.ts file content.
93
+ */
94
+ function generateQueryOptionsFile(operations, ctx) {
95
+ const project = createGenerationProject();
96
+ const sourceFile = project.createSourceFile(`${OpenApiRqFiles.queryOptions}.ts`, undefined, { overwrite: true });
97
+ // Add imports
98
+ const imports = [
99
+ buildCommonImport(),
100
+ buildQueryOptionsImport(),
101
+ buildClientImport(ctx),
102
+ buildServiceImport(ctx),
103
+ ];
104
+ const modelImport = buildModelImport(ctx);
105
+ if (modelImport) {
106
+ imports.push(modelImport);
107
+ }
108
+ sourceFile.addImportDeclarations(imports);
109
+ // Only GET operations have query options
110
+ const getOperations = operations.filter((op) => op.httpMethod === "GET");
111
+ for (const op of getOperations) {
112
+ sourceFile.addVariableStatement(buildQueryOptionsFn(op, ctx));
113
+ }
114
+ for (const op of getOperations) {
115
+ const infiniteOptions = buildInfiniteQueryOptionsFn(op, ctx);
116
+ if (infiniteOptions) {
117
+ sourceFile.addVariableStatement(infiniteOptions);
118
+ }
119
+ }
120
+ return sourceFile.getFullText();
121
+ }
97
122
  /**
98
123
  * Generate the suspense.ts file content.
99
124
  */
@@ -108,6 +133,13 @@ function generateSuspenseFile(operations, ctx) {
108
133
  for (const op of getOperations) {
109
134
  sourceFile.addVariableStatement(buildUseSuspenseQueryHook(op, ctx));
110
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
+ }
111
143
  return sourceFile.getFullText();
112
144
  }
113
145
  /**
@@ -143,6 +175,13 @@ function generatePrefetchFile(operations, ctx) {
143
175
  for (const op of getOperations) {
144
176
  sourceFile.addVariableStatement(buildPrefetchFn(op, ctx));
145
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
+ }
146
185
  return sourceFile.getFullText();
147
186
  }
148
187
  /**
@@ -175,7 +214,7 @@ export function generateAllFiles(operations, ctx) {
175
214
  return [
176
215
  {
177
216
  name: `${OpenApiRqFiles.index}.ts`,
178
- content: addHeaderComment(generateIndexFile(ctx), ctx.version),
217
+ content: addHeaderComment(generateIndexFile(), ctx.version),
179
218
  },
180
219
  {
181
220
  name: `${OpenApiRqFiles.common}.ts`,
@@ -185,6 +224,10 @@ export function generateAllFiles(operations, ctx) {
185
224
  name: `${OpenApiRqFiles.queries}.ts`,
186
225
  content: addHeaderComment(generateQueriesFile(operations, ctx), ctx.version),
187
226
  },
227
+ {
228
+ name: `${OpenApiRqFiles.queryOptions}.ts`,
229
+ content: addHeaderComment(generateQueryOptionsFile(operations, ctx), ctx.version),
230
+ },
188
231
  {
189
232
  name: `${OpenApiRqFiles.suspense}.ts`,
190
233
  content: addHeaderComment(generateSuspenseFile(operations, ctx), ctx.version),
@@ -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,17 +41,32 @@ 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
  }
60
+ /**
61
+ * Build import structure for the queryOptions/infiniteQueryOptions helpers.
62
+ */
63
+ export function buildQueryOptionsImport() {
64
+ return {
65
+ kind: StructureKind.ImportDeclaration,
66
+ moduleSpecifier: "@tanstack/react-query",
67
+ namedImports: [{ name: "queryOptions" }, { name: "infiniteQueryOptions" }],
68
+ };
69
+ }
55
70
  /**
56
71
  * Build import structure for services.
57
72
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nohe/openapi-react-query-codegen",
3
- "version": "3.0.0-beta.1",
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
  },