UNPKG

@7nohe/openapi-react-query-codegen

Version:
330 lines (329 loc) 17.3 kB
import { StructureKind, VariableDeclarationKind, } from "ts-morph"; import { getDataTypeName, getErrorType } from "./operationNames.mjs"; /** * 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 }"; /** SDK call arguments for TanStack query functions with cancellation. */ export const QUERY_SDK_CALL_ARGS = "{ ...clientOptions, signal, throwOnError: true }"; /** Resolve the OpenAPI page parameter type, preserving older numeric output. */ export function getPageParamType(op) { return op.pageParamType ?? "number"; } /** * Build the client options parameter string. */ export function buildClientOptionsParam(op) { const dataTypeName = getDataTypeName(op); const hasParams = op.parameters.length > 0; if (!hasParams) { return `clientOptions: Options<${dataTypeName}, true> = {}`; } const defaultValue = op.allParamsOptional ? " = {}" : ""; return `clientOptions: Options<${dataTypeName}, true>${defaultValue}`; } /** * 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}`; } /** * The type of a single page. TanStack Query instantiates the infinite options * with this as TQueryFnData, so it is what `getNextPageParam` receives as * `lastPage` (#203). NonNullable because `throwOnError: true` means a resolved * page is always present. */ export function getPageType(op) { return `NonNullable<Common.${op.capitalizedMethodName}DefaultResponse>`; } /** * Build the paginated SDK call shared by every infinite query builder. * The resolved response is narrowed to the page type so it matches the * TQueryFnData the infinite options are instantiated with (#203). Spelled as * a cast rather than `!` because generated code is linted downstream, and a * non-null assertion trips biome's noNonNullAssertion. */ export function buildPagedQueryFn(op, ctx) { const dataTypeName = getDataTypeName(op); const thenClause = `.then(response => response.data as ${getPageType(op)})`; const pageParamType = getPageParamType(op); // When the initial page param is omitted, the first request must send no // page param at all, so spread it in only once TanStack Query provides one. const pageQuery = ctx.omitInitialPageParam ? `...(pageParam === undefined ? {} : { ${ctx.pageParam}: pageParam as ${pageParamType} })` : `${ctx.pageParam}: pageParam as ${pageParamType}`; return `({ pageParam, signal }) => ${op.methodName}({ ...clientOptions, query: { ...clientOptions.query, ${pageQuery} }, signal, throwOnError: true } as Options<${dataTypeName}, true>)${thenClause}`; } /** * Format the initialPageParam literal. Emits `undefined` when the caller opted * to omit it (#177); otherwise a numeric literal when possible so the inferred * pageParam type matches what getNextPageParam returns. */ export function formatInitialPageParam(ctx, op) { if (ctx.omitInitialPageParam) { return "undefined"; } const isStringPageParam = op ? op.pageParamTypeKind === "string" || (op.pageParamTypeKind === undefined && /\bstring\b/.test(getPageParamType(op))) : false; if (isStringPageParam) { return JSON.stringify(ctx.initialPageParam); } 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, pageParamType = "number") { const segments = nextPageParam.split("."); return segments.reduceRight((acc, segment) => { return `{ ${segment}: ${acc} }`; }, pageParamType); } /** * 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, op) { const nestedType = buildNestedNextPageType(ctx.nextPageParam, op ? getPageParamType(op) : "number"); 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). * * The first type argument is TQueryFnData — a single page — not TData (#203). * Passing TData there made `getNextPageParam` receive the aggregated * `InfiniteData<...>` as `lastPage`, so any custom pagination logic failed to * compile. Only the first three type arguments are written: TanStack Query * dropped TQueryData from `UseInfiniteQueryOptions` within the v5 line, so * positions beyond TData are not stable across the supported peer range. */ export function buildOverridableInfiniteOptionsType(optionsTypeName, pageType) { const instantiated = `${optionsTypeName}<${pageType}, TError, TData>`; return `Omit<${instantiated}, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"> & Partial<Pick<${instantiated}, "initialPageParam" | "getNextPageParam">>`; } /** * Build useQuery hook. * Example: * export const useFindPets = <TData = Common.FindPetsDefaultResponse, TError = FindPetsError, TQueryKey extends Array<unknown> = unknown[]>( * clientOptions: Options<FindPetsData, true> = {}, * queryKey?: TQueryKey, * options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn"> * ) => useQuery<TData, TError>({ * queryKey: Common.UseFindPetsKeyFn(clientOptions, queryKey), * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data as TData) as TData, * ...options * }); */ export function buildUseQueryHook(op, ctx) { const hookName = `use${op.capitalizedMethodName}`; const errorType = getErrorType(op, ctx); const dataTypeDefault = `Common.${op.capitalizedMethodName}DefaultResponse`; const clientOptionsParam = buildClientOptionsParam(op); const queryFn = `({ signal }) => ${op.methodName}(${QUERY_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 })`; 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: hookName, initializer: `<TData = ${dataTypeDefault}, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${clientOptionsParam}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => ${body}`, }, ], }; } /** * Build useSuspenseQuery hook. */ export function buildUseSuspenseQueryHook(op, ctx) { const hookName = `use${op.capitalizedMethodName}Suspense`; const errorType = getErrorType(op, ctx); const dataTypeDefault = `NonNullable<Common.${op.capitalizedMethodName}DefaultResponse>`; const clientOptionsParam = buildClientOptionsParam(op); const queryFn = `({ signal }) => ${op.methodName}(${QUERY_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 })`; 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: hookName, initializer: `<TData = ${dataTypeDefault}, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${clientOptionsParam}, queryKey?: TQueryKey, options?: Omit<UseSuspenseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => ${body}`, }, ], }; } /** * 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. */ function buildInfiniteHook(op, ctx, suspense) { if (!op.isPaginatable) { return null; } 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 dataTypeDefault = suspense ? `InfiniteData<NonNullable<${baseDataType}>>` : `InfiniteData<${baseDataType}>`; const queryFn = buildPagedQueryFn(op, ctx); const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx, op)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx, op)}`; const body = `${hookCall}({ queryKey: Common.Use${op.capitalizedMethodName}InfiniteKeyFn(clientOptions, queryKey), 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: hookName, initializer: `<TData = ${dataTypeDefault}, TError = ${errorType}, TQueryKey extends Array<unknown> = unknown[]>(${buildInfiniteClientOptionsParam(op)}, queryKey?: TQueryKey, options?: ${buildOverridableInfiniteOptionsType(optionsTypeName, getPageType(op))}) => ${body}`, }, ], }; } /** * 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> = {}, options?: Omit<FetchQueryOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) => * queryClient.prefetchQuery({ * queryKey: Common.UseFindPetsKeyFn(clientOptions), * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data), * ...options * }); */ export function buildPrefetchFn(op) { const fnName = `prefetchUse${op.capitalizedMethodName}`; const queryFn = `({ signal }) => ${op.methodName}(${QUERY_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 { 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, ${buildClientOptionsParam(op)}, ${optionsParam}) => ${body}`, }, ], }; } /** * Build prefetchInfiniteQuery function for a paginatable operation. * Example: * export const prefetchUseFindPaginatedPetsInfinite = (queryClient: QueryClient, clientOptions: Common.FindPaginatedPetsInfiniteClientOptions = {}, options?: Omit<FetchInfiniteQueryOptions<NonNullable<Common.FindPaginatedPetsDefaultResponse>>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"> & Partial<Pick<FetchInfiniteQueryOptions<NonNullable<Common.FindPaginatedPetsDefaultResponse>>, "initialPageParam">> & { getNextPageParam?: GetNextPageParamFunction<unknown, NonNullable<Common.FindPaginatedPetsDefaultResponse>> }) => * 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 as NonNullable<Common.FindPaginatedPetsDefaultResponse>), * 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); const infiniteOptions = `initialPageParam: ${formatInitialPageParam(ctx, op)}, getNextPageParam: ${buildGetNextPageParamExpr(ctx, op)}`; // The pagination fields are re-added as optional overrides: `...options` is // spread after the defaults, so replacing them works at runtime and must not // be forbidden by the type (#203). getNextPageParam is spelled out rather // than Picked, because FetchInfiniteQueryOptions only exposes it on the // union member that also requires `pages`. const pageType = getPageType(op); const instantiated = `FetchInfiniteQueryOptions<${pageType}>`; const overrides = `Partial<Pick<${instantiated}, "initialPageParam">> & { getNextPageParam?: GetNextPageParamFunction<unknown, ${pageType}> }`; const optionsParam = `options?: Omit<${instantiated}, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"> & ${overrides}`; 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> = {}, options?: Omit<EnsureQueryDataOptions<Common.FindPetsDefaultResponse>, "queryKey" | "queryFn">) => * queryClient.ensureQueryData({ * queryKey: Common.UseFindPetsKeyFn(clientOptions), * queryFn: () => findPets({ ...clientOptions, throwOnError: true }).then(response => response.data), * ...options * }); */ export function buildEnsureQueryDataFn(op) { const fnName = `ensureUse${op.capitalizedMethodName}Data`; const queryFn = `({ signal }) => ${op.methodName}(${QUERY_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 { 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, ${buildClientOptionsParam(op)}, ${optionsParam}) => ${body}`, }, ], }; }