typed-openapi 1.5.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OVT6OLBK.js → chunk-E6A7N4ND.js} +318 -41
- package/dist/{chunk-O7DZWQK4.js → chunk-MCVYB63W.js} +21 -11
- package/dist/cli.js +7 -4
- package/dist/index.d.ts +8 -3
- package/dist/index.js +1 -1
- package/dist/node.export.d.ts +4 -1
- package/dist/node.export.js +2 -2
- package/dist/{types-DLE5RaXi.d.ts → types-DsI2d-HE.d.ts} +2 -0
- package/package.json +6 -1
- package/src/cli.ts +8 -3
- package/src/generate-client-files.ts +36 -10
- package/src/generator.ts +185 -20
- package/src/map-openapi-endpoints.ts +46 -2
- package/src/tanstack-query.generator.ts +69 -25
package/dist/node.export.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as arktype_internal_methods_object_ts from 'arktype/internal/methods/object.ts';
|
|
2
|
-
import { N as NameTransformOptions } from './types-
|
|
2
|
+
import { N as NameTransformOptions } from './types-DsI2d-HE.js';
|
|
3
3
|
import 'openapi3-ts/oas31';
|
|
4
4
|
import 'openapi3-ts/oas30';
|
|
5
5
|
|
|
@@ -8,6 +8,9 @@ declare const optionsSchema: arktype_internal_methods_object_ts.ObjectType<{
|
|
|
8
8
|
tanstack: string | boolean;
|
|
9
9
|
schemasOnly: boolean;
|
|
10
10
|
output?: string;
|
|
11
|
+
includeClient?: boolean | "false" | "true";
|
|
12
|
+
successStatusCodes?: string;
|
|
13
|
+
errorStatusCodes?: string;
|
|
11
14
|
}, {}>;
|
|
12
15
|
type GenerateClientFilesOptions = typeof optionsSchema.infer & {
|
|
13
16
|
nameTransform?: NameTransformOptions;
|
package/dist/node.export.js
CHANGED
|
@@ -100,6 +100,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
100
100
|
type DefaultEndpoint = {
|
|
101
101
|
parameters?: EndpointParameters | undefined;
|
|
102
102
|
response: AnyBox;
|
|
103
|
+
responses?: Record<string, AnyBox>;
|
|
103
104
|
responseHeaders?: Record<string, AnyBox>;
|
|
104
105
|
};
|
|
105
106
|
type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
@@ -114,6 +115,7 @@ type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
114
115
|
areParametersRequired: boolean;
|
|
115
116
|
};
|
|
116
117
|
response: TConfig["response"];
|
|
118
|
+
responses?: TConfig["responses"];
|
|
117
119
|
responseHeaders?: TConfig["responseHeaders"];
|
|
118
120
|
};
|
|
119
121
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@changesets/cli": "^2.29.4",
|
|
33
|
+
"@tanstack/react-query": "5.85.0",
|
|
33
34
|
"@types/node": "^22.15.17",
|
|
34
35
|
"@types/prettier": "3.0.0",
|
|
36
|
+
"msw": "2.10.5",
|
|
35
37
|
"tsup": "^8.4.0",
|
|
36
38
|
"typescript": "^5.8.3",
|
|
37
39
|
"vitest": "^3.1.3"
|
|
@@ -64,6 +66,9 @@
|
|
|
64
66
|
"dev": "tsup --watch",
|
|
65
67
|
"build": "tsup",
|
|
66
68
|
"test": "vitest",
|
|
69
|
+
"generate:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts",
|
|
70
|
+
"test:runtime:run": "vitest run tests/integration-runtime-msw.test.ts",
|
|
71
|
+
"test:runtime": "pnpm run generate:runtime && pnpm run test:runtime:run",
|
|
67
72
|
"fmt": "prettier --write src",
|
|
68
73
|
"typecheck": "tsc -b ./tsconfig.build.json"
|
|
69
74
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { cac } from "cac";
|
|
2
|
-
|
|
3
2
|
import { readFileSync } from "fs";
|
|
4
3
|
import { generateClientFiles } from "./generate-client-files.ts";
|
|
5
4
|
import { allowedRuntimes } from "./generator.ts";
|
|
@@ -11,16 +10,22 @@ cli
|
|
|
11
10
|
.command("<input>", "Generate")
|
|
12
11
|
.option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)")
|
|
13
12
|
.option(
|
|
14
|
-
"-r, --runtime <
|
|
13
|
+
"-r, --runtime <n>",
|
|
15
14
|
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
16
15
|
{ default: "none" },
|
|
17
16
|
)
|
|
18
17
|
.option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false })
|
|
18
|
+
.option("--include-client", "Include API client types and implementation (defaults to true)", { default: true })
|
|
19
|
+
.option(
|
|
20
|
+
"--success-status-codes <codes>",
|
|
21
|
+
"Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)",
|
|
22
|
+
)
|
|
23
|
+
.option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)")
|
|
19
24
|
.option(
|
|
20
25
|
"--tanstack [name]",
|
|
21
26
|
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file",
|
|
22
27
|
)
|
|
23
|
-
.action(async (input, _options) => {
|
|
28
|
+
.action(async (input: string, _options: any) => {
|
|
24
29
|
return generateClientFiles(input, _options);
|
|
25
30
|
});
|
|
26
31
|
|
|
@@ -3,7 +3,13 @@ import type { OpenAPIObject } from "openapi3-ts/oas31";
|
|
|
3
3
|
import { basename, join, dirname } from "pathe";
|
|
4
4
|
import { type } from "arktype";
|
|
5
5
|
import { mkdir, writeFile } from "fs/promises";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
allowedRuntimes,
|
|
8
|
+
generateFile,
|
|
9
|
+
DEFAULT_SUCCESS_STATUS_CODES,
|
|
10
|
+
DEFAULT_ERROR_STATUS_CODES,
|
|
11
|
+
type GeneratorOptions,
|
|
12
|
+
} from "./generator.ts";
|
|
7
13
|
import { mapOpenApiEndpoints } from "./map-openapi-endpoints.ts";
|
|
8
14
|
import { generateTanstackQueryFile } from "./tanstack-query.generator.ts";
|
|
9
15
|
import { prettify } from "./format.ts";
|
|
@@ -25,6 +31,9 @@ export const optionsSchema = type({
|
|
|
25
31
|
runtime: allowedRuntimes,
|
|
26
32
|
tanstack: "boolean | string",
|
|
27
33
|
schemasOnly: "boolean",
|
|
34
|
+
"includeClient?": "boolean | 'true' | 'false'",
|
|
35
|
+
"successStatusCodes?": "string",
|
|
36
|
+
"errorStatusCodes?": "string",
|
|
28
37
|
});
|
|
29
38
|
|
|
30
39
|
type GenerateClientFilesOptions = typeof optionsSchema.infer & {
|
|
@@ -37,14 +46,31 @@ export async function generateClientFiles(input: string, options: GenerateClient
|
|
|
37
46
|
const ctx = mapOpenApiEndpoints(openApiDoc, options);
|
|
38
47
|
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
// Parse success status codes if provided
|
|
50
|
+
const successStatusCodes = options.successStatusCodes
|
|
51
|
+
? (options.successStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
// Parse error status codes if provided
|
|
55
|
+
const errorStatusCodes = options.errorStatusCodes
|
|
56
|
+
? (options.errorStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) as readonly number[])
|
|
57
|
+
: undefined;
|
|
58
|
+
|
|
59
|
+
// Convert string boolean to actual boolean
|
|
60
|
+
const includeClient =
|
|
61
|
+
options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
|
|
62
|
+
|
|
63
|
+
const generatorOptions: GeneratorOptions = {
|
|
64
|
+
...ctx,
|
|
65
|
+
runtime: options.runtime,
|
|
66
|
+
schemasOnly: options.schemasOnly,
|
|
67
|
+
nameTransform: options.nameTransform,
|
|
68
|
+
includeClient: includeClient ?? true,
|
|
69
|
+
successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
|
|
70
|
+
errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const content = await prettify(generateFile(generatorOptions));
|
|
48
74
|
const outputPath = join(
|
|
49
75
|
cwd,
|
|
50
76
|
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`,
|
|
@@ -56,7 +82,7 @@ export async function generateClientFiles(input: string, options: GenerateClient
|
|
|
56
82
|
|
|
57
83
|
if (options.tanstack) {
|
|
58
84
|
const tanstackContent = await generateTanstackQueryFile({
|
|
59
|
-
...
|
|
85
|
+
...generatorOptions,
|
|
60
86
|
relativeApiClientPath: "./" + basename(outputPath),
|
|
61
87
|
});
|
|
62
88
|
const tanstackOutputPath = join(
|
package/src/generator.ts
CHANGED
|
@@ -8,10 +8,26 @@ import { type } from "arktype";
|
|
|
8
8
|
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
|
|
9
9
|
import type { NameTransformOptions } from "./types.ts";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Default success status codes (2xx and 3xx ranges)
|
|
12
|
+
export const DEFAULT_SUCCESS_STATUS_CODES = [
|
|
13
|
+
200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308,
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
// Default error status codes (4xx and 5xx ranges)
|
|
17
|
+
export const DEFAULT_ERROR_STATUS_CODES = [
|
|
18
|
+
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424,
|
|
19
|
+
425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type ErrorStatusCode = (typeof DEFAULT_ERROR_STATUS_CODES)[number];
|
|
23
|
+
|
|
24
|
+
export type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
|
|
12
25
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
13
26
|
schemasOnly?: boolean;
|
|
14
27
|
nameTransform?: NameTransformOptions | undefined;
|
|
28
|
+
successStatusCodes?: readonly number[];
|
|
29
|
+
errorStatusCodes?: readonly number[];
|
|
30
|
+
includeClient?: boolean;
|
|
15
31
|
};
|
|
16
32
|
type GeneratorContext = Required<GeneratorOptions>;
|
|
17
33
|
|
|
@@ -62,11 +78,18 @@ const replacerByRuntime = {
|
|
|
62
78
|
};
|
|
63
79
|
|
|
64
80
|
export const generateFile = (options: GeneratorOptions) => {
|
|
65
|
-
const ctx = {
|
|
81
|
+
const ctx = {
|
|
82
|
+
...options,
|
|
83
|
+
runtime: options.runtime ?? "none",
|
|
84
|
+
successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
|
|
85
|
+
errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
|
|
86
|
+
includeClient: options.includeClient ?? true,
|
|
87
|
+
} as GeneratorContext;
|
|
66
88
|
|
|
67
89
|
const schemaList = generateSchemaList(ctx);
|
|
68
90
|
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
|
|
69
|
-
const
|
|
91
|
+
const endpointByMethod = options.schemasOnly ? "" : generateEndpointByMethod(ctx);
|
|
92
|
+
const apiClient = options.schemasOnly || !ctx.includeClient ? "" : generateApiClient(ctx);
|
|
70
93
|
|
|
71
94
|
const transform =
|
|
72
95
|
ctx.runtime === "none"
|
|
@@ -98,6 +121,7 @@ export const generateFile = (options: GeneratorOptions) => {
|
|
|
98
121
|
|
|
99
122
|
const file = `
|
|
100
123
|
${transform(schemaList + endpointSchemaList)}
|
|
124
|
+
${endpointByMethod}
|
|
101
125
|
${apiClient}
|
|
102
126
|
`;
|
|
103
127
|
|
|
@@ -153,6 +177,24 @@ const responseHeadersObjectToString = (responseHeaders: Record<string, AnyBox>,
|
|
|
153
177
|
return str + "}";
|
|
154
178
|
};
|
|
155
179
|
|
|
180
|
+
const generateResponsesObject = (responses: Record<string, AnyBox>, ctx: GeneratorContext) => {
|
|
181
|
+
let str = "{";
|
|
182
|
+
for (const [statusCode, responseType] of Object.entries(responses)) {
|
|
183
|
+
const value =
|
|
184
|
+
ctx.runtime === "none"
|
|
185
|
+
? responseType.recompute((box) => {
|
|
186
|
+
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
|
|
187
|
+
box.value = `Schemas.${box.value}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return box;
|
|
191
|
+
}).value
|
|
192
|
+
: responseType.value;
|
|
193
|
+
str += `${wrapWithQuotesIfNeeded(statusCode)}: ${value},\n`;
|
|
194
|
+
}
|
|
195
|
+
return str + "}";
|
|
196
|
+
};
|
|
197
|
+
|
|
156
198
|
const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
157
199
|
let file = `
|
|
158
200
|
${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
|
|
@@ -199,6 +241,7 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
199
241
|
}).value
|
|
200
242
|
: endpoint.response.value
|
|
201
243
|
},
|
|
244
|
+
${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
|
|
202
245
|
${
|
|
203
246
|
endpoint.responseHeaders
|
|
204
247
|
? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},`
|
|
@@ -227,9 +270,11 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
|
|
|
227
270
|
${Object.entries(byMethods)
|
|
228
271
|
.map(([method, list]) => {
|
|
229
272
|
return `${method}: {
|
|
230
|
-
${list
|
|
231
|
-
(
|
|
232
|
-
|
|
273
|
+
${list
|
|
274
|
+
.map(
|
|
275
|
+
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
|
|
276
|
+
)
|
|
277
|
+
.join(",\n")}
|
|
233
278
|
}`;
|
|
234
279
|
})
|
|
235
280
|
.join(",\n")}
|
|
@@ -251,9 +296,12 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
|
|
|
251
296
|
};
|
|
252
297
|
|
|
253
298
|
const generateApiClient = (ctx: GeneratorContext) => {
|
|
299
|
+
if (!ctx.includeClient) {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
|
|
254
303
|
const { endpointList } = ctx;
|
|
255
304
|
const byMethods = groupBy(endpointList, "method");
|
|
256
|
-
const endpointSchemaList = generateEndpointByMethod(ctx);
|
|
257
305
|
|
|
258
306
|
const apiClientTypes = `
|
|
259
307
|
// <ApiClientTypes>
|
|
@@ -272,6 +320,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
272
320
|
export type DefaultEndpoint = {
|
|
273
321
|
parameters?: EndpointParameters | undefined;
|
|
274
322
|
response: unknown;
|
|
323
|
+
responses?: Record<string, unknown>;
|
|
275
324
|
responseHeaders?: Record<string, unknown>;
|
|
276
325
|
};
|
|
277
326
|
|
|
@@ -287,11 +336,64 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
287
336
|
areParametersRequired: boolean;
|
|
288
337
|
};
|
|
289
338
|
response: TConfig["response"];
|
|
339
|
+
responses?: TConfig["responses"];
|
|
290
340
|
responseHeaders?: TConfig["responseHeaders"]
|
|
291
341
|
};
|
|
292
342
|
|
|
293
343
|
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
|
|
294
344
|
|
|
345
|
+
export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
|
|
346
|
+
export type SuccessStatusCode = typeof successStatusCodes[number];
|
|
347
|
+
|
|
348
|
+
export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
|
|
349
|
+
export type ErrorStatusCode = typeof errorStatusCodes[number];
|
|
350
|
+
|
|
351
|
+
// Error handling types
|
|
352
|
+
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
|
|
353
|
+
interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
|
|
354
|
+
ok: true;
|
|
355
|
+
status: TStatusCode;
|
|
356
|
+
data: TSuccess;
|
|
357
|
+
/** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
|
|
358
|
+
json: () => Promise<TSuccess>;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
|
|
362
|
+
interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
|
|
363
|
+
ok: false;
|
|
364
|
+
status: TStatusCode;
|
|
365
|
+
data: TData;
|
|
366
|
+
/** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
|
|
367
|
+
json: () => Promise<TData>;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
|
|
371
|
+
(keyof TAllResponses extends never
|
|
372
|
+
? SuccessResponse<TSuccess, number>
|
|
373
|
+
: {
|
|
374
|
+
[K in keyof TAllResponses]: K extends string
|
|
375
|
+
? K extends \`\${infer TStatusCode extends number}\`
|
|
376
|
+
? TStatusCode extends SuccessStatusCode
|
|
377
|
+
? SuccessResponse<TSuccess, TStatusCode>
|
|
378
|
+
: ErrorResponse<TAllResponses[K], TStatusCode>
|
|
379
|
+
: never
|
|
380
|
+
: K extends number
|
|
381
|
+
? K extends SuccessStatusCode
|
|
382
|
+
? SuccessResponse<TSuccess, K>
|
|
383
|
+
: ErrorResponse<TAllResponses[K], K>
|
|
384
|
+
: never;
|
|
385
|
+
}[keyof TAllResponses]);
|
|
386
|
+
|
|
387
|
+
export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
|
|
388
|
+
? TResponses extends Record<string, unknown>
|
|
389
|
+
? TypedApiResponse<TSuccess, TResponses>
|
|
390
|
+
: SuccessResponse<TSuccess, number>
|
|
391
|
+
: TEndpoint extends { response: infer TSuccess }
|
|
392
|
+
? SuccessResponse<TSuccess, number>
|
|
393
|
+
: never;
|
|
394
|
+
|
|
395
|
+
export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
|
|
396
|
+
|
|
295
397
|
type RequiredKeys<T> = {
|
|
296
398
|
[P in keyof T]-?: undefined extends T[P] ? never : P;
|
|
297
399
|
}[keyof T];
|
|
@@ -302,9 +404,23 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
|
|
|
302
404
|
`;
|
|
303
405
|
|
|
304
406
|
const apiClient = `
|
|
407
|
+
// <TypedResponseError>
|
|
408
|
+
export class TypedResponseError extends Error {
|
|
409
|
+
response: ErrorResponse<unknown, ErrorStatusCode>;
|
|
410
|
+
status: number;
|
|
411
|
+
constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
|
|
412
|
+
super(\`HTTP \${response.status}: \${response.statusText}\`);
|
|
413
|
+
this.name = 'TypedResponseError';
|
|
414
|
+
this.response = response;
|
|
415
|
+
this.status = response.status;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// </TypedResponseError>
|
|
305
419
|
// <ApiClient>
|
|
306
420
|
export class ApiClient {
|
|
307
421
|
baseUrl: string = "";
|
|
422
|
+
successStatusCodes = successStatusCodes;
|
|
423
|
+
errorStatusCodes = errorStatusCodes;
|
|
308
424
|
|
|
309
425
|
constructor(public fetcher: Fetcher) {}
|
|
310
426
|
|
|
@@ -333,16 +449,53 @@ export class ApiClient {
|
|
|
333
449
|
...params: MaybeOptionalArg<${match(ctx.runtime)
|
|
334
450
|
.with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
|
|
335
451
|
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
|
|
336
|
-
.otherwise(() => `TEndpoint["parameters"]`)}>
|
|
452
|
+
.otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
|
|
337
453
|
): Promise<${match(ctx.runtime)
|
|
338
454
|
.with("zod", "yup", () => infer(`TEndpoint["response"]`))
|
|
339
455
|
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
|
|
340
|
-
.otherwise(() => `TEndpoint["response"]`)}
|
|
341
|
-
|
|
342
|
-
|
|
456
|
+
.otherwise(() => `TEndpoint["response"]`)}>;
|
|
457
|
+
|
|
458
|
+
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
459
|
+
path: Path,
|
|
460
|
+
...params: MaybeOptionalArg<${match(ctx.runtime)
|
|
461
|
+
.with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
|
|
462
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
|
|
463
|
+
.otherwise(() => `TEndpoint["parameters"]`)} & { withResponse: true; throwOnStatusError?: boolean }>
|
|
464
|
+
): Promise<SafeApiResponse<TEndpoint>>;
|
|
465
|
+
|
|
466
|
+
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
467
|
+
path: Path,
|
|
468
|
+
...params: MaybeOptionalArg<any>
|
|
469
|
+
): Promise<any> {
|
|
470
|
+
const requestParams = params[0];
|
|
471
|
+
const withResponse = requestParams?.withResponse;
|
|
472
|
+
const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
|
|
473
|
+
|
|
474
|
+
const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
|
|
475
|
+
.then(async (response) => {
|
|
476
|
+
const data = await this.parseResponse(response);
|
|
477
|
+
const typedResponse = Object.assign(response, {
|
|
478
|
+
data: data,
|
|
479
|
+
json: () => Promise.resolve(data)
|
|
480
|
+
}) as SafeApiResponse<TEndpoint>;
|
|
481
|
+
|
|
482
|
+
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
483
|
+
throw new TypedResponseError(typedResponse as never);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return withResponse ? typedResponse : data;
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return promise ${match(ctx.runtime)
|
|
343
490
|
.with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
|
|
344
|
-
.with(
|
|
345
|
-
|
|
491
|
+
.with(
|
|
492
|
+
"arktype",
|
|
493
|
+
"io-ts",
|
|
494
|
+
"typebox",
|
|
495
|
+
"valibot",
|
|
496
|
+
() => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`,
|
|
497
|
+
)
|
|
498
|
+
.otherwise(() => `as Promise<TEndpoint["response"]>`)}
|
|
346
499
|
}
|
|
347
500
|
// </ApiClient.${method}>
|
|
348
501
|
`
|
|
@@ -373,11 +526,8 @@ export class ApiClient {
|
|
|
373
526
|
() => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`,
|
|
374
527
|
)
|
|
375
528
|
.otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
|
|
376
|
-
: Promise<
|
|
377
|
-
|
|
378
|
-
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
|
|
379
|
-
}> {
|
|
380
|
-
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
|
|
529
|
+
: Promise<SafeApiResponse<TEndpoint>> {
|
|
530
|
+
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
|
|
381
531
|
}
|
|
382
532
|
// </ApiClient.request>
|
|
383
533
|
}
|
|
@@ -395,10 +545,25 @@ export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
|
|
|
395
545
|
api.get("/users").then((users) => console.log(users));
|
|
396
546
|
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
|
|
397
547
|
api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
|
|
548
|
+
|
|
549
|
+
// With error handling
|
|
550
|
+
const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
|
|
551
|
+
if (result.ok) {
|
|
552
|
+
// Access data directly
|
|
553
|
+
const user = result.data;
|
|
554
|
+
console.log(user);
|
|
555
|
+
|
|
556
|
+
// Or use the json() method for compatibility
|
|
557
|
+
const userFromJson = await result.json();
|
|
558
|
+
console.log(userFromJson);
|
|
559
|
+
} else {
|
|
560
|
+
const error = result.data;
|
|
561
|
+
console.error(\`Error \${result.status}:\`, error);
|
|
562
|
+
}
|
|
398
563
|
*/
|
|
399
564
|
|
|
400
|
-
// </ApiClient
|
|
565
|
+
// </ApiClient>
|
|
401
566
|
`;
|
|
402
567
|
|
|
403
|
-
return
|
|
568
|
+
return apiClientTypes + apiClient;
|
|
404
569
|
};
|
|
@@ -130,14 +130,56 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
|
|
|
130
130
|
|
|
131
131
|
// Match the first 2xx-3xx response found, or fallback to default one otherwise
|
|
132
132
|
let responseObject: ResponseObject | undefined;
|
|
133
|
+
const allResponses: Record<string, AnyBox> = {};
|
|
134
|
+
|
|
133
135
|
Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
|
|
134
136
|
const statusCode = Number(status);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
const responseObj = refs.unwrap<ResponseObject>(responseOrRef);
|
|
138
|
+
|
|
139
|
+
// Collect all responses for error handling
|
|
140
|
+
const content = responseObj?.content;
|
|
141
|
+
if (content) {
|
|
142
|
+
const matchingMediaType = Object.keys(content).find(isResponseMediaType);
|
|
143
|
+
if (matchingMediaType && content[matchingMediaType]) {
|
|
144
|
+
allResponses[status] = openApiSchemaToTs({
|
|
145
|
+
schema: content[matchingMediaType]?.schema ?? {},
|
|
146
|
+
ctx,
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
// If no JSON content, use unknown type
|
|
150
|
+
allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// If no content defined, use unknown type
|
|
154
|
+
allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Keep the current logic for the main response (first 2xx-3xx)
|
|
158
|
+
if (statusCode >= 200 && statusCode < 300 && !responseObject) {
|
|
159
|
+
responseObject = responseObj;
|
|
137
160
|
}
|
|
138
161
|
});
|
|
162
|
+
|
|
139
163
|
if (!responseObject && operation.responses?.default) {
|
|
140
164
|
responseObject = refs.unwrap(operation.responses.default);
|
|
165
|
+
// Also add default to all responses if not already covered
|
|
166
|
+
if (!allResponses["default"]) {
|
|
167
|
+
const content = responseObject?.content;
|
|
168
|
+
if (content) {
|
|
169
|
+
const matchingMediaType = Object.keys(content).find(isResponseMediaType);
|
|
170
|
+
if (matchingMediaType && content[matchingMediaType]) {
|
|
171
|
+
allResponses["default"] = openApiSchemaToTs({
|
|
172
|
+
schema: content[matchingMediaType]?.schema ?? {},
|
|
173
|
+
ctx,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Set the responses collection
|
|
181
|
+
if (Object.keys(allResponses).length > 0) {
|
|
182
|
+
endpoint.responses = allResponses;
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
const content = responseObject?.content;
|
|
@@ -206,6 +248,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
206
248
|
type DefaultEndpoint = {
|
|
207
249
|
parameters?: EndpointParameters | undefined;
|
|
208
250
|
response: AnyBox;
|
|
251
|
+
responses?: Record<string, AnyBox>;
|
|
209
252
|
responseHeaders?: Record<string, AnyBox>;
|
|
210
253
|
};
|
|
211
254
|
|
|
@@ -221,5 +264,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
221
264
|
areParametersRequired: boolean;
|
|
222
265
|
};
|
|
223
266
|
response: TConfig["response"];
|
|
267
|
+
responses?: TConfig["responses"];
|
|
224
268
|
responseHeaders?: TConfig["responseHeaders"];
|
|
225
269
|
};
|