@7nohe/openapi-react-query-codegen
Advanced tools
@@ -144,5 +144,7 @@ import { StructureKind, VariableDeclarationKind, } from "ts-morph"; | ||
| * 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"; | ||
| * Shares the plain query key as its first segment so a single | ||
| * `invalidateQueries({ queryKey: [useXKey] })` matches both the plain and the | ||
| * infinite cache entries of an operation (#174), while the extra "infinite" | ||
| * segment keeps cached InfiniteData from colliding with plain query data (#140). | ||
| * Example: export const useFindPaginatedPetsInfiniteKey = [useFindPaginatedPetsKey, "infinite"] as const; | ||
| */ | ||
@@ -157,3 +159,3 @@ export function buildInfiniteQueryKeyConst(op) { | ||
| name: `use${op.capitalizedMethodName}InfiniteKey`, | ||
| initializer: `"${op.capitalizedMethodName}Infinite"`, | ||
| initializer: `[use${op.capitalizedMethodName}Key, "infinite"] as const`, | ||
| }, | ||
@@ -165,4 +167,7 @@ ], | ||
| * Build the infinite query key function. | ||
| * The custom queryKey argument only replaces the params segment โ the | ||
| * hierarchical [opKey, "infinite"] prefix is always preserved so | ||
| * prefix-based invalidation keeps working. | ||
| * Example: export const UseFindPaginatedPetsInfiniteKeyFn = (clientOptions: FindPaginatedPetsInfiniteClientOptions = {}, queryKey?: Array<unknown>) => | ||
| * [useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])]; | ||
| * [...useFindPaginatedPetsInfiniteKey, ...(queryKey ?? [clientOptions])]; | ||
| */ | ||
@@ -182,3 +187,3 @@ export function buildInfiniteQueryKeyFn(op) { | ||
| name: `Use${op.capitalizedMethodName}InfiniteKeyFn`, | ||
| initializer: `(${params.join(", ")}) => [use${op.capitalizedMethodName}InfiniteKey, ...(queryKey ?? [clientOptions])]`, | ||
| initializer: `(${params.join(", ")}) => [...use${op.capitalizedMethodName}InfiniteKey, ...(queryKey ?? [clientOptions])]`, | ||
| }, | ||
@@ -185,0 +190,0 @@ ], |
| import { StructureKind, VariableDeclarationKind, } from "ts-morph"; | ||
| import { SDK_CALL_ARGS } from "./buildQueryHooks.mjs"; | ||
| /** | ||
@@ -36,3 +37,3 @@ * Get the error type string based on client type. | ||
| const optionsType = `Options<${dataTypeName}, true>`; | ||
| const mutationFn = `clientOptions => ${op.methodName}(clientOptions) as unknown as Promise<TData>`; | ||
| const mutationFn = `clientOptions => ${op.methodName}(${SDK_CALL_ARGS}) as unknown as Promise<TData>`; | ||
| const body = `useMutation<TData, TError, ${optionsType}, TContext>({ mutationKey: Common.Use${op.capitalizedMethodName}KeyFn(mutationKey), mutationFn: ${mutationFn}, ...options })`; | ||
@@ -39,0 +40,0 @@ return { |
@@ -17,28 +17,2 @@ import { StructureKind, VariableDeclarationKind, } from "ts-morph"; | ||
| /** | ||
| * Get the data type based on hook type. | ||
| */ | ||
| function getDataTypeDefault(op, hookType) { | ||
| const baseType = `Common.${op.capitalizedMethodName}DefaultResponse`; | ||
| if (hookType === "useSuspenseQuery") { | ||
| return `NonNullable<${baseType}>`; | ||
| } | ||
| if (hookType === "useInfiniteQuery") { | ||
| return `InfiniteData<${baseType}>`; | ||
| } | ||
| return baseType; | ||
| } | ||
| /** | ||
| * Get the options type name. | ||
| */ | ||
| function getOptionsTypeName(hookType) { | ||
| switch (hookType) { | ||
| case "useSuspenseQuery": | ||
| return "UseSuspenseQueryOptions"; | ||
| case "useInfiniteQuery": | ||
| return "UseInfiniteQueryOptions"; | ||
| default: | ||
| return "UseQueryOptions"; | ||
| } | ||
| } | ||
| /** | ||
| * Resolve the generated Data type name for an operation, falling back to | ||
@@ -53,8 +27,13 @@ * unknown when the operation has no generated Data type. | ||
| /** | ||
| * SDK call arguments shared by every generated queryFn/mutationFn. | ||
| * throwOnError: true forces the SDK call to reject on error responses; the | ||
| * hey-api runtime default is false, which would resolve undefined data and | ||
| * swallow the error instead of surfacing it to TanStack Query (#172). | ||
| */ | ||
| export const SDK_CALL_ARGS = "{ ...clientOptions, throwOnError: true }"; | ||
| /** | ||
| * Build the client options parameter string. | ||
| */ | ||
| export function buildClientOptionsParam(op, ctx) { | ||
| const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`) | ||
| ? `${op.capitalizedMethodName}Data` | ||
| : "unknown"; | ||
| const dataTypeName = getDataTypeName(op, ctx); | ||
| const hasParams = op.parameters.length > 0; | ||
@@ -68,2 +47,58 @@ if (!hasParams) { | ||
| /** | ||
| * Build the clientOptions parameter typed with the page-less infinite | ||
| * options type โ the page parameter is supplied by TanStack Query's | ||
| * pageParam mechanism. | ||
| */ | ||
| export function buildInfiniteClientOptionsParam(op) { | ||
| const defaultValue = op.allParamsOptional ? " = {}" : ""; | ||
| return `clientOptions: Common.${op.capitalizedMethodName}InfiniteClientOptions${defaultValue}`; | ||
| } | ||
| /** | ||
| * Build the paginated SDK call shared by every infinite query builder. | ||
| */ | ||
| export function buildPagedQueryFn(op, ctx, castTData) { | ||
| const dataTypeName = getDataTypeName(op, ctx); | ||
| const thenClause = castTData | ||
| ? ".then(response => response.data as TData) as TData" | ||
| : ".then(response => response.data)"; | ||
| return `({ pageParam }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${ctx.pageParam}: pageParam as number }, throwOnError: true } as Options<${dataTypeName}, true>)${thenClause}`; | ||
| } | ||
| /** | ||
| * Format the initialPageParam literal. Emits a numeric literal when possible | ||
| * so the inferred pageParam type matches what getNextPageParam returns. | ||
| */ | ||
| export function formatInitialPageParam(ctx) { | ||
| return /^-?\d+$/.test(ctx.initialPageParam) | ||
| ? ctx.initialPageParam | ||
| : JSON.stringify(ctx.initialPageParam); | ||
| } | ||
| /** | ||
| * Build the nested type for getNextPageParam. | ||
| * E.g., "meta.next" becomes "{ meta: { next: number } }" | ||
| */ | ||
| export function buildNestedNextPageType(nextPageParam) { | ||
| const segments = nextPageParam.split("."); | ||
| return segments.reduceRight((acc, segment) => { | ||
| return `{ ${segment}: ${acc} }`; | ||
| }, "number"); | ||
| } | ||
| /** | ||
| * Build the getNextPageParam expression. The parameter is annotated because | ||
| * not every TanStack entry point contextually types it (prefetchInfiniteQuery | ||
| * does not, which would fail noImplicitAny). | ||
| */ | ||
| export function buildGetNextPageParamExpr(ctx) { | ||
| const nestedType = buildNestedNextPageType(ctx.nextPageParam); | ||
| return `(response: unknown) => (response as ${nestedType}).${ctx.nextPageParam}`; | ||
| } | ||
| /** | ||
| * Build an options type where the pagination fields TanStack Query marks as | ||
| * required become optional overrides: the generator supplies them, and | ||
| * callers may replace them for custom pagination schemes (#156, #146). | ||
| */ | ||
| export function buildOverridableInfiniteOptionsType(optionsTypeName) { | ||
| const instantiated = `${optionsTypeName}<TData, TError>`; | ||
| return `Omit<${instantiated}, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"> & Partial<Pick<${instantiated}, "initialPageParam" | "getNextPageParam">>`; | ||
| } | ||
| /** | ||
| * Build useQuery hook. | ||
@@ -77,3 +112,3 @@ * Example: | ||
| * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey), | ||
| * queryFn: () => findPets({ ...clientOptions }).then(response => response.data as TData) as TData, | ||
| * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data as TData) as TData, | ||
| * ...options | ||
@@ -85,8 +120,5 @@ * }); | ||
| const errorType = getErrorType(op, ctx); | ||
| const dataTypeDefault = getDataTypeDefault(op, "useQuery"); | ||
| const dataTypeDefault = `Common.${op.capitalizedMethodName}DefaultResponse`; | ||
| const clientOptionsParam = buildClientOptionsParam(op, ctx); | ||
| const hasParams = op.parameters.length > 0; | ||
| // Build the queryFn body | ||
| const callArgs = hasParams ? "{ ...clientOptions }" : "{ ...clientOptions }"; | ||
| const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data as TData) as TData`; | ||
| const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data as TData) as TData`; | ||
| const body = `useQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`; | ||
@@ -113,7 +145,5 @@ return { | ||
| const errorType = getErrorType(op, ctx); | ||
| const dataTypeDefault = getDataTypeDefault(op, "useSuspenseQuery"); | ||
| const dataTypeDefault = `NonNullable<Common.${op.capitalizedMethodName}DefaultResponse>`; | ||
| const clientOptionsParam = buildClientOptionsParam(op, ctx); | ||
| const hasParams = op.parameters.length > 0; | ||
| const callArgs = hasParams ? "{ ...clientOptions }" : "{ ...clientOptions }"; | ||
| const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data as TData) as TData`; | ||
| const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data as TData) as TData`; | ||
| const body = `useSuspenseQuery<TData, TError>({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ...options })`; | ||
@@ -135,34 +165,26 @@ return { | ||
| /** | ||
| * Build the nested type for getNextPageParam. | ||
| * E.g., "meta.next" becomes "{ meta: { next: number } }" | ||
| * Build a useInfiniteQuery / useSuspenseInfiniteQuery hook. Both variants | ||
| * share the infinite query key (and therefore the cache); they differ only | ||
| * in the TanStack hook called, the options type, and the NonNullable TData | ||
| * default of the suspense variant. | ||
| */ | ||
| export function buildNestedNextPageType(nextPageParam) { | ||
| const segments = nextPageParam.split("."); | ||
| return segments.reduceRight((acc, segment) => { | ||
| return `{ ${segment}: ${acc} }`; | ||
| }, "number"); | ||
| } | ||
| /** | ||
| * Build useInfiniteQuery hook. | ||
| */ | ||
| export function buildUseInfiniteQueryHook(op, ctx) { | ||
| function buildInfiniteHook(op, ctx, suspense) { | ||
| if (!op.isPaginatable) { | ||
| return null; | ||
| } | ||
| const hookName = `use${op.capitalizedMethodName}Infinite`; | ||
| const hookCall = suspense ? "useSuspenseInfiniteQuery" : "useInfiniteQuery"; | ||
| const optionsTypeName = suspense | ||
| ? "UseSuspenseInfiniteQueryOptions" | ||
| : "UseInfiniteQueryOptions"; | ||
| const hookName = suspense | ||
| ? `use${op.capitalizedMethodName}SuspenseInfinite` | ||
| : `use${op.capitalizedMethodName}Infinite`; | ||
| const errorType = getErrorType(op, ctx); | ||
| const baseDataType = `Common.${op.capitalizedMethodName}DefaultResponse`; | ||
| 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: 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 } } as Options<${dataTypeName}, true>).then(response => response.data as TData) as TData`; | ||
| // Build getNextPageParam with nested type | ||
| const nestedType = buildNestedNextPageType(ctx.nextPageParam); | ||
| const getNextPageParam = `getNextPageParam: (response) => (response as ${nestedType}).${ctx.nextPageParam}`; | ||
| // initialPageParam is a string literal | ||
| const infiniteOptions = `initialPageParam: "${ctx.initialPageParam}", ${getNextPageParam}`; | ||
| const body = `useInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`; | ||
| const dataTypeDefault = suspense | ||
| ? `InfiniteData<NonNullable<${baseDataType}>>` | ||
| : `InfiniteData<${baseDataType}>`; | ||
| const queryFn = buildPagedQueryFn(op, ctx, true); | ||
| const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`; | ||
| const body = `${hookCall}({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`; | ||
| return { | ||
@@ -177,3 +199,3 @@ kind: StructureKind.VariableStatement, | ||
| name: hookName, | ||
| initializer: `<TData = InfiniteData<${baseDataType}>, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${clientOptionsParam}, queryKey?: TQueryKey, options?: Omit<UseInfiniteQueryOptions<TData, TError>, "queryKey" | "queryFn">) => ${body}`, | ||
| initializer: `<TData = ${dataTypeDefault}, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${buildInfiniteClientOptionsParam(op)}, queryKey?: TQueryKey, options?: ${buildOverridableInfiniteOptionsType(optionsTypeName)}) => ${body}`, | ||
| }, | ||
@@ -184,8 +206,21 @@ ], | ||
| /** | ||
| * Build useInfiniteQuery hook. | ||
| */ | ||
| export function buildUseInfiniteQueryHook(op, ctx) { | ||
| return buildInfiniteHook(op, ctx, false); | ||
| } | ||
| /** | ||
| * Build useSuspenseInfiniteQuery hook. | ||
| */ | ||
| export function buildUseSuspenseInfiniteQueryHook(op, ctx) { | ||
| return buildInfiniteHook(op, ctx, true); | ||
| } | ||
| /** | ||
| * Build prefetch function. | ||
| * Example: | ||
| * export const prefetchUseFindPets = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}) => | ||
| * export const prefetchUseFindPets = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}, options?: Omit<FetchQueryOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) => | ||
| * queryClient.prefetchQuery({ | ||
| * queryKey: Common.UseFindPetsKeyFn(clientOptions), | ||
| * queryFn: () => findPets({ ...clientOptions }).then(response => response.data) | ||
| * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data), | ||
| * ...options | ||
| * }); | ||
@@ -195,13 +230,5 @@ */ | ||
| const fnName = `prefetchUse${op.capitalizedMethodName}`; | ||
| const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`) | ||
| ? `${op.capitalizedMethodName}Data` | ||
| : "unknown"; | ||
| const hasParams = op.parameters.length > 0; | ||
| const defaultValue = op.allParamsOptional ? " = {}" : ""; | ||
| const clientOptionsParam = hasParams | ||
| ? `clientOptions: Options<${dataTypeName}, true>${defaultValue}` | ||
| : `clientOptions: Options<${dataTypeName}, true> = {}`; | ||
| const callArgs = "{ ...clientOptions }"; | ||
| const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data)`; | ||
| const body = `queryClient.prefetchQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn} })`; | ||
| const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`; | ||
| const optionsParam = `options?: Omit<FetchQueryOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn">`; | ||
| const body = `queryClient.prefetchQuery({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn}, ...options })`; | ||
| return { | ||
@@ -216,3 +243,3 @@ kind: StructureKind.VariableStatement, | ||
| name: fnName, | ||
| initializer: `(queryClient: QueryClient, ${clientOptionsParam}) => ${body}`, | ||
| initializer: `(queryClient: QueryClient, ${buildClientOptionsParam(op, ctx)}, ${optionsParam}) => ${body}`, | ||
| }, | ||
@@ -223,8 +250,44 @@ ], | ||
| /** | ||
| * Build prefetchInfiniteQuery function for a paginatable operation. | ||
| * Example: | ||
| * export const prefetchUseFindPaginatedPetsInfinite = (queryClient: QueryClient, clientOptions: Common.FindPaginatedPetsInfiniteClientOptions = {}, options?: Omit<FetchInfiniteQueryOptions<Common.FindPaginatedPetsDefaultResponse>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam">) => | ||
| * queryClient.prefetchInfiniteQuery({ | ||
| * queryKey: Common.UseFindPaginatedPetsInfiniteKeyFn(clientOptions), | ||
| * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam as number }, throwOnError: true } as Options<FindPaginatedPetsData, true>).then(response => response.data), | ||
| * initialPageParam: 1, | ||
| * getNextPageParam: (response: unknown) => (response as { nextPage: number }).nextPage, | ||
| * ...options | ||
| * }); | ||
| */ | ||
| export function buildPrefetchInfiniteQueryFn(op, ctx) { | ||
| if (!op.isPaginatable) { | ||
| return null; | ||
| } | ||
| const fnName = `prefetchUse${op.capitalizedMethodName}Infinite`; | ||
| const queryFn = buildPagedQueryFn(op, ctx, false); | ||
| const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`; | ||
| const optionsParam = `options?: Omit<FetchInfiniteQueryOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam">`; | ||
| const body = `queryClient.prefetchInfiniteQuery({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions), queryFn: ${queryFn}, ${infiniteOptions}, ...options })`; | ||
| 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: `(queryClient: QueryClient, ${buildInfiniteClientOptionsParam(op)}, ${optionsParam}) => ${body}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| /** | ||
| * Build ensureQueryData function. | ||
| * Example: | ||
| * export const ensureUseFindPetsData = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}) => | ||
| * export const ensureUseFindPetsData = (queryClient: QueryClient, clientOptions: Options<FindPetsData, true> = {}, options?: Omit<EnsureQueryDataOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) => | ||
| * queryClient.ensureQueryData({ | ||
| * queryKey: Common.UseFindPetsKeyFn(clientOptions), | ||
| * queryFn: () => findPets({ ...clientOptions }).then(response => response.data) | ||
| * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data), | ||
| * ...options | ||
| * }); | ||
@@ -234,13 +297,5 @@ */ | ||
| const fnName = `ensureUse${op.capitalizedMethodName}Data`; | ||
| const dataTypeName = ctx.modelNames.includes(`${op.capitalizedMethodName}Data`) | ||
| ? `${op.capitalizedMethodName}Data` | ||
| : "unknown"; | ||
| const hasParams = op.parameters.length > 0; | ||
| const defaultValue = op.allParamsOptional ? " = {}" : ""; | ||
| const clientOptionsParam = hasParams | ||
| ? `clientOptions: Options<${dataTypeName}, true>${defaultValue}` | ||
| : `clientOptions: Options<${dataTypeName}, true> = {}`; | ||
| const callArgs = "{ ...clientOptions }"; | ||
| const queryFn = `() => ${op.methodName}(${callArgs}).then(response => response.data)`; | ||
| const body = `queryClient.ensureQueryData({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn} })`; | ||
| const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`; | ||
| const optionsParam = `options?: Omit<EnsureQueryDataOptions<Common.${op.capitalizedMethodName}DefaultResponse>, "queryKey" | "queryFn">`; | ||
| const body = `queryClient.ensureQueryData({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions), queryFn: ${queryFn}, ...options })`; | ||
| return { | ||
@@ -255,3 +310,3 @@ kind: StructureKind.VariableStatement, | ||
| name: fnName, | ||
| initializer: `(queryClient: QueryClient, ${clientOptionsParam}) => ${body}`, | ||
| initializer: `(queryClient: QueryClient, ${buildClientOptionsParam(op, ctx)}, ${optionsParam}) => ${body}`, | ||
| }, | ||
@@ -258,0 +313,0 @@ ], |
| import { StructureKind, VariableDeclarationKind, } from "ts-morph"; | ||
| import { buildClientOptionsParam, buildNestedNextPageType, getDataTypeName, } from "./buildQueryHooks.mjs"; | ||
| import { buildClientOptionsParam, buildGetNextPageParamExpr, buildInfiniteClientOptionsParam, buildPagedQueryFn, formatInitialPageParam, SDK_CALL_ARGS, } from "./buildQueryHooks.mjs"; | ||
| /** | ||
@@ -12,3 +12,3 @@ * Build a queryOptions factory for a GET operation. | ||
| * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey), | ||
| * queryFn: () => findPets({ ...clientOptions }).then(response => response.data), | ||
| * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data), | ||
| * }); | ||
@@ -19,3 +19,3 @@ */ | ||
| const clientOptionsParam = buildClientOptionsParam(op, ctx); | ||
| const queryFn = `() => ${op.methodName}({ ...clientOptions }).then(response => response.data)`; | ||
| const queryFn = `() => ${op.methodName}(${SDK_CALL_ARGS}).then(response => response.data)`; | ||
| const body = `queryOptions({ queryKey: Common.Use${op.capitalizedMethodName}KeyFn(clientOptions, queryKey), queryFn: ${queryFn} })`; | ||
@@ -43,5 +43,5 @@ return { | ||
| * queryKey: Common.UseFindPaginatedPetsInfiniteKeyFn(clientOptions, queryKey), | ||
| * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam } } as Options<FindPaginatedPetsData, true>).then(response => response.data), | ||
| * queryFn: ({ pageParam }) => findPaginatedPets({ ...clientOptions, query: { ...clientOptions.query, page: pageParam as number }, throwOnError: true } as Options<FindPaginatedPetsData, true>).then(response => response.data), | ||
| * initialPageParam: 1, | ||
| * getNextPageParam: (response) => (response as { nextPage: number }).nextPage, | ||
| * getNextPageParam: (response: unknown) => (response as { nextPage: number }).nextPage, | ||
| * }); | ||
@@ -54,14 +54,5 @@ */ | ||
| 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} })`; | ||
| const queryFn = buildPagedQueryFn(op, ctx, false); | ||
| const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx)}`; | ||
| const body = `infiniteQueryOptions({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), queryFn: ${queryFn}, ${infiniteOptions} })`; | ||
| return { | ||
@@ -76,3 +67,3 @@ kind: StructureKind.VariableStatement, | ||
| name: fnName, | ||
| initializer: `(${clientOptionsParam}, queryKey?: Array<unknown>) => ${body}`, | ||
| initializer: `(${buildInfiniteClientOptionsParam(op)}, queryKey?: Array<unknown>) => ${body}`, | ||
| }, | ||
@@ -79,0 +70,0 @@ ], |
@@ -1,6 +0,5 @@ | ||
| import { StructureKind, } from "ts-morph"; | ||
| import { OpenApiRqFiles } from "../constants.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 { buildEnsureQueryDataFn, buildPrefetchFn, buildPrefetchInfiniteQueryFn, buildUseInfiniteQueryHook, buildUseQueryHook, buildUseSuspenseInfiniteQueryHook, buildUseSuspenseQueryHook, } from "./buildQueryHooks.mjs"; | ||
| import { buildInfiniteQueryOptionsFn, buildQueryOptionsFn, } from "./buildQueryOptions.mjs"; | ||
@@ -34,22 +33,6 @@ import { buildAxiosErrorImport, buildClientImport, buildCommonImport, buildModelImport, buildQueryImport, buildQueryOptionsImport, buildServiceImport, createGenerationProject, } from "./projectFactory.mjs"; | ||
| * Generate the index.ts file content. | ||
| * The content is constant, so no ts-morph project is needed. | ||
| */ | ||
| function generateIndexFile(ctx) { | ||
| const project = createGenerationProject(); | ||
| const sourceFile = project.createSourceFile(`${OpenApiRqFiles.index}.ts`, undefined, { overwrite: true }); | ||
| const exports = [ | ||
| { | ||
| kind: StructureKind.ExportDeclaration, | ||
| moduleSpecifier: "./common", | ||
| }, | ||
| { | ||
| kind: StructureKind.ExportDeclaration, | ||
| moduleSpecifier: "./queries", | ||
| }, | ||
| { | ||
| kind: StructureKind.ExportDeclaration, | ||
| moduleSpecifier: "./queryOptions", | ||
| }, | ||
| ]; | ||
| sourceFile.addExportDeclarations(exports); | ||
| return sourceFile.getFullText(); | ||
| function generateIndexFile() { | ||
| return `export * from "./common";\nexport * from "./queries";\nexport * from "./queryOptions";\n`; | ||
| } | ||
@@ -154,2 +137,9 @@ /** | ||
| } | ||
| // Add useSuspenseInfiniteQuery hooks for paginatable operations | ||
| for (const op of getOperations.filter((o) => o.isPaginatable)) { | ||
| const hook = buildUseSuspenseInfiniteQueryHook(op, ctx); | ||
| if (hook) { | ||
| sourceFile.addVariableStatement(hook); | ||
| } | ||
| } | ||
| return sourceFile.getFullText(); | ||
@@ -190,2 +180,9 @@ } | ||
| } | ||
| // Add prefetchInfiniteQuery functions for paginatable operations | ||
| for (const op of getOperations.filter((o) => o.isPaginatable)) { | ||
| const fn = buildPrefetchInfiniteQueryFn(op, ctx); | ||
| if (fn) { | ||
| sourceFile.addVariableStatement(fn); | ||
| } | ||
| } | ||
| return sourceFile.getFullText(); | ||
@@ -223,3 +220,3 @@ } | ||
| name: `${OpenApiRqFiles.index}.ts`, | ||
| content: addHeaderComment(generateIndexFile(ctx), ctx.version), | ||
| content: addHeaderComment(generateIndexFile(), ctx.version), | ||
| }, | ||
@@ -226,0 +223,0 @@ { |
@@ -0,5 +1,5 @@ | ||
| export * from "./buildCommon.mjs"; | ||
| export * from "./buildMutationHooks.mjs"; | ||
| export * from "./buildQueryHooks.mjs"; | ||
| export { generateAllFiles } from "./generateFiles.mjs"; | ||
| export { createGenerationProject } from "./projectFactory.mjs"; | ||
| export * from "./buildCommon.mjs"; | ||
| export * from "./buildQueryHooks.mjs"; | ||
| export * from "./buildMutationHooks.mjs"; |
@@ -44,2 +44,3 @@ import { IndentationText, NewLineKind, Project, QuoteKind, StructureKind, } from "ts-morph"; | ||
| { name: "useInfiniteQuery" }, | ||
| { name: "useSuspenseInfiniteQuery" }, | ||
| { name: "useMutation" }, | ||
@@ -49,2 +50,3 @@ { name: "UseQueryResult" }, | ||
| { name: "UseInfiniteQueryOptions" }, | ||
| { name: "UseSuspenseInfiniteQueryOptions" }, | ||
| { name: "UseMutationOptions" }, | ||
@@ -54,2 +56,5 @@ { name: "UseMutationResult" }, | ||
| { name: "InfiniteData" }, | ||
| { name: "FetchQueryOptions", isTypeOnly: true }, | ||
| { name: "FetchInfiniteQueryOptions", isTypeOnly: true }, | ||
| { name: "EnsureQueryDataOptions", isTypeOnly: true }, | ||
| ], | ||
@@ -56,0 +61,0 @@ }; |
+11
-9
| { | ||
| "name": "@7nohe/openapi-react-query-codegen", | ||
| "version": "3.0.0-beta.2", | ||
| "version": "3.0.0-beta.3", | ||
| "description": "OpenAPI React Query Codegen", | ||
@@ -46,16 +46,18 @@ "bin": { | ||
| "devDependencies": { | ||
| "@biomejs/biome": "^1.9.3", | ||
| "@biomejs/biome": "^2.5.4", | ||
| "@types/cross-spawn": "^6.0.6", | ||
| "@types/node": "^22.7.4", | ||
| "@types/node": "^22.20.1", | ||
| "@types/semver": "^7.7.1", | ||
| "@vitest/coverage-v8": "^1.5.0", | ||
| "commander": "^12.0.0", | ||
| "lefthook": "^1.6.10", | ||
| "rimraf": "^5.0.5", | ||
| "@vitest/coverage-v8": "^4.1.10", | ||
| "commander": "^15.0.0", | ||
| "lefthook": "^2.1.10", | ||
| "rimraf": "^6.1.3", | ||
| "ts-morph": "^28.0.0", | ||
| "typescript": "^6.0.3", | ||
| "vitest": "^1.5.0" | ||
| "vite": "^7", | ||
| "vitest": "^4.1.10" | ||
| }, | ||
| "peerDependencies": { | ||
| "commander": "12.x", | ||
| "@tanstack/react-query": "^5.0.0", | ||
| "commander": "12.x || 13.x || 14.x || 15.x", | ||
| "ts-morph": "28.x", | ||
@@ -62,0 +64,0 @@ "typescript": "5.x || 6.x" |
+55
-5
| # OpenAPI React Query Codegen | ||
| > Code generator for creating [React Query (also known as TanStack Query)](https://tanstack.com/query) hooks based on your OpenAPI schema. | ||
| > 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. | ||
| [](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen) | ||
| ๐ **[Documentation](https://openapi-react-query-codegen.vercel.app)** ยท [Migrating to v3](https://openapi-react-query-codegen.vercel.app/guides/migrating-to-v3/) | ||
| ## Features | ||
| - Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks | ||
| - Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions | ||
| - Generates query keys and functions for query caching | ||
| - Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | ||
| - **`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 | ||
| - **Custom hooks**: `useQuery`, `useSuspenseQuery`, `useMutation`, `useInfiniteQuery`, and `useSuspenseInfiniteQuery` variants per operation | ||
| - **SSR helpers**: `prefetchQuery`, `prefetchInfiniteQuery`, and `ensureQueryData` functions per operation โ ready for Next.js App Router hydration | ||
| - **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 | ||
| - **Pure TypeScript clients** generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) (fetch and axios) | ||
| ## Quick start | ||
| ```bash | ||
| npm install -D @7nohe/openapi-react-query-codegen | ||
| npx openapi-rq -i ./petstore.yaml | ||
| ``` | ||
| ```tsx | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { findPetsOptions } from "./openapi/queries"; | ||
| function Pets() { | ||
| const { data } = useQuery(findPetsOptions({ query: { limit: 10 } })); | ||
| // ...or use the generated hook directly: useFindPets({ query: { limit: 10 } }) | ||
| } | ||
| ``` | ||
| See the [documentation](https://openapi-react-query-codegen.vercel.app) for CLI options, SSR recipes, and infinite query usage. | ||
| ## How it compares | ||
| | | This library | @hey-api tanstack-query plugin | Orval | | ||
| |---|---|---|---| | ||
| | `queryOptions` / `infiniteQueryOptions` factories (TanStack v5 pattern) | โ | โ | โ | | ||
| | Ready-to-use hooks (`useQuery` / suspense / infinite variants) | โ | โ (options only) | โ | | ||
| | SSR helpers (`prefetchQuery` / `prefetchInfiniteQuery` / `ensureQueryData`) | โ | โ | Partial (`usePrefetch`) | | ||
| | Hierarchical query keys for granular invalidation | โ | โ (tags) | Partial | | ||
| | Stable release line | โ SemVer | pre-1.0, frequent breaking changes | โ | | ||
| | MSW mock generation | โ (out of scope) | โ | โ | | ||
| | Vue / Solid / Svelte / Angular | โ React-focused | โ | โ | | ||
| **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. | ||
| ## Stability policy | ||
| 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. | ||
| ## Requirements | ||
| - Node.js 22.18+ | ||
| - `@tanstack/react-query` 5.x (peer dependency) | ||
| - `typescript` 5.x or 6.x, `ts-morph` 28.x, `commander` 12โ15 (peer dependencies) | ||
| ## License | ||
| MIT |
77190
9.62%1723
3.3%63
384.62%6
20%12
9.09%