@7nohe/openapi-react-query-codegen
Advanced tools
| import { StructureKind, VariableDeclarationKind, } from "ts-morph"; | ||
| import { buildClientOptionsParam, buildNestedNextPageType, getDataTypeName, } from "./buildQueryHooks.mjs"; | ||
| /** | ||
| * Build a queryOptions factory for a GET operation. | ||
| * The factory centralizes queryKey and queryFn so they can be reused with | ||
| * every TanStack Query utility (useQuery, useQueries, prefetchQuery, | ||
| * ensureQueryData, setQueryData, ...) with full type safety. | ||
| * Example: | ||
| * export const findPetsOptions = (clientOptions: Options<FindPetsData, true> = {}, queryKey?: Array<unknown>) => | ||
| * queryOptions({ | ||
| * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey), | ||
| * queryFn: () => findPets({ ...clientOptions }).then(response => response.data), | ||
| * }); | ||
| */ | ||
| export function buildQueryOptionsFn(op, ctx) { | ||
| const fnName = `${op.methodName}Options`; | ||
| const clientOptionsParam = buildClientOptionsParam(op, ctx); | ||
| const queryFn = `() => ${op.methodName}({ ...clientOptions }).then(response => response.data)`; | ||
| const body = `queryOptions({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn} })`; | ||
| return { | ||
| kind: StructureKind.VariableStatement, | ||
| // Copy the operation's JSDoc (description and @deprecated) from the SDK function | ||
| leadingTrivia: op.jsDoc ? `${op.jsDoc}\n` : undefined, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
| name: fnName, | ||
| initializer: `(${clientOptionsParam}, queryKey?: Array<unknown>) => ${body}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| /** | ||
| * Build an infiniteQueryOptions factory for a paginatable GET operation. | ||
| * Uses the dedicated infinite query key and page-less options type. | ||
| * Example: | ||
| * export const findPaginatedPetsInfiniteOptions = (clientOptions: Common.FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) => | ||
| * infiniteQueryOptions({ | ||
| * queryKey: Common.UseFindPaginatedPetsInfiniteKeyFn(clientOptions, queryKey), | ||
| * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam } } as Options<FindPaginatedPetsData, true>).then(response => response.data), | ||
| * initialPageParam: 1, | ||
| * getNextPageParam: (response) => (response as { nextPage: number }).nextPage, | ||
| * }); | ||
| */ | ||
| export function buildInfiniteQueryOptionsFn(op, ctx) { | ||
| if (!op.isPaginatable) { | ||
| return null; | ||
| } | ||
| const fnName = `${op.methodName}InfiniteOptions`; | ||
| const dataTypeName = getDataTypeName(op, ctx); | ||
| const defaultValue = op.allParamsOptional ? " = {}" : ""; | ||
| const clientOptionsParam = `clientOptions: Common.${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`; | ||
| const queryFn = `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${ctx.pageParam}: pageParam } } as Options<${dataTypeName}, true>).then(response => response.data)`; | ||
| // Emit a numeric literal when possible so the inferred pageParam type | ||
| // matches what getNextPageParam returns | ||
| const initialPageParam = /^-?\d+$/.test(ctx.initialPageParam) | ||
| ? ctx.initialPageParam | ||
| : JSON.stringify(ctx.initialPageParam); | ||
| const nestedType = buildNestedNextPageType(ctx.nextPageParam); | ||
| const getNextPageParam = `(response) => (response as ${nestedType}).${ctx.nextPageParam}`; | ||
| const body = `infiniteQueryOptions({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, initialPageParam: ${initialPageParam}, getNextPageParam: ${getNextPageParam} })`; | ||
| return { | ||
| kind: StructureKind.VariableStatement, | ||
| // Copy the operation's JSDoc (description and @deprecated) from the SDK function | ||
| leadingTrivia: op.jsDoc ? `${op.jsDoc}\n` : undefined, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
| name: fnName, | ||
| initializer: `(${clientOptionsParam}, queryKey?: Array<unknown>) => ${body}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } |
@@ -8,2 +8,3 @@ export const defaultOutputPath = "openapi"; | ||
| queries: "queries", | ||
| queryOptions: "queryOptions", | ||
| infiniteQueries: "infiniteQueries", | ||
@@ -10,0 +11,0 @@ common: "common", |
@@ -120,1 +120,65 @@ import { StructureKind, VariableDeclarationKind, } from "ts-morph"; | ||
| } | ||
| /** | ||
| * Build the client options type for infinite queries. | ||
| * The page parameter is excluded because TanStack Query supplies it via the | ||
| * pageParam mechanism (#140). | ||
| * Example: | ||
| * export type FindPaginatedPetsInfiniteClientOptions = Omit<Options<FindPaginatedPetsData, true>, "query"> & | ||
| * { query?: Omit<NonNullable<FindPaginatedPetsData["query"]>, "page"> }; | ||
| */ | ||
| export function buildInfiniteClientOptionsType(op, ctx) { | ||
| const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`) | ||
| ? `${op.capitalizedMethodName}Data` | ||
| : "unknown"; | ||
| const type = dataTypeName === "unknown" | ||
| ? "Options<unknown, true>" | ||
| : `Omit<Options<${dataTypeName}, true>, "query"> & { query?: Omit<NonNullable<${dataTypeName}["query"]>, "${ctx.pageParam}"> }`; | ||
| return { | ||
| kind: StructureKind.TypeAlias, | ||
| isExported: true, | ||
| name: `${op.capitalizedMethodName}InfiniteClientOptions`, | ||
| type, | ||
| }; | ||
| } | ||
| /** | ||
| * Build the infinite query key constant. | ||
| * Kept distinct from the plain query key so cached InfiniteData never | ||
| * collides with plain query data for the same operation (#140). | ||
| * Example: export const useFindPaginatedPetsInfiniteKey = "FindPaginatedPetsInfinite"; | ||
| */ | ||
| export function buildInfiniteQueryKeyConst(op) { | ||
| return { | ||
| kind: StructureKind.VariableStatement, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
| name: `use${op.capitalizedMethodName}InfiniteKey`, | ||
| initializer: `"${op.capitalizedMethodName}Infinite"`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| /** | ||
| * Build the infinite query key function. | ||
| * Example: export const UseFindPaginatedPetsInfiniteKeyFn = (clientOptions: FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) => | ||
| * [useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])]; | ||
| */ | ||
| export function buildInfiniteQueryKeyFn(op) { | ||
| const defaultValue = op.allParamsOptional ? " = {}" : ""; | ||
| const params = [ | ||
| `clientOptions: ${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`, | ||
| "queryKey?: Array<unknown>", | ||
| ]; | ||
| return { | ||
| kind: StructureKind.VariableStatement, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
| name: `Use${op.capitalizedMethodName}InfiniteKeyFn`, | ||
| initializer: `(${params.join(", ")}) => [use${op.capitalizedMethodName}InfiniteKey, ...(queryKey ?? [clientOptions])]`, | ||
| }, | ||
| ], | ||
| }; | ||
| } |
@@ -43,5 +43,14 @@ import { StructureKind, VariableDeclarationKind, } from "ts-morph"; | ||
| /** | ||
| * Resolve the generated Data type name for an operation, falling back to | ||
| * unknown when the operation has no generated Data type. | ||
| */ | ||
| export function getDataTypeName(op, ctx) { | ||
| return ctx.modelNames.includes(`${op.capitalizedMethodName}Data`) | ||
| ? `${op.capitalizedMethodName}Data` | ||
| : "unknown"; | ||
| } | ||
| /** | ||
| * Build the client options parameter string. | ||
| */ | ||
| function buildClientOptionsParam(op, ctx) { | ||
| export function buildClientOptionsParam(op, ctx) { | ||
| const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`) | ||
@@ -124,3 +133,3 @@ ? `${op.capitalizedMethodName}Data` | ||
| */ | ||
| function buildNestedNextPageType(nextPageParam) { | ||
| export function buildNestedNextPageType(nextPageParam) { | ||
| const segments = nextPageParam.split("."); | ||
@@ -141,9 +150,9 @@ return segments.reduceRight((acc, segment) => { | ||
| const baseDataType = `Common.${op.capitalizedMethodName}DefaultResponse`; | ||
| const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`) | ||
| ? `${op.capitalizedMethodName}Data` | ||
| : "unknown"; | ||
| const dataTypeName = getDataTypeName(op, ctx); | ||
| // Infinite queries take a dedicated options type that excludes the page | ||
| // parameter — it is supplied by TanStack Query's pageParam mechanism | ||
| const defaultValue = op.allParamsOptional ? " = {}" : ""; | ||
| const clientOptionsParam = `clientOptions: Options<${dataTypeName}, true>${defaultValue}`; | ||
| const clientOptionsParam = `clientOptions: Common.${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`; | ||
| // Build the queryFn with pageParam handling | ||
| const queryFn = `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${ctx.pageParam}: pageParam as number } }).then(response => response.data as TData) as TData`; | ||
| 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`; | ||
| // Build getNextPageParam with nested type | ||
@@ -154,3 +163,3 @@ const nestedType = buildNestedNextPageType(ctx.nextPageParam); | ||
| const infiniteOptions = `initialPageParam: "${ctx.initialPageParam}", ${getNextPageParam}`; | ||
| const body = `useInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`; | ||
| const body = `useInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`; | ||
| return { | ||
@@ -157,0 +166,0 @@ kind: StructureKind.VariableStatement, |
| import { StructureKind, } from "ts-morph"; | ||
| import { OpenApiRqFiles } from "../constants.mjs"; | ||
| import { buildDefaultResponseType, buildMutationKeyConst, buildMutationKeyFn, buildMutationResultType, buildQueryKeyConst, buildQueryKeyFn, buildQueryResultType, } from "./buildCommon.mjs"; | ||
| import { buildDefaultResponseType, buildInfiniteClientOptionsType, buildInfiniteQueryKeyConst, buildInfiniteQueryKeyFn, buildMutationKeyConst, buildMutationKeyFn, buildMutationResultType, buildQueryKeyConst, buildQueryKeyFn, buildQueryResultType, } from "./buildCommon.mjs"; | ||
| import { buildUseMutationHook } from "./buildMutationHooks.mjs"; | ||
| import { buildEnsureQueryDataFn, buildPrefetchFn, buildUseInfiniteQueryHook, buildUseQueryHook, buildUseSuspenseQueryHook, } from "./buildQueryHooks.mjs"; | ||
| import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs"; | ||
| import { buildInfiniteQueryOptionsFn, buildQueryOptionsFn, } from "./buildQueryOptions.mjs"; | ||
| import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildQueryOptionsImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs"; | ||
| /** | ||
@@ -46,2 +47,6 @@ * Build imports for common.ts file. | ||
| }, | ||
| { | ||
| kind: StructureKind.ExportDeclaration, | ||
| moduleSpecifier: "./queryOptions", | ||
| }, | ||
| ]; | ||
@@ -69,2 +74,8 @@ sourceFile.addExportDeclarations(exports); | ||
| } | ||
| // Add dedicated infinite query types and keys for paginatable operations | ||
| for (const op of getOperations.filter((o) => o.isPaginatable)) { | ||
| sourceFile.addTypeAlias(buildInfiniteClientOptionsType(op, ctx)); | ||
| sourceFile.addVariableStatement(buildInfiniteQueryKeyConst(op)); | ||
| sourceFile.addVariableStatement(buildInfiniteQueryKeyFn(op)); | ||
| } | ||
| // Add mutation types and keys | ||
@@ -100,2 +111,33 @@ for (const op of mutationOperations) { | ||
| /** | ||
| * Generate the queryOptions.ts file content. | ||
| */ | ||
| function generateQueryOptionsFile(operations, ctx) { | ||
| const project = createGenerationProject(); | ||
| const sourceFile = project.createSourceFile(`${OpenApiRqFiles.queryOptions}.ts`, undefined, { overwrite: true }); | ||
| // Add imports | ||
| const imports = [ | ||
| buildCommonImport(), | ||
| buildQueryOptionsImport(), | ||
| buildClientImport(ctx), | ||
| buildServiceImport(ctx), | ||
| ]; | ||
| const modelImport = buildModelImport(ctx); | ||
| if (modelImport) { | ||
| imports.push(modelImport); | ||
| } | ||
| sourceFile.addImportDeclarations(imports); | ||
| // Only GET operations have query options | ||
| const getOperations = operations.filter((op) => op.httpMethod === "GET"); | ||
| for (const op of getOperations) { | ||
| sourceFile.addVariableStatement(buildQueryOptionsFn(op, ctx)); | ||
| } | ||
| for (const op of getOperations) { | ||
| const infiniteOptions = buildInfiniteQueryOptionsFn(op, ctx); | ||
| if (infiniteOptions) { | ||
| sourceFile.addVariableStatement(infiniteOptions); | ||
| } | ||
| } | ||
| return sourceFile.getFullText(); | ||
| } | ||
| /** | ||
| * Generate the suspense.ts file content. | ||
@@ -192,2 +234,6 @@ */ | ||
| { | ||
| name: `${OpenApiRqFiles.queryOptions}.ts`, | ||
| content: addHeaderComment(generateQueryOptionsFile(operations, ctx), ctx.version), | ||
| }, | ||
| { | ||
| name: `${OpenApiRqFiles.suspense}.ts`, | ||
@@ -194,0 +240,0 @@ content: addHeaderComment(generateSuspenseFile(operations, ctx), ctx.version), |
@@ -56,2 +56,12 @@ import { IndentationText, NewLineKind, Project, QuoteKind, StructureKind, } from "ts-morph"; | ||
| /** | ||
| * Build import structure for the queryOptions/infiniteQueryOptions helpers. | ||
| */ | ||
| export function buildQueryOptionsImport() { | ||
| return { | ||
| kind: StructureKind.ImportDeclaration, | ||
| moduleSpecifier: "@tanstack/react-query", | ||
| namedImports: [{ name: "queryOptions" }, { name: "infiniteQueryOptions" }], | ||
| }; | ||
| } | ||
| /** | ||
| * Build import structure for services. | ||
@@ -58,0 +68,0 @@ */ |
+1
-1
| { | ||
| "name": "@7nohe/openapi-react-query-codegen", | ||
| "version": "3.0.0-beta.1", | ||
| "version": "3.0.0-beta.2", | ||
| "description": "OpenAPI React Query Codegen", | ||
@@ -5,0 +5,0 @@ "bin": { |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
70419
15.64%22
4.76%1668
14.17%0
-100%