typed-openapi 1.5.0 → 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-N6BWPZUB.js → chunk-E6A7N4ND.js} +420 -67
- package/dist/chunk-KAEXXJ7X.js +21 -0
- package/dist/{chunk-RGFFCU3R.js → chunk-MCVYB63W.js} +25 -12
- package/dist/cli.js +8 -4
- package/dist/index.d.ts +12 -230
- package/dist/index.js +2 -1
- package/dist/node.export.d.ts +11 -5
- package/dist/node.export.js +4 -6
- package/dist/pretty.export.d.ts +5 -0
- package/dist/pretty.export.js +6 -0
- package/dist/types-DsI2d-HE.d.ts +244 -0
- package/package.json +8 -2
- package/src/cli.ts +8 -3
- package/src/generate-client-files.ts +43 -11
- package/src/generator.ts +187 -20
- package/src/map-openapi-endpoints.ts +61 -8
- package/src/node.export.ts +0 -1
- package/src/pretty.export.ts +1 -0
- package/src/ref-resolver.ts +12 -2
- package/src/sanitize-name.ts +79 -0
- package/src/tanstack-query.generator.ts +69 -25
- package/src/types.ts +14 -1
package/src/generator.ts
CHANGED
|
@@ -6,10 +6,28 @@ import * as Codegen from "@sinclair/typebox-codegen";
|
|
|
6
6
|
import { match } from "ts-pattern";
|
|
7
7
|
import { type } from "arktype";
|
|
8
8
|
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
|
|
9
|
+
import type { NameTransformOptions } from "./types.ts";
|
|
9
10
|
|
|
10
|
-
|
|
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> & {
|
|
11
25
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
12
26
|
schemasOnly?: boolean;
|
|
27
|
+
nameTransform?: NameTransformOptions | undefined;
|
|
28
|
+
successStatusCodes?: readonly number[];
|
|
29
|
+
errorStatusCodes?: readonly number[];
|
|
30
|
+
includeClient?: boolean;
|
|
13
31
|
};
|
|
14
32
|
type GeneratorContext = Required<GeneratorOptions>;
|
|
15
33
|
|
|
@@ -60,11 +78,18 @@ const replacerByRuntime = {
|
|
|
60
78
|
};
|
|
61
79
|
|
|
62
80
|
export const generateFile = (options: GeneratorOptions) => {
|
|
63
|
-
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;
|
|
64
88
|
|
|
65
89
|
const schemaList = generateSchemaList(ctx);
|
|
66
90
|
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
|
|
67
|
-
const
|
|
91
|
+
const endpointByMethod = options.schemasOnly ? "" : generateEndpointByMethod(ctx);
|
|
92
|
+
const apiClient = options.schemasOnly || !ctx.includeClient ? "" : generateApiClient(ctx);
|
|
68
93
|
|
|
69
94
|
const transform =
|
|
70
95
|
ctx.runtime === "none"
|
|
@@ -96,6 +121,7 @@ export const generateFile = (options: GeneratorOptions) => {
|
|
|
96
121
|
|
|
97
122
|
const file = `
|
|
98
123
|
${transform(schemaList + endpointSchemaList)}
|
|
124
|
+
${endpointByMethod}
|
|
99
125
|
${apiClient}
|
|
100
126
|
`;
|
|
101
127
|
|
|
@@ -151,6 +177,24 @@ const responseHeadersObjectToString = (responseHeaders: Record<string, AnyBox>,
|
|
|
151
177
|
return str + "}";
|
|
152
178
|
};
|
|
153
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
|
+
|
|
154
198
|
const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
155
199
|
let file = `
|
|
156
200
|
${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
|
|
@@ -197,6 +241,7 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
197
241
|
}).value
|
|
198
242
|
: endpoint.response.value
|
|
199
243
|
},
|
|
244
|
+
${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
|
|
200
245
|
${
|
|
201
246
|
endpoint.responseHeaders
|
|
202
247
|
? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},`
|
|
@@ -225,9 +270,11 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
|
|
|
225
270
|
${Object.entries(byMethods)
|
|
226
271
|
.map(([method, list]) => {
|
|
227
272
|
return `${method}: {
|
|
228
|
-
${list
|
|
229
|
-
(
|
|
230
|
-
|
|
273
|
+
${list
|
|
274
|
+
.map(
|
|
275
|
+
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
|
|
276
|
+
)
|
|
277
|
+
.join(",\n")}
|
|
231
278
|
}`;
|
|
232
279
|
})
|
|
233
280
|
.join(",\n")}
|
|
@@ -249,9 +296,12 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
|
|
|
249
296
|
};
|
|
250
297
|
|
|
251
298
|
const generateApiClient = (ctx: GeneratorContext) => {
|
|
299
|
+
if (!ctx.includeClient) {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
|
|
252
303
|
const { endpointList } = ctx;
|
|
253
304
|
const byMethods = groupBy(endpointList, "method");
|
|
254
|
-
const endpointSchemaList = generateEndpointByMethod(ctx);
|
|
255
305
|
|
|
256
306
|
const apiClientTypes = `
|
|
257
307
|
// <ApiClientTypes>
|
|
@@ -270,6 +320,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
270
320
|
export type DefaultEndpoint = {
|
|
271
321
|
parameters?: EndpointParameters | undefined;
|
|
272
322
|
response: unknown;
|
|
323
|
+
responses?: Record<string, unknown>;
|
|
273
324
|
responseHeaders?: Record<string, unknown>;
|
|
274
325
|
};
|
|
275
326
|
|
|
@@ -285,11 +336,64 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
285
336
|
areParametersRequired: boolean;
|
|
286
337
|
};
|
|
287
338
|
response: TConfig["response"];
|
|
339
|
+
responses?: TConfig["responses"];
|
|
288
340
|
responseHeaders?: TConfig["responseHeaders"]
|
|
289
341
|
};
|
|
290
342
|
|
|
291
343
|
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
|
|
292
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
|
+
|
|
293
397
|
type RequiredKeys<T> = {
|
|
294
398
|
[P in keyof T]-?: undefined extends T[P] ? never : P;
|
|
295
399
|
}[keyof T];
|
|
@@ -300,9 +404,23 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
|
|
|
300
404
|
`;
|
|
301
405
|
|
|
302
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>
|
|
303
419
|
// <ApiClient>
|
|
304
420
|
export class ApiClient {
|
|
305
421
|
baseUrl: string = "";
|
|
422
|
+
successStatusCodes = successStatusCodes;
|
|
423
|
+
errorStatusCodes = errorStatusCodes;
|
|
306
424
|
|
|
307
425
|
constructor(public fetcher: Fetcher) {}
|
|
308
426
|
|
|
@@ -331,16 +449,53 @@ export class ApiClient {
|
|
|
331
449
|
...params: MaybeOptionalArg<${match(ctx.runtime)
|
|
332
450
|
.with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
|
|
333
451
|
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
|
|
334
|
-
.otherwise(() => `TEndpoint["parameters"]`)}>
|
|
452
|
+
.otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
|
|
335
453
|
): Promise<${match(ctx.runtime)
|
|
336
454
|
.with("zod", "yup", () => infer(`TEndpoint["response"]`))
|
|
337
455
|
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
|
|
338
|
-
.otherwise(() => `TEndpoint["response"]`)}
|
|
339
|
-
|
|
340
|
-
|
|
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)
|
|
341
490
|
.with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
|
|
342
|
-
.with(
|
|
343
|
-
|
|
491
|
+
.with(
|
|
492
|
+
"arktype",
|
|
493
|
+
"io-ts",
|
|
494
|
+
"typebox",
|
|
495
|
+
"valibot",
|
|
496
|
+
() => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`,
|
|
497
|
+
)
|
|
498
|
+
.otherwise(() => `as Promise<TEndpoint["response"]>`)}
|
|
344
499
|
}
|
|
345
500
|
// </ApiClient.${method}>
|
|
346
501
|
`
|
|
@@ -371,11 +526,8 @@ export class ApiClient {
|
|
|
371
526
|
() => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`,
|
|
372
527
|
)
|
|
373
528
|
.otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
|
|
374
|
-
: Promise<
|
|
375
|
-
|
|
376
|
-
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
|
|
377
|
-
}> {
|
|
378
|
-
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>>;
|
|
379
531
|
}
|
|
380
532
|
// </ApiClient.request>
|
|
381
533
|
}
|
|
@@ -393,10 +545,25 @@ export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
|
|
|
393
545
|
api.get("/users").then((users) => console.log(users));
|
|
394
546
|
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
|
|
395
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
|
+
}
|
|
396
563
|
*/
|
|
397
564
|
|
|
398
|
-
// </ApiClient
|
|
565
|
+
// </ApiClient>
|
|
399
566
|
`;
|
|
400
567
|
|
|
401
|
-
return
|
|
568
|
+
return apiClientTypes + apiClient;
|
|
402
569
|
};
|
|
@@ -8,11 +8,13 @@ import { createRefResolver } from "./ref-resolver.ts";
|
|
|
8
8
|
import { tsFactory } from "./ts-factory.ts";
|
|
9
9
|
import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types.ts";
|
|
10
10
|
import { pathToVariableName } from "./string-utils.ts";
|
|
11
|
+
import { NameTransformOptions } from "./types.ts";
|
|
11
12
|
import { match, P } from "ts-pattern";
|
|
13
|
+
import { sanitizeName } from "./sanitize-name.ts";
|
|
12
14
|
|
|
13
15
|
const factory = tsFactory;
|
|
14
16
|
|
|
15
|
-
export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
17
|
+
export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransform?: NameTransformOptions }) => {
|
|
16
18
|
const refs = createRefResolver(doc, factory);
|
|
17
19
|
const ctx: OpenapiSchemaConvertContext = { refs, factory };
|
|
18
20
|
const endpointList = [] as Array<Endpoint>;
|
|
@@ -22,6 +24,10 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
22
24
|
Object.entries(pathItem).forEach(([method, operation]) => {
|
|
23
25
|
if (operation.deprecated) return;
|
|
24
26
|
|
|
27
|
+
let alias = getAlias({ path, method, operation } as Endpoint);
|
|
28
|
+
if (options?.nameTransform?.transformEndpointName) {
|
|
29
|
+
alias = options.nameTransform.transformEndpointName({ alias, path, method: method as Method, operation });
|
|
30
|
+
}
|
|
25
31
|
const endpoint = {
|
|
26
32
|
operation,
|
|
27
33
|
method: method as Method,
|
|
@@ -29,7 +35,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
29
35
|
requestFormat: "json",
|
|
30
36
|
response: openApiSchemaToTs({ schema: {}, ctx }),
|
|
31
37
|
meta: {
|
|
32
|
-
alias
|
|
38
|
+
alias,
|
|
33
39
|
areParametersRequired: false,
|
|
34
40
|
hasParameters: false,
|
|
35
41
|
},
|
|
@@ -84,7 +90,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
84
90
|
|
|
85
91
|
if (matchingMediaType && content[matchingMediaType]) {
|
|
86
92
|
params.body = openApiSchemaToTs({
|
|
87
|
-
schema: content[matchingMediaType]?.schema ?? {}
|
|
93
|
+
schema: content[matchingMediaType]?.schema ?? {},
|
|
88
94
|
ctx,
|
|
89
95
|
});
|
|
90
96
|
}
|
|
@@ -124,14 +130,56 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
124
130
|
|
|
125
131
|
// Match the first 2xx-3xx response found, or fallback to default one otherwise
|
|
126
132
|
let responseObject: ResponseObject | undefined;
|
|
133
|
+
const allResponses: Record<string, AnyBox> = {};
|
|
134
|
+
|
|
127
135
|
Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
|
|
128
136
|
const statusCode = Number(status);
|
|
129
|
-
|
|
130
|
-
|
|
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;
|
|
131
160
|
}
|
|
132
161
|
});
|
|
162
|
+
|
|
133
163
|
if (!responseObject && operation.responses?.default) {
|
|
134
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;
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
const content = responseObject?.content;
|
|
@@ -139,7 +187,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
|
139
187
|
const matchingMediaType = Object.keys(content).find(isResponseMediaType);
|
|
140
188
|
if (matchingMediaType && content[matchingMediaType]) {
|
|
141
189
|
endpoint.response = openApiSchemaToTs({
|
|
142
|
-
schema: content[matchingMediaType]?.schema ?? {}
|
|
190
|
+
schema: content[matchingMediaType]?.schema ?? {},
|
|
143
191
|
ctx,
|
|
144
192
|
});
|
|
145
193
|
}
|
|
@@ -180,10 +228,13 @@ const isAllowedParamMediaTypes = (
|
|
|
180
228
|
|
|
181
229
|
const isResponseMediaType = (mediaType: string) => mediaType === "application/json";
|
|
182
230
|
const getAlias = ({ path, method, operation }: Endpoint) =>
|
|
183
|
-
(
|
|
231
|
+
sanitizeName(
|
|
232
|
+
(method + "_" + capitalize(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"),
|
|
233
|
+
"endpoint",
|
|
234
|
+
);
|
|
184
235
|
|
|
185
236
|
type MutationMethod = "post" | "put" | "patch" | "delete";
|
|
186
|
-
type Method = "get" | "head" | "options" | MutationMethod;
|
|
237
|
+
export type Method = "get" | "head" | "options" | MutationMethod;
|
|
187
238
|
|
|
188
239
|
export type EndpointParameters = {
|
|
189
240
|
body?: Box<BoxRef>;
|
|
@@ -197,6 +248,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
197
248
|
type DefaultEndpoint = {
|
|
198
249
|
parameters?: EndpointParameters | undefined;
|
|
199
250
|
response: AnyBox;
|
|
251
|
+
responses?: Record<string, AnyBox>;
|
|
200
252
|
responseHeaders?: Record<string, AnyBox>;
|
|
201
253
|
};
|
|
202
254
|
|
|
@@ -212,5 +264,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
212
264
|
areParametersRequired: boolean;
|
|
213
265
|
};
|
|
214
266
|
response: TConfig["response"];
|
|
267
|
+
responses?: TConfig["responses"];
|
|
215
268
|
responseHeaders?: TConfig["responseHeaders"];
|
|
216
269
|
};
|
package/src/node.export.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { prettify } from "./format.ts";
|
package/src/ref-resolver.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { Box } from "./box.ts";
|
|
|
5
5
|
import { isReferenceObject } from "./is-reference-object.ts";
|
|
6
6
|
import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
|
|
7
7
|
import { normalizeString } from "./string-utils.ts";
|
|
8
|
+
import { NameTransformOptions } from "./types.ts";
|
|
8
9
|
import { AnyBoxDef, GenericFactory, type LibSchemaObject } from "./types.ts";
|
|
9
10
|
import { topologicalSort } from "./topological-sort.ts";
|
|
11
|
+
import { sanitizeName } from "./sanitize-name.ts";
|
|
10
12
|
|
|
11
13
|
const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1));
|
|
12
14
|
const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
|
|
@@ -26,7 +28,11 @@ export type RefInfo = {
|
|
|
26
28
|
kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers";
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
export const createRefResolver = (
|
|
31
|
+
export const createRefResolver = (
|
|
32
|
+
doc: OpenAPIObject,
|
|
33
|
+
factory: GenericFactory,
|
|
34
|
+
nameTransform?: NameTransformOptions,
|
|
35
|
+
) => {
|
|
30
36
|
// both used for debugging purpose
|
|
31
37
|
const nameByRef = new Map<string, string>();
|
|
32
38
|
const refByName = new Map<string, string>();
|
|
@@ -48,7 +54,11 @@ export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) =
|
|
|
48
54
|
|
|
49
55
|
// "#/components/schemas/Something.jsonld" -> "Something.jsonld"
|
|
50
56
|
const name = split[split.length - 1]!;
|
|
51
|
-
|
|
57
|
+
let normalized = normalizeString(name);
|
|
58
|
+
if (nameTransform?.transformSchemaName) {
|
|
59
|
+
normalized = nameTransform.transformSchemaName(normalized);
|
|
60
|
+
}
|
|
61
|
+
normalized = sanitizeName(normalized, "schema");
|
|
52
62
|
|
|
53
63
|
nameByRef.set(correctRef, normalized);
|
|
54
64
|
refByName.set(normalized, correctRef);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const reservedWords = new Set([
|
|
2
|
+
// TS keywords and built-ins
|
|
3
|
+
"import",
|
|
4
|
+
"package",
|
|
5
|
+
"namespace",
|
|
6
|
+
"Record",
|
|
7
|
+
"Partial",
|
|
8
|
+
"Required",
|
|
9
|
+
"Readonly",
|
|
10
|
+
"Pick",
|
|
11
|
+
"Omit",
|
|
12
|
+
"String",
|
|
13
|
+
"Number",
|
|
14
|
+
"Boolean",
|
|
15
|
+
"Object",
|
|
16
|
+
"Array",
|
|
17
|
+
"Function",
|
|
18
|
+
"any",
|
|
19
|
+
"unknown",
|
|
20
|
+
"never",
|
|
21
|
+
"void",
|
|
22
|
+
"extends",
|
|
23
|
+
"super",
|
|
24
|
+
"class",
|
|
25
|
+
"interface",
|
|
26
|
+
"type",
|
|
27
|
+
"enum",
|
|
28
|
+
"const",
|
|
29
|
+
"let",
|
|
30
|
+
"var",
|
|
31
|
+
"if",
|
|
32
|
+
"else",
|
|
33
|
+
"for",
|
|
34
|
+
"while",
|
|
35
|
+
"do",
|
|
36
|
+
"switch",
|
|
37
|
+
"case",
|
|
38
|
+
"default",
|
|
39
|
+
"break",
|
|
40
|
+
"continue",
|
|
41
|
+
"return",
|
|
42
|
+
"try",
|
|
43
|
+
"catch",
|
|
44
|
+
"finally",
|
|
45
|
+
"throw",
|
|
46
|
+
"new",
|
|
47
|
+
"delete",
|
|
48
|
+
"in",
|
|
49
|
+
"instanceof",
|
|
50
|
+
"typeof",
|
|
51
|
+
"void",
|
|
52
|
+
"with",
|
|
53
|
+
"yield",
|
|
54
|
+
"await",
|
|
55
|
+
"static",
|
|
56
|
+
"public",
|
|
57
|
+
"private",
|
|
58
|
+
"protected",
|
|
59
|
+
"abstract",
|
|
60
|
+
"as",
|
|
61
|
+
"asserts",
|
|
62
|
+
"from",
|
|
63
|
+
"get",
|
|
64
|
+
"set",
|
|
65
|
+
"module",
|
|
66
|
+
"require",
|
|
67
|
+
"keyof",
|
|
68
|
+
"readonly",
|
|
69
|
+
"global",
|
|
70
|
+
"symbol",
|
|
71
|
+
"bigint",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export function sanitizeName(name: string, type: "schema" | "endpoint") {
|
|
75
|
+
let n = name.replace(/[\W/]+/g, "_");
|
|
76
|
+
if (/^\d/.test(n)) n = "_" + n;
|
|
77
|
+
if (reservedWords.has(n)) n = (type === "schema" ? "Schema_" : "Endpoint_") + n;
|
|
78
|
+
return n;
|
|
79
|
+
}
|