@@ -2,14 +2,6 @@ import { capitalize } from "pastable/server";
22import { prettify } from "./format.ts" ;
33import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts" ;
44
5- // Default error status codes (4xx and 5xx ranges)
6- export const DEFAULT_ERROR_STATUS_CODES = [
7- 400 , 401 , 402 , 403 , 404 , 405 , 406 , 407 , 408 , 409 , 410 , 411 , 412 , 413 , 414 , 415 , 416 , 417 , 418 , 421 , 422 , 423 , 424 ,
8- 425 , 426 , 428 , 429 , 431 , 451 , 500 , 501 , 502 , 503 , 504 , 505 , 506 , 507 , 508 , 510 , 511 ,
9- ] as const ;
10-
11- export type ErrorStatusCode = ( typeof DEFAULT_ERROR_STATUS_CODES ) [ number ] ;
12-
135type GeneratorOptions = ReturnType < typeof mapOpenApiEndpoints > ;
146type GeneratorContext = Required < GeneratorOptions > & {
157 errorStatusCodes ?: readonly number [ ] ;
@@ -18,12 +10,10 @@ type GeneratorContext = Required<GeneratorOptions> & {
1810export const generateTanstackQueryFile = async ( ctx : GeneratorContext & { relativeApiClientPath : string } ) => {
1911 const endpointMethods = new Set ( ctx . endpointList . map ( ( endpoint ) => endpoint . method . toLowerCase ( ) ) ) ;
2012
21- // Use configured error status codes or default
22- const errorStatusCodes = ctx . errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES ;
23-
2413 const file = `
2514 import { queryOptions } from "@tanstack/react-query"
26- import type { EndpointByMethod, ApiClient, SafeApiResponse } from "${ ctx . relativeApiClientPath } "
15+ import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus } from "${ ctx . relativeApiClientPath } "
16+ import { errorStatusCodes, TypedResponseError } from "${ ctx . relativeApiClientPath } "
2717
2818 type EndpointQueryKey<TOptions extends EndpointParameters> = [
2919 TOptions & {
@@ -76,8 +66,6 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
7666
7767 type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
7868
79- type ErrorStatusCode = ${ errorStatusCodes . join ( " | " ) } ;
80-
8169 // </ApiClientTypes>
8270
8371 // <ApiClient>
@@ -97,6 +85,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
9785 /** type-only property if you need easy access to the endpoint params */
9886 "~endpoint": {} as TEndpoint,
9987 queryKey,
88+ queryFn: {} as "You need to pass .queryOptions to the useQuery hook",
10089 queryOptions: queryOptions({
10190 queryFn: async ({ queryKey, signal, }) => {
10291 const requestParams = {
@@ -110,6 +99,7 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
11099 },
111100 queryKey: queryKey
112101 }),
102+ mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
113103 mutationOptions: {
114104 mutationKey: queryKey,
115105 mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
@@ -134,84 +124,65 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
134124
135125 // <ApiClient.request>
136126 /**
137- * Generic mutation method with full type-safety for any endpoint that doesnt require parameters to be passed initially
127+ * Generic mutation method with full type-safety for any endpoint; it doesnt require parameters to be passed initially
128+ * but instead will require them to be passed when calling the mutation.mutate() method
138129 */
139130 mutation<
140131 TMethod extends keyof EndpointByMethod,
141132 TPath extends keyof EndpointByMethod[TMethod],
142133 TEndpoint extends EndpointByMethod[TMethod][TPath],
143134 TWithResponse extends boolean = false,
144135 TSelection = TWithResponse extends true
145- ? SafeApiResponse <TEndpoint>
136+ ? InferResponseByStatus <TEndpoint, SuccessStatusCode >
146137 : TEndpoint extends { response: infer Res } ? Res : never,
147138 TError = TEndpoint extends { responses: infer TResponses }
148139 ? TResponses extends Record<string | number, unknown>
149- ? {
150- [K in keyof TResponses]: K extends string
151- ? K extends \`\${infer TStatusCode extends number}\`
152- ? TStatusCode extends ErrorStatusCode
153- ? Omit<Response, 'status'> & { status: TStatusCode; data: TResponses[K] }
154- : never
155- : never
156- : K extends number
157- ? K extends ErrorStatusCode
158- ? Omit<Response, 'status'> & { status: K; data: TResponses[K] }
159- : never
160- : never;
161- }[keyof TResponses]
140+ ? InferResponseByStatus<TEndpoint, ErrorStatusCode>
162141 : Error
163142 : Error
164143 >(method: TMethod, path: TPath, options?: {
165144 withResponse?: TWithResponse;
166145 selectFn?: (res: TWithResponse extends true
167- ? SafeApiResponse <TEndpoint>
146+ ? InferResponseByStatus <TEndpoint, SuccessStatusCode >
168147 : TEndpoint extends { response: infer Res } ? Res : never
169148 ) => TSelection;
149+ throwOnStatusError?: boolean
170150 }) {
171151 const mutationKey = [{ method, path }] as const;
172152 return {
173153 /** type-only property if you need easy access to the endpoint params */
174154 "~endpoint": {} as TEndpoint,
175155 mutationKey: mutationKey,
156+ mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
176157 mutationOptions: {
177158 mutationKey: mutationKey,
178- mutationFn: async (params: TEndpoint extends { parameters: infer Parameters } ? Parameters : never): Promise<TSelection> => {
179- const withResponse = options?.withResponse ?? false;
159+ mutationFn: async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
160+ ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
161+ : TEndpoint extends { response: infer Res }
162+ ? Res
163+ : never>
164+ (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
165+ withResponse?: TLocalWithResponse;
166+ throwOnStatusError?: boolean;
167+ }): Promise<TLocalSelection> => {
168+ const withResponse = params.withResponse ??options?.withResponse ?? false;
169+ const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
180170 const selectFn = options?.selectFn;
171+ const response = await (this.client as any)[method](path, { ...params as any, withResponse: true, throwOnStatusError: false });
181172
182- if (withResponse) {
183- // Type assertion is safe because we're handling the method dynamically
184- const response = await (this.client as any)[method](path, { ...params as any, withResponse: true });
185- if (!response.ok) {
186- // Create a Response-like error object with additional data property
187- const error = Object.assign(Object.create(Response.prototype), {
188- ...response,
189- data: response.data
190- }) as TError;
191- throw error;
192- }
193- const res = selectFn ? selectFn(response as any) : response;
194- return res as TSelection;
195- }
196-
197- // Type assertion is safe because we're handling the method dynamically
198- // Always get the full response for error handling, even when withResponse is false
199- const response = await (this.client as any)[method](path, { ...params as any, withResponse: true });
200- if (!response.ok) {
201- // Create a Response-like error object with additional data property
202- const error = Object.assign(Object.create(Response.prototype), {
203- ...response,
204- data: response.data
205- }) as TError;
206- throw error;
173+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
174+ throw new TypedResponseError(response as never);
207175 }
208176
209177 // Return just the data if withResponse is false, otherwise return the full response
210178 const finalResponse = withResponse ? response : response.data;
211179 const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
212- return res as TSelection ;
180+ return res as never ;
213181 }
214- } as import("@tanstack/react-query").UseMutationOptions<TSelection, TError, TEndpoint extends { parameters: infer Parameters } ? Parameters : never>,
182+ } satisfies import("@tanstack/react-query").UseMutationOptions<TSelection, TError, (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
183+ withResponse?: boolean;
184+ throwOnStatusError?: boolean;
185+ }>,
215186 }
216187 }
217188 // </ApiClient.request>
0 commit comments