Skip to content

Commit 4d68d5b

Browse files
committed
refactor: throw a Response
1 parent 9d3fe0d commit 4d68d5b

File tree

3 files changed

+51
-16
lines changed

3 files changed

+51
-16
lines changed

.changeset/true-lemons-think.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Add comprehensive type-safe error handling and configurable status codes
1010
- Advanced mutation options supporting `withResponse` and `selectFn` parameters
1111
- Automatic error type inference based on OpenAPI error schemas instead of generic Error type
1212
- Type-safe error handling with discriminated unions for mutations
13+
- Response-like error objects that extend Response with additional `data` property for consistency
1314
- **Configurable status codes**: Made success and error status codes fully configurable:
1415
- New `--success-status-codes` and `--error-status-codes` CLI options
1516
- `GeneratorOptions` now accepts `successStatusCodes` and `errorStatusCodes` arrays

packages/typed-openapi/src/tanstack-query.generator.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ export const DEFAULT_ERROR_STATUS_CODES = [
1010

1111
export type ErrorStatusCode = (typeof DEFAULT_ERROR_STATUS_CODES)[number];
1212

13-
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
13+
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>;
14+
type GeneratorContext = Required<GeneratorOptions> & {
1415
errorStatusCodes?: readonly number[];
1516
};
16-
type GeneratorContext = Required<GeneratorOptions>;
1717

1818
export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relativeApiClientPath: string }) => {
1919
const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
@@ -150,12 +150,12 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
150150
[K in keyof TResponses]: K extends string
151151
? K extends \`\${infer TStatusCode extends number}\`
152152
? TStatusCode extends ErrorStatusCode
153-
? { status: TStatusCode; data: TResponses[K] }
153+
? Omit<Response, 'status'> & { status: TStatusCode; data: TResponses[K] }
154154
: never
155155
: never
156156
: K extends number
157157
? K extends ErrorStatusCode
158-
? { status: K; data: TResponses[K] }
158+
? Omit<Response, 'status'> & { status: K; data: TResponses[K] }
159159
: never
160160
: never;
161161
}[keyof TResponses]
@@ -183,16 +183,32 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
183183
// Type assertion is safe because we're handling the method dynamically
184184
const response = await (this.client as any)[method](path, { ...params as any, withResponse: true });
185185
if (!response.ok) {
186-
const error = { status: response.status, data: response.data } as TError;
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;
187191
throw error;
188192
}
189193
const res = selectFn ? selectFn(response as any) : response;
190194
return res as TSelection;
191195
}
192196
193197
// Type assertion is safe because we're handling the method dynamically
194-
const response = await (this.client as any)[method](path, { ...params as any, withResponse: false });
195-
const res = selectFn ? selectFn(response as any) : response;
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;
207+
}
208+
209+
// Return just the data if withResponse is false, otherwise return the full response
210+
const finalResponse = withResponse ? response : response.data;
211+
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
196212
return res as TSelection;
197213
}
198214
} as import("@tanstack/react-query").UseMutationOptions<TSelection, TError, TEndpoint extends { parameters: infer Parameters } ? Parameters : never>,

packages/typed-openapi/tests/tanstack-query.generator.test.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { generateTanstackQueryFile } from "../src/tanstack-query.generator.ts";
77
describe("generator", () => {
88
test("petstore", async ({ expect }) => {
99
const openApiDoc = (await SwaggerParser.parse("./tests/samples/petstore.yaml")) as OpenAPIObject;
10-
expect(await generateTanstackQueryFile({
11-
...mapOpenApiEndpoints(openApiDoc),
12-
relativeApiClientPath: "./api.client.ts"
13-
})).toMatchInlineSnapshot(`
10+
expect(
11+
await generateTanstackQueryFile({
12+
...mapOpenApiEndpoints(openApiDoc),
13+
relativeApiClientPath: "./api.client.ts",
14+
}),
15+
).toMatchInlineSnapshot(`
1416
"import { queryOptions } from "@tanstack/react-query";
1517
import type { EndpointByMethod, ApiClient, SafeApiResponse } from "./api.client.ts";
1618
@@ -302,12 +304,12 @@ describe("generator", () => {
302304
[K in keyof TResponses]: K extends string
303305
? K extends \`\${infer TStatusCode extends number}\`
304306
? TStatusCode extends ErrorStatusCode
305-
? { status: TStatusCode; data: TResponses[K] }
307+
? Omit<Response, "status"> & { status: TStatusCode; data: TResponses[K] }
306308
: never
307309
: never
308310
: K extends number
309311
? K extends ErrorStatusCode
310-
? { status: K; data: TResponses[K] }
312+
? Omit<Response, "status"> & { status: K; data: TResponses[K] }
311313
: never
312314
: never;
313315
}[keyof TResponses]
@@ -344,16 +346,32 @@ describe("generator", () => {
344346
// Type assertion is safe because we're handling the method dynamically
345347
const response = await (this.client as any)[method](path, { ...(params as any), withResponse: true });
346348
if (!response.ok) {
347-
const error = { status: response.status, data: response.data } as TError;
349+
// Create a Response-like error object with additional data property
350+
const error = Object.assign(Object.create(Response.prototype), {
351+
...response,
352+
data: response.data,
353+
}) as TError;
348354
throw error;
349355
}
350356
const res = selectFn ? selectFn(response as any) : response;
351357
return res as TSelection;
352358
}
353359
354360
// Type assertion is safe because we're handling the method dynamically
355-
const response = await (this.client as any)[method](path, { ...(params as any), withResponse: false });
356-
const res = selectFn ? selectFn(response as any) : response;
361+
// Always get the full response for error handling, even when withResponse is false
362+
const response = await (this.client as any)[method](path, { ...(params as any), withResponse: true });
363+
if (!response.ok) {
364+
// Create a Response-like error object with additional data property
365+
const error = Object.assign(Object.create(Response.prototype), {
366+
...response,
367+
data: response.data,
368+
}) as TError;
369+
throw error;
370+
}
371+
372+
// Return just the data if withResponse is false, otherwise return the full response
373+
const finalResponse = withResponse ? response : response.data;
374+
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
357375
return res as TSelection;
358376
},
359377
} as import("@tanstack/react-query").UseMutationOptions<

0 commit comments

Comments
 (0)