@7nohe/openapi-react-query-codegen 3.0.0-beta.1 → 3.0.0-beta.2

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.
@@ -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,67 @@ 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
+ * 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";
147
+ */
148
+ export function buildInfiniteQueryKeyConst(op) {
149
+ return {
150
+ kind: StructureKind.VariableStatement,
151
+ isExported: true,
152
+ declarationKind: VariableDeclarationKind.Const,
153
+ declarations: [
154
+ {
155
+ name: `use${op.capitalizedMethodName}InfiniteKey`,
156
+ initializer: `"${op.capitalizedMethodName}Infinite"`,
157
+ },
158
+ ],
159
+ };
160
+ }
161
+ /**
162
+ * Build the infinite query key function.
163
+ * Example: export const UseFindPaginatedPetsInfiniteKeyFn = (clientOptions: FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) =>
164
+ * [useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])];
165
+ */
166
+ export function buildInfiniteQueryKeyFn(op) {
167
+ const defaultValue = op.allParamsOptional ? " = {}" : "";
168
+ const params = [
169
+ `clientOptions: ${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`,
170
+ "queryKey?: Array<unknown>",
171
+ ];
172
+ return {
173
+ kind: StructureKind.VariableStatement,
174
+ isExported: true,
175
+ declarationKind: VariableDeclarationKind.Const,
176
+ declarations: [
177
+ {
178
+ name: `Use${op.capitalizedMethodName}InfiniteKeyFn`,
179
+ initializer: `(${params.join(", ")}) => [use${op.capitalizedMethodName}InfiniteKey, ...(queryKey ?? [clientOptions])]`,
180
+ },
181
+ ],
182
+ };
183
+ }
@@ -39,10 +39,19 @@ function getOptionsTypeName(hookType) {
39
39
  return "UseQueryOptions";
40
40
  }
41
41
  }
42
+ /**
43
+ * Resolve the generated Data type name for an operation, falling back to
44
+ * unknown when the operation has no generated Data type.
45
+ */
46
+ export function getDataTypeName(op, ctx) {
47
+ return ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
48
+ ? `${op.capitalizedMethodName}Data`
49
+ : "unknown";
50
+ }
42
51
  /**
43
52
  * Build the client options parameter string.
44
53
  */
45
- function buildClientOptionsParam(op, ctx) {
54
+ export function buildClientOptionsParam(op, ctx) {
46
55
  const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
47
56
  ? `${op.capitalizedMethodName}Data`
48
57
  : "unknown";
@@ -120,7 +129,7 @@ export function buildUseSuspenseQueryHook(op, ctx) {
120
129
  * Build the nested type for getNextPageParam.
121
130
  * E.g., "meta.next" becomes "{ meta: { next: number } }"
122
131
  */
123
- function buildNestedNextPageType(nextPageParam) {
132
+ export function buildNestedNextPageType(nextPageParam) {
124
133
  const segments = nextPageParam.split(".");
125
134
  return segments.reduceRight((acc, segment) => {
126
135
  return `{ ${segment}: ${acc} }`;
@@ -136,19 +145,19 @@ export function buildUseInfiniteQueryHook(op, ctx) {
136
145
  const hookName = `use${op.capitalizedMethodName}Infinite`;
137
146
  const errorType = getErrorType(op, ctx);
138
147
  const baseDataType = `Common.${op.capitalizedMethodName}DefaultResponse`;
139
- const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`)
140
- ? `${op.capitalizedMethodName}Data`
141
- : "unknown";
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
142
151
  const defaultValue = op.allParamsOptional ? " = {}" : "";
143
- const clientOptionsParam = `clientOptions: Options<${dataTypeName}, true>${defaultValue}`;
152
+ const clientOptionsParam = `clientOptions: Common.${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`;
144
153
  // 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`;
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`;
146
155
  // Build getNextPageParam with nested type
147
156
  const nestedType = buildNestedNextPageType(ctx.nextPageParam);
148
157
  const getNextPageParam = `getNextPageParam: (response) => (response as ${nestedType}).${ctx.nextPageParam}`;
149
158
  // initialPageParam is a string literal
150
159
  const infiniteOptions = `initialPageParam: "${ctx.initialPageParam}", ${getNextPageParam}`;
151
- const body = `useInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`;
160
+ const body = `useInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`;
152
161
  return {
153
162
  kind: StructureKind.VariableStatement,
154
163
  // Copy the operation's JSDoc (description and @deprecated) from the SDK function
@@ -0,0 +1,76 @@
1
+ import { StructureKind, VariableDeclarationKind, } from "ts-morph";
2
+ import { buildClientOptionsParam, buildNestedNextPageType, getDataTypeName, } 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 }).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}({ ...clientOptions }).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 Options<FindPaginatedPetsData, true>).then(response => response.data),
42
+ * initialPageParam: 1,
43
+ * getNextPageParam: (response) => (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 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} })`;
63
+ return {
64
+ kind: StructureKind.VariableStatement,
65
+ // Copy the operation's JSDoc (description and @deprecated) from the SDK function
66
+ leadingTrivia: op.jsDoc ? `${op.jsDoc}\n` : undefined,
67
+ isExported: true,
68
+ declarationKind: VariableDeclarationKind.Const,
69
+ declarations: [
70
+ {
71
+ name: fnName,
72
+ initializer: `(${clientOptionsParam}, queryKey?: Array<unknown>) => ${body}`,
73
+ },
74
+ ],
75
+ };
76
+ }
@@ -1,9 +1,10 @@
1
1
  import { StructureKind, } from "ts-morph";
2
2
  import { OpenApiRqFiles } from "../constants.mjs";
3
- import { buildDefaultResponseType, buildMutationKeyConst, buildMutationKeyFn, buildMutationResultType, buildQueryKeyConst, buildQueryKeyFn, buildQueryResultType, } from "./buildCommon.mjs";
3
+ import { buildDefaultResponseType, buildInfiniteClientOptionsType, buildInfiniteQueryKeyConst, buildInfiniteQueryKeyFn, buildMutationKeyConst, buildMutationKeyFn, buildMutationResultType, buildQueryKeyConst, buildQueryKeyFn, buildQueryResultType, } from "./buildCommon.mjs";
4
4
  import { buildUseMutationHook } from "./buildMutationHooks.mjs";
5
5
  import { buildEnsureQueryDataFn, buildPrefetchFn, buildUseInfiniteQueryHook, buildUseQueryHook, buildUseSuspenseQueryHook, } from "./buildQueryHooks.mjs";
6
- import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs";
6
+ import { buildInfiniteQueryOptionsFn, buildQueryOptionsFn, } from "./buildQueryOptions.mjs";
7
+ import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildQueryOptionsImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs";
7
8
  /**
8
9
  * Build imports for common.ts file.
9
10
  */
@@ -43,6 +44,10 @@ function generateIndexFile(ctx) {
43
44
  kind: StructureKind.ExportDeclaration,
44
45
  moduleSpecifier: "./queries",
45
46
  },
47
+ {
48
+ kind: StructureKind.ExportDeclaration,
49
+ moduleSpecifier: "./queryOptions",
50
+ },
46
51
  ];
47
52
  sourceFile.addExportDeclarations(exports);
48
53
  return sourceFile.getFullText();
@@ -65,6 +70,12 @@ function generateCommonFile(operations, ctx) {
65
70
  sourceFile.addVariableStatement(buildQueryKeyConst(op));
66
71
  sourceFile.addVariableStatement(buildQueryKeyFn(op, ctx));
67
72
  }
73
+ // Add dedicated infinite query types and keys for paginatable operations
74
+ for (const op of getOperations.filter((o) => o.isPaginatable)) {
75
+ sourceFile.addTypeAlias(buildInfiniteClientOptionsType(op, ctx));
76
+ sourceFile.addVariableStatement(buildInfiniteQueryKeyConst(op));
77
+ sourceFile.addVariableStatement(buildInfiniteQueryKeyFn(op));
78
+ }
68
79
  // Add mutation types and keys
69
80
  for (const op of mutationOperations) {
70
81
  sourceFile.addTypeAlias(buildMutationResultType(op));
@@ -94,6 +105,37 @@ function generateQueriesFile(operations, ctx) {
94
105
  }
95
106
  return sourceFile.getFullText();
96
107
  }
108
+ /**
109
+ * Generate the queryOptions.ts file content.
110
+ */
111
+ function generateQueryOptionsFile(operations, ctx) {
112
+ const project = createGenerationProject();
113
+ const sourceFile = project.createSourceFile(`${OpenApiRqFiles.queryOptions}.ts`, undefined, { overwrite: true });
114
+ // Add imports
115
+ const imports = [
116
+ buildCommonImport(),
117
+ buildQueryOptionsImport(),
118
+ buildClientImport(ctx),
119
+ buildServiceImport(ctx),
120
+ ];
121
+ const modelImport = buildModelImport(ctx);
122
+ if (modelImport) {
123
+ imports.push(modelImport);
124
+ }
125
+ sourceFile.addImportDeclarations(imports);
126
+ // Only GET operations have query options
127
+ const getOperations = operations.filter((op) => op.httpMethod === "GET");
128
+ for (const op of getOperations) {
129
+ sourceFile.addVariableStatement(buildQueryOptionsFn(op, ctx));
130
+ }
131
+ for (const op of getOperations) {
132
+ const infiniteOptions = buildInfiniteQueryOptionsFn(op, ctx);
133
+ if (infiniteOptions) {
134
+ sourceFile.addVariableStatement(infiniteOptions);
135
+ }
136
+ }
137
+ return sourceFile.getFullText();
138
+ }
97
139
  /**
98
140
  * Generate the suspense.ts file content.
99
141
  */
@@ -185,6 +227,10 @@ export function generateAllFiles(operations, ctx) {
185
227
  name: `${OpenApiRqFiles.queries}.ts`,
186
228
  content: addHeaderComment(generateQueriesFile(operations, ctx), ctx.version),
187
229
  },
230
+ {
231
+ name: `${OpenApiRqFiles.queryOptions}.ts`,
232
+ content: addHeaderComment(generateQueryOptionsFile(operations, ctx), ctx.version),
233
+ },
188
234
  {
189
235
  name: `${OpenApiRqFiles.suspense}.ts`,
190
236
  content: addHeaderComment(generateSuspenseFile(operations, ctx), ctx.version),
@@ -52,6 +52,16 @@ export function buildQueryImport() {
52
52
  ],
53
53
  };
54
54
  }
55
+ /**
56
+ * Build import structure for the queryOptions/infiniteQueryOptions helpers.
57
+ */
58
+ export function buildQueryOptionsImport() {
59
+ return {
60
+ kind: StructureKind.ImportDeclaration,
61
+ moduleSpecifier: "@tanstack/react-query",
62
+ namedImports: [{ name: "queryOptions" }, { name: "infiniteQueryOptions" }],
63
+ };
64
+ }
55
65
  /**
56
66
  * Build import structure for services.
57
67
  */
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.2",
4
4
  "description": "OpenAPI React Query Codegen",
5
5
  "bin": {
6
6
  "openapi-rq": "dist/cli.mjs"