import { type InfiniteData, type QueryClient, type QueryFunctionContext, type SkipToken, type UseInfiniteQueryOptions, type UseInfiniteQueryResult, type UseMutationOptions, type UseMutationResult, type UseQueryOptions, type UseQueryResult, type UseSuspenseQueryOptions, type UseSuspenseQueryResult, useInfiniteQuery, useMutation, useQuery, useSuspenseQuery, } from "@tanstack/react-query"; import type { ClientMethod, DefaultParamsOption, Client as FetchClient, FetchResponse, MaybeOptionalInit, } from "openapi-fetch"; import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers"; // Helper type to dynamically infer the type from the `select` property type InferSelectReturnType = TSelect extends (data: TData) => infer R ? R : TData; type InitWithUnknowns = Init & { [key: string]: unknown }; export type QueryKey< Paths extends Record>, Method extends HttpMethod, Path extends PathsWithMethod, Init = MaybeOptionalInit, > = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init]; export type QueryOptionsFunction>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseQueryOptions< Response["data"], Response["error"], InferSelectReturnType, QueryKey >, "queryKey" | "queryFn" >, >( method: Method, path: Path, ...[init, options]: RequiredKeysOf extends never ? [InitWithUnknowns?, Options?] : [InitWithUnknowns, Options?] ) => NoInfer< Omit< UseQueryOptions< Response["data"], Response["error"], InferSelectReturnType, QueryKey >, "queryFn" > & { queryFn: Exclude< UseQueryOptions< Response["data"], Response["error"], InferSelectReturnType, QueryKey >["queryFn"], SkipToken | undefined >; } >; export type UseQueryMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseQueryOptions< Response["data"], Response["error"], InferSelectReturnType, QueryKey >, "queryKey" | "queryFn" >, >( method: Method, url: Path, ...[init, options, queryClient]: RequiredKeysOf extends never ? [InitWithUnknowns?, Options?, QueryClient?] : [InitWithUnknowns, Options?, QueryClient?] ) => UseQueryResult, Response["error"]>; export type UseInfiniteQueryMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, Options extends Omit< UseInfiniteQueryOptions< Response["data"], Response["error"], InferSelectReturnType, Options["select"]>, QueryKey, unknown >, "queryKey" | "queryFn" > & { pageParamName?: string; }, >( method: Method, url: Path, init: InitWithUnknowns, options: Options, queryClient?: QueryClient, ) => UseInfiniteQueryResult< InferSelectReturnType, Options["select"]>, Response["error"] >; export type UseSuspenseQueryMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types Options extends Omit< UseSuspenseQueryOptions< Response["data"], Response["error"], InferSelectReturnType, QueryKey >, "queryKey" | "queryFn" >, >( method: Method, url: Path, ...[init, options, queryClient]: RequiredKeysOf extends never ? [InitWithUnknowns?, Options?, QueryClient?] : [InitWithUnknowns, Options?, QueryClient?] ) => UseSuspenseQueryResult, Response["error"]>; export type UseMutationMethod>, Media extends MediaType> = < Method extends HttpMethod, Path extends PathsWithMethod, Init extends MaybeOptionalInit, Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types TOnMutateResult = unknown, >( method: Method, url: Path, options?: Omit< UseMutationOptions, "mutationKey" | "mutationFn" >, queryClient?: QueryClient, ) => UseMutationResult; export interface OpenapiQueryClient { queryOptions: QueryOptionsFunction; useQuery: UseQueryMethod; useSuspenseQuery: UseSuspenseQueryMethod; useInfiniteQuery: UseInfiniteQueryMethod; useMutation: UseMutationMethod; } export type MethodResponse< CreatedClient extends OpenapiQueryClient, Method extends HttpMethod, Path extends CreatedClient extends OpenapiQueryClient ? PathsWithMethod : never, Options = object, > = CreatedClient extends OpenapiQueryClient ? NonNullable["data"]> : never; // TODO: Add the ability to bring queryClient as argument export default function createClient( client: FetchClient, ): OpenapiQueryClient { const queryFn = async >({ queryKey: [method, path, init], signal, }: QueryFunctionContext>) => { const mth = method.toUpperCase() as Uppercase; const fn = client[mth] as ClientMethod; const { data, error, response } = await fn(path, { signal, ...(init as any) }); // TODO: find a way to avoid as any if (error) { throw error; } if (response.status === 204 || response.headers.get("Content-Length") === "0") { return data ?? null; } return data; }; const queryOptions: QueryOptionsFunction = (method, path, ...[init, options]) => ({ queryKey: (init === undefined ? ([method, path] as const) : ([method, path, init] as const)) as QueryKey< Paths, typeof method, typeof path >, queryFn, ...options, }); return { queryOptions, useQuery: (method, path, ...[init, options, queryClient]) => useQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), useSuspenseQuery: (method, path, ...[init, options, queryClient]) => useSuspenseQuery(queryOptions(method, path, init as InitWithUnknowns, options), queryClient), useInfiniteQuery: (method, path, init, options, queryClient) => { const { pageParamName = "cursor", ...restOptions } = options; const { queryKey } = queryOptions(method, path, init); return useInfiniteQuery( { queryKey, queryFn: async ({ queryKey: [method, path, init], pageParam = 0, signal }) => { const mth = method.toUpperCase() as Uppercase; const fn = client[mth] as ClientMethod; const mergedInit = { ...init, signal, params: { ...(init?.params || {}), query: { ...(init?.params as { query?: DefaultParamsOption })?.query, [pageParamName]: pageParam, }, }, }; const { data, error } = await fn(path, mergedInit as any); if (error) { throw error; } return data; }, ...restOptions, }, queryClient, ); }, useMutation: (method, path, options, queryClient) => useMutation( { mutationKey: [method, path], mutationFn: async (init) => { const mth = method.toUpperCase() as Uppercase; const fn = client[mth] as ClientMethod; const { data, error } = await fn(path, init as InitWithUnknowns); if (error) { throw error; } return data as Exclude; }, ...options, }, queryClient, ), }; }