import { capitalize } from "pastable/server"; import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts"; type GeneratorOptions = ReturnType; type GeneratorContext = Required & { errorStatusCodes?: readonly number[]; }; export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relativeApiClientPath: string }) => { const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase())); const file = ` import { queryOptions } from "@tanstack/react-query" import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus, TypedSuccessResponse } from "${ctx.relativeApiClientPath}" import { errorStatusCodes, TypedStatusError } from "${ctx.relativeApiClientPath}" type EndpointQueryKey = [ TOptions & { _id: string; _infinite?: boolean; } ]; const createQueryKey = (id: string, options?: TOptions, infinite?: boolean): [ EndpointQueryKey[0] ] => { const params: EndpointQueryKey[0] = { _id: id, } as EndpointQueryKey[0]; if (infinite) { params._infinite = infinite; } if (options?.body) { params.body = options.body; } if (options?.header) { params.header = options.header; } if (options?.path) { params.path = options.path; } if (options?.query) { params.query = options.query; } return [ params ]; }; // ${Array.from(endpointMethods) .map((method) => `export type ${capitalize(method)}Endpoints = EndpointByMethod["${method}"];`) .join("\n")} // // export type EndpointParameters = { body?: unknown; query?: Record; header?: Record; path?: Record; }; type RequiredKeys = { [P in keyof T]-?: undefined extends T[P] ? never : P; }[keyof T]; type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T]; type InferResponseData = TypedSuccessResponse extends InferResponseByStatus ? Extract, { data: {}}>["data"] : Extract["data"], {}>; // // export class TanstackQueryApiClient { constructor(public client: ApiClient) { } ${Array.from(endpointMethods) .map( (method) => ` // ${method}< Path extends keyof ${capitalize(method)}Endpoints, TEndpoint extends ${capitalize(method)}Endpoints[Path] >( path: Path, ...params: MaybeOptionalArg ) { const queryKey = createQueryKey(path as string, params[0]); const query = { /** type-only property if you need easy access to the endpoint params */ "~endpoint": {} as TEndpoint, queryKey, queryFn: {} as "You need to pass .queryOptions to the useQuery hook", queryOptions: queryOptions({ queryFn: async ({ queryKey, signal, }) => { const requestParams = { ...(params[0] || {}), ...(queryKey[0] || {}), overrides: { signal }, withResponse: false as const }; const res = await this.client.${method}(path, requestParams as never); return res as InferResponseData; }, queryKey: queryKey }), }; return query } // `, ) .join("\n")} // /** * Generic mutation method with full type-safety for any endpoint; it doesnt require parameters to be passed initially * but instead will require them to be passed when calling the mutation.mutate() method */ mutation< TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath], TWithResponse extends boolean = false, TSelection = TWithResponse extends true ? InferResponseByStatus : InferResponseData, TError = TEndpoint extends { responses: infer TResponses } ? TResponses extends Record ? TypedStatusError> : Error : Error >(method: TMethod, path: TPath, options?: { withResponse?: TWithResponse; selectFn?: (res: TWithResponse extends true ? InferResponseByStatus : InferResponseData ) => TSelection; throwOnStatusError?: boolean throwOnError?: boolean | ((error: TError) => boolean) }) { const mutationKey = [{ method, path }] as const; const mutationFn = async (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & { throwOnStatusError?: boolean; overrides?: RequestInit; }): Promise => { const withResponse = options?.withResponse ?? false; const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true); const selectFn = options?.selectFn; const response = await (this.client as any)[method](path, { ...params as any, withResponse: true, throwOnStatusError: false, }); if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) { throw new TypedStatusError(response as never); } // Return just the data if withResponse is false, otherwise return the full response const finalResponse = withResponse ? response : response.data; const res = selectFn ? selectFn(finalResponse as any) : finalResponse; return res as never; }; return { /** type-only property if you need easy access to the endpoint params */ "~endpoint": {} as TEndpoint, mutationKey: mutationKey, mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook", mutationOptions: { throwOnError: options?.throwOnError as boolean | ((error: TError) => boolean), mutationKey: mutationKey, mutationFn: mutationFn, } as Omit, "mutationFn"> & { mutationFn: typeof mutationFn }, } } // } `; return file; };