Skip to content

Commit 57217a8

Browse files
committed
feat: configurable error status codes
1 parent d5bcebb commit 57217a8

29 files changed

+200
-198
lines changed

packages/typed-openapi/src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ cli
2020
"--success-status-codes <codes>",
2121
"Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)",
2222
)
23+
.option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)")
2324
.option(
2425
"--tanstack [name]",
2526
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file",

packages/typed-openapi/src/generate-client-files.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import type { OpenAPIObject } from "openapi3-ts/oas31";
33
import { basename, join, dirname } from "pathe";
44
import { type } from "arktype";
55
import { mkdir, writeFile } from "fs/promises";
6-
import { allowedRuntimes, generateFile } from "./generator.ts";
6+
import {
7+
allowedRuntimes,
8+
generateFile,
9+
DEFAULT_SUCCESS_STATUS_CODES,
10+
DEFAULT_ERROR_STATUS_CODES,
11+
} from "./generator.ts";
712
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
813
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
914
import { prettify } from "./format.ts";
@@ -26,6 +31,7 @@ export const optionsSchema = type({
2631
schemasOnly: "boolean",
2732
"includeClient?": "boolean | 'true' | 'false'",
2833
"successStatusCodes?": "string",
34+
"errorStatusCodes?": "string",
2935
});
3036

3137
export async function generateClientFiles(input: string, options: typeof optionsSchema.infer) {
@@ -39,19 +45,25 @@ export async function generateClientFiles(input: string, options: typeof options
3945
? (options.successStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
4046
: undefined;
4147

48+
// Parse error status codes if provided
49+
const errorStatusCodes = options.errorStatusCodes
50+
? (options.errorStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
51+
: undefined;
52+
4253
// Convert string boolean to actual boolean
4354
const includeClient =
4455
options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
4556

46-
const content = await prettify(
47-
generateFile({
48-
...ctx,
49-
runtime: options.runtime,
50-
schemasOnly: options.schemasOnly,
51-
...(includeClient !== undefined && { includeClient }),
52-
...(successStatusCodes !== undefined && { successStatusCodes }),
53-
}),
54-
);
57+
const generatorOptions = {
58+
...ctx,
59+
runtime: options.runtime,
60+
schemasOnly: options.schemasOnly,
61+
includeClient: includeClient ?? true,
62+
successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
63+
errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
64+
};
65+
66+
const content = await prettify(generateFile(generatorOptions));
5567
const outputPath = join(
5668
cwd,
5769
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
@@ -63,7 +75,7 @@ export async function generateClientFiles(input: string, options: typeof options
6375

6476
if (options.tanstack) {
6577
const tanstackContent = await generateTanstackQueryFile({
66-
...ctx,
78+
...generatorOptions,
6779
relativeApiClientPath: "./" + basename(outputPath),
6880
});
6981
const tanstackOutputPath = join(

packages/typed-openapi/src/generator.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ export const DEFAULT_SUCCESS_STATUS_CODES = [
1212
200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308,
1313
] as const;
1414

15+
// Default error status codes (4xx and 5xx ranges)
16+
export const DEFAULT_ERROR_STATUS_CODES = [
17+
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424,
18+
425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
19+
] as const;
20+
21+
export type ErrorStatusCode = (typeof DEFAULT_ERROR_STATUS_CODES)[number];
22+
1523
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
1624
runtime?: "none" | keyof typeof runtimeValidationGenerator;
1725
schemasOnly?: boolean;
1826
successStatusCodes?: readonly number[];
27+
errorStatusCodes?: readonly number[];
1928
includeClient?: boolean;
2029
};
2130
type GeneratorContext = Required<GeneratorOptions>;
@@ -71,6 +80,7 @@ export const generateFile = (options: GeneratorOptions) => {
7180
...options,
7281
runtime: options.runtime ?? "none",
7382
successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
83+
errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
7484
includeClient: options.includeClient ?? true,
7585
} as GeneratorContext;
7686

@@ -338,7 +348,7 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
338348
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
339349
340350
// Status code type for success responses
341-
export type StatusCode = ${statusCodeType};
351+
export type SuccessStatusCode = ${statusCodeType};
342352
343353
// Error handling types
344354
export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
@@ -352,7 +362,7 @@ export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | num
352362
: {
353363
[K in keyof TAllResponses]: K extends string
354364
? K extends \`\${infer TStatusCode extends number}\`
355-
? TStatusCode extends StatusCode
365+
? TStatusCode extends SuccessStatusCode
356366
? Omit<Response, "ok" | "status" | "json"> & {
357367
ok: true;
358368
status: TStatusCode;
@@ -367,7 +377,7 @@ export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | num
367377
}
368378
: never
369379
: K extends number
370-
? K extends StatusCode
380+
? K extends SuccessStatusCode
371381
? Omit<Response, "ok" | "status" | "json"> & {
372382
ok: true;
373383
status: K;

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,25 @@ import { capitalize } from "pastable/server";
22
import { prettify } from "./format.ts";
33
import type { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
44

5-
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>;
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+
13+
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
14+
errorStatusCodes?: readonly number[];
15+
};
616
type GeneratorContext = Required<GeneratorOptions>;
717

818
export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relativeApiClientPath: string }) => {
919
const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
1020

21+
// Use configured error status codes or default
22+
const errorStatusCodes = ctx.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES;
23+
1124
const file = `
1225
import { queryOptions } from "@tanstack/react-query"
1326
import type { EndpointByMethod, ApiClient, SafeApiResponse } from "${ctx.relativeApiClientPath}"
@@ -63,6 +76,8 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
6376
6477
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
6578
79+
type ErrorStatusCode = ${errorStatusCodes.join(" | ")};
80+
6681
// </ApiClientTypes>
6782
6883
// <ApiClient>
@@ -133,13 +148,13 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
133148
? TResponses extends Record<string | number, unknown>
134149
? {
135150
[K in keyof TResponses]: K extends string
136-
? K extends \`\${infer StatusCode extends number}\`
137-
? StatusCode extends 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511
138-
? { status: StatusCode; data: TResponses[K] }
151+
? K extends \`\${infer TStatusCode extends number}\`
152+
? TStatusCode extends ErrorStatusCode
153+
? { status: TStatusCode; data: TResponses[K] }
139154
: never
140155
: never
141156
: K extends number
142-
? K extends 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511
157+
? K extends ErrorStatusCode
143158
? { status: K; data: TResponses[K] }
144159
: never
145160
: never;

packages/typed-openapi/tests/configurable-status-codes.test.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,50 +18,52 @@ it("should use custom success status codes", async () => {
1818
description: "Success",
1919
content: {
2020
"application/json": {
21-
schema: { type: "object", properties: { message: { type: "string" } } }
22-
}
23-
}
21+
schema: { type: "object", properties: { message: { type: "string" } } },
22+
},
23+
},
2424
},
2525
201: {
2626
description: "Created",
2727
content: {
2828
"application/json": {
29-
schema: { type: "object", properties: { id: { type: "string" } } }
30-
}
31-
}
29+
schema: { type: "object", properties: { id: { type: "string" } } },
30+
},
31+
},
3232
},
3333
400: {
3434
description: "Bad Request",
3535
content: {
3636
"application/json": {
37-
schema: { type: "object", properties: { error: { type: "string" } } }
38-
}
39-
}
40-
}
41-
}
42-
}
43-
}
44-
}
37+
schema: { type: "object", properties: { error: { type: "string" } } },
38+
},
39+
},
40+
},
41+
},
42+
},
43+
},
44+
},
4545
};
4646

4747
const endpoints = mapOpenApiEndpoints(openApiDoc);
4848

4949
// Test with default success status codes (should include 200 and 201)
5050
const defaultGenerated = await prettify(generateFile(endpoints));
51-
expect(defaultGenerated).toContain("export type StatusCode =");
51+
expect(defaultGenerated).toContain("export type SuccessStatusCode =");
5252
expect(defaultGenerated).toContain("| 200");
5353
expect(defaultGenerated).toContain("| 201");
5454

5555
// Test with custom success status codes (only 200)
56-
const customGenerated = await prettify(generateFile({
57-
...endpoints,
58-
successStatusCodes: [200] as const
59-
}));
56+
const customGenerated = await prettify(
57+
generateFile({
58+
...endpoints,
59+
successStatusCodes: [200] as const,
60+
}),
61+
);
6062

6163
// Should only contain 200 in the StatusCode type
62-
expect(customGenerated).toContain("export type StatusCode = 200;");
64+
expect(customGenerated).toContain("export type SuccessStatusCode = 200;");
6365
expect(customGenerated).not.toContain("| 201");
6466

6567
// The ApiResponse type should use the custom StatusCode
66-
expect(customGenerated).toContain("TStatusCode extends StatusCode");
68+
expect(customGenerated).toContain("TStatusCode extends SuccessStatusCode");
6769
});

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ describe("generator", () => {
326326
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
327327
328328
// Status code type for success responses
329-
export type StatusCode =
329+
export type SuccessStatusCode =
330330
| 200
331331
| 201
332332
| 202
@@ -361,7 +361,7 @@ describe("generator", () => {
361361
: {
362362
[K in keyof TAllResponses]: K extends string
363363
? K extends \`\${infer TStatusCode extends number}\`
364-
? TStatusCode extends StatusCode
364+
? TStatusCode extends SuccessStatusCode
365365
? Omit<Response, "ok" | "status" | "json"> & {
366366
ok: true;
367367
status: TStatusCode;
@@ -376,7 +376,7 @@ describe("generator", () => {
376376
}
377377
: never
378378
: K extends number
379-
? K extends StatusCode
379+
? K extends SuccessStatusCode
380380
? Omit<Response, "ok" | "status" | "json"> & {
381381
ok: true;
382382
status: K;
@@ -1014,7 +1014,7 @@ describe("generator", () => {
10141014
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
10151015
10161016
// Status code type for success responses
1017-
export type StatusCode =
1017+
export type SuccessStatusCode =
10181018
| 200
10191019
| 201
10201020
| 202
@@ -1049,7 +1049,7 @@ describe("generator", () => {
10491049
: {
10501050
[K in keyof TAllResponses]: K extends string
10511051
? K extends \`\${infer TStatusCode extends number}\`
1052-
? TStatusCode extends StatusCode
1052+
? TStatusCode extends SuccessStatusCode
10531053
? Omit<Response, "ok" | "status" | "json"> & {
10541054
ok: true;
10551055
status: TStatusCode;
@@ -1064,7 +1064,7 @@ describe("generator", () => {
10641064
}
10651065
: never
10661066
: K extends number
1067-
? K extends StatusCode
1067+
? K extends SuccessStatusCode
10681068
? Omit<Response, "ok" | "status" | "json"> & {
10691069
ok: true;
10701070
status: K;
@@ -1363,7 +1363,7 @@ describe("generator", () => {
13631363
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
13641364
13651365
// Status code type for success responses
1366-
export type StatusCode =
1366+
export type SuccessStatusCode =
13671367
| 200
13681368
| 201
13691369
| 202
@@ -1398,7 +1398,7 @@ describe("generator", () => {
13981398
: {
13991399
[K in keyof TAllResponses]: K extends string
14001400
? K extends \`\${infer TStatusCode extends number}\`
1401-
? TStatusCode extends StatusCode
1401+
? TStatusCode extends SuccessStatusCode
14021402
? Omit<Response, "ok" | "status" | "json"> & {
14031403
ok: true;
14041404
status: TStatusCode;
@@ -1413,7 +1413,7 @@ describe("generator", () => {
14131413
}
14141414
: never
14151415
: K extends number
1416-
? K extends StatusCode
1416+
? K extends SuccessStatusCode
14171417
? Omit<Response, "ok" | "status" | "json"> & {
14181418
ok: true;
14191419
status: K;

packages/typed-openapi/tests/include-client.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ it("should exclude API client when includeClient is false", async () => {
4343
expect(withoutClient).not.toContain("// <ApiClient>");
4444
expect(withoutClient).not.toContain("export class ApiClient");
4545
expect(withoutClient).not.toContain("export type EndpointParameters");
46-
expect(withoutClient).not.toContain("export type StatusCode");
46+
expect(withoutClient).not.toContain("export type SuccessStatusCode");
4747
expect(withoutClient).not.toContain("export type TypedApiResponse");
4848

4949
// Should still contain schemas and endpoints
@@ -64,6 +64,6 @@ it("should exclude API client when includeClient is false", async () => {
6464
expect(withClient).toContain("// <ApiClient>");
6565
expect(withClient).toContain("export class ApiClient");
6666
expect(withClient).toContain("export type EndpointParameters");
67-
expect(withClient).toContain("export type StatusCode");
67+
expect(withClient).toContain("export type SuccessStatusCode");
6868
expect(withClient).toContain("export type TypedApiResponse");
6969
});

packages/typed-openapi/tests/multiple-success-responses.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ describe("multiple success responses", () => {
184184
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
185185
186186
// Status code type for success responses
187-
export type StatusCode =
187+
export type SuccessStatusCode =
188188
| 200
189189
| 201
190190
| 202
@@ -219,7 +219,7 @@ describe("multiple success responses", () => {
219219
: {
220220
[K in keyof TAllResponses]: K extends string
221221
? K extends \`\${infer TStatusCode extends number}\`
222-
? TStatusCode extends StatusCode
222+
? TStatusCode extends SuccessStatusCode
223223
? Omit<Response, "ok" | "status" | "json"> & {
224224
ok: true;
225225
status: TStatusCode;
@@ -234,7 +234,7 @@ describe("multiple success responses", () => {
234234
}
235235
: never
236236
: K extends number
237-
? K extends StatusCode
237+
? K extends SuccessStatusCode
238238
? Omit<Response, "ok" | "status" | "json"> & {
239239
ok: true;
240240
status: K;

0 commit comments

Comments
 (0)