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
|
@@ -282,6 +282,69 @@ import { capitalize as capitalize2, groupBy } from "pastable/server";
|
|
|
282
282
|
import * as Codegen from "@sinclair/typebox-codegen";
|
|
283
283
|
import { match } from "ts-pattern";
|
|
284
284
|
import { type } from "arktype";
|
|
285
|
+
var DEFAULT_SUCCESS_STATUS_CODES = [
|
|
286
|
+
200,
|
|
287
|
+
201,
|
|
288
|
+
202,
|
|
289
|
+
203,
|
|
290
|
+
204,
|
|
291
|
+
205,
|
|
292
|
+
206,
|
|
293
|
+
207,
|
|
294
|
+
208,
|
|
295
|
+
226,
|
|
296
|
+
300,
|
|
297
|
+
301,
|
|
298
|
+
302,
|
|
299
|
+
303,
|
|
300
|
+
304,
|
|
301
|
+
305,
|
|
302
|
+
306,
|
|
303
|
+
307,
|
|
304
|
+
308
|
|
305
|
+
];
|
|
306
|
+
var DEFAULT_ERROR_STATUS_CODES = [
|
|
307
|
+
400,
|
|
308
|
+
401,
|
|
309
|
+
402,
|
|
310
|
+
403,
|
|
311
|
+
404,
|
|
312
|
+
405,
|
|
313
|
+
406,
|
|
314
|
+
407,
|
|
315
|
+
408,
|
|
316
|
+
409,
|
|
317
|
+
410,
|
|
318
|
+
411,
|
|
319
|
+
412,
|
|
320
|
+
413,
|
|
321
|
+
414,
|
|
322
|
+
415,
|
|
323
|
+
416,
|
|
324
|
+
417,
|
|
325
|
+
418,
|
|
326
|
+
421,
|
|
327
|
+
422,
|
|
328
|
+
423,
|
|
329
|
+
424,
|
|
330
|
+
425,
|
|
331
|
+
426,
|
|
332
|
+
428,
|
|
333
|
+
429,
|
|
334
|
+
431,
|
|
335
|
+
451,
|
|
336
|
+
500,
|
|
337
|
+
501,
|
|
338
|
+
502,
|
|
339
|
+
503,
|
|
340
|
+
504,
|
|
341
|
+
505,
|
|
342
|
+
506,
|
|
343
|
+
507,
|
|
344
|
+
508,
|
|
345
|
+
510,
|
|
346
|
+
511
|
|
347
|
+
];
|
|
285
348
|
var allowedRuntimes = type("'none' | 'arktype' | 'io-ts' | 'typebox' | 'valibot' | 'yup' | 'zod'");
|
|
286
349
|
var runtimeValidationGenerator = {
|
|
287
350
|
arktype: Codegen.ModelToArkType.Generate,
|
|
@@ -314,10 +377,17 @@ var replacerByRuntime = {
|
|
|
314
377
|
)
|
|
315
378
|
};
|
|
316
379
|
var generateFile = (options) => {
|
|
317
|
-
const ctx = {
|
|
380
|
+
const ctx = {
|
|
381
|
+
...options,
|
|
382
|
+
runtime: options.runtime ?? "none",
|
|
383
|
+
successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
|
|
384
|
+
errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
|
|
385
|
+
includeClient: options.includeClient ?? true
|
|
386
|
+
};
|
|
318
387
|
const schemaList = generateSchemaList(ctx);
|
|
319
388
|
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
|
|
320
|
-
const
|
|
389
|
+
const endpointByMethod = options.schemasOnly ? "" : generateEndpointByMethod(ctx);
|
|
390
|
+
const apiClient = options.schemasOnly || !ctx.includeClient ? "" : generateApiClient(ctx);
|
|
321
391
|
const transform = ctx.runtime === "none" ? (file2) => file2 : (file2) => {
|
|
322
392
|
const model = Codegen.TypeScriptToModel.Generate(file2);
|
|
323
393
|
const transformer = runtimeValidationGenerator[ctx.runtime];
|
|
@@ -337,6 +407,7 @@ var generateFile = (options) => {
|
|
|
337
407
|
};
|
|
338
408
|
const file = `
|
|
339
409
|
${transform(schemaList + endpointSchemaList)}
|
|
410
|
+
${endpointByMethod}
|
|
340
411
|
${apiClient}
|
|
341
412
|
`;
|
|
342
413
|
return file;
|
|
@@ -380,6 +451,20 @@ var responseHeadersObjectToString = (responseHeaders, ctx) => {
|
|
|
380
451
|
}
|
|
381
452
|
return str + "}";
|
|
382
453
|
};
|
|
454
|
+
var generateResponsesObject = (responses, ctx) => {
|
|
455
|
+
let str = "{";
|
|
456
|
+
for (const [statusCode, responseType] of Object.entries(responses)) {
|
|
457
|
+
const value = ctx.runtime === "none" ? responseType.recompute((box) => {
|
|
458
|
+
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
|
|
459
|
+
box.value = `Schemas.${box.value}`;
|
|
460
|
+
}
|
|
461
|
+
return box;
|
|
462
|
+
}).value : responseType.value;
|
|
463
|
+
str += `${wrapWithQuotesIfNeeded(statusCode)}: ${value},
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
return str + "}";
|
|
467
|
+
};
|
|
383
468
|
var generateEndpointSchemaList = (ctx) => {
|
|
384
469
|
let file = `
|
|
385
470
|
${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
|
|
@@ -411,6 +496,7 @@ var generateEndpointSchemaList = (ctx) => {
|
|
|
411
496
|
}
|
|
412
497
|
return box;
|
|
413
498
|
}).value : endpoint.response.value},
|
|
499
|
+
${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
|
|
414
500
|
${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},` : ""}
|
|
415
501
|
}
|
|
416
502
|
`;
|
|
@@ -431,7 +517,7 @@ var generateEndpointByMethod = (ctx) => {
|
|
|
431
517
|
return `${method}: {
|
|
432
518
|
${list.map(
|
|
433
519
|
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`
|
|
434
|
-
)}
|
|
520
|
+
).join(",\n")}
|
|
435
521
|
}`;
|
|
436
522
|
}).join(",\n")}
|
|
437
523
|
}
|
|
@@ -447,9 +533,11 @@ var generateEndpointByMethod = (ctx) => {
|
|
|
447
533
|
return endpointByMethod + shorthands;
|
|
448
534
|
};
|
|
449
535
|
var generateApiClient = (ctx) => {
|
|
536
|
+
if (!ctx.includeClient) {
|
|
537
|
+
return "";
|
|
538
|
+
}
|
|
450
539
|
const { endpointList } = ctx;
|
|
451
540
|
const byMethods = groupBy(endpointList, "method");
|
|
452
|
-
const endpointSchemaList = generateEndpointByMethod(ctx);
|
|
453
541
|
const apiClientTypes = `
|
|
454
542
|
// <ApiClientTypes>
|
|
455
543
|
export type EndpointParameters = {
|
|
@@ -467,6 +555,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
|
|
|
467
555
|
export type DefaultEndpoint = {
|
|
468
556
|
parameters?: EndpointParameters | undefined;
|
|
469
557
|
response: unknown;
|
|
558
|
+
responses?: Record<string, unknown>;
|
|
470
559
|
responseHeaders?: Record<string, unknown>;
|
|
471
560
|
};
|
|
472
561
|
|
|
@@ -482,11 +571,64 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
482
571
|
areParametersRequired: boolean;
|
|
483
572
|
};
|
|
484
573
|
response: TConfig["response"];
|
|
574
|
+
responses?: TConfig["responses"];
|
|
485
575
|
responseHeaders?: TConfig["responseHeaders"]
|
|
486
576
|
};
|
|
487
577
|
|
|
488
578
|
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
|
|
489
579
|
|
|
580
|
+
export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
|
|
581
|
+
export type SuccessStatusCode = typeof successStatusCodes[number];
|
|
582
|
+
|
|
583
|
+
export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
|
|
584
|
+
export type ErrorStatusCode = typeof errorStatusCodes[number];
|
|
585
|
+
|
|
586
|
+
// Error handling types
|
|
587
|
+
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
|
|
588
|
+
interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
|
|
589
|
+
ok: true;
|
|
590
|
+
status: TStatusCode;
|
|
591
|
+
data: TSuccess;
|
|
592
|
+
/** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
|
|
593
|
+
json: () => Promise<TSuccess>;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
|
|
597
|
+
interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
|
|
598
|
+
ok: false;
|
|
599
|
+
status: TStatusCode;
|
|
600
|
+
data: TData;
|
|
601
|
+
/** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
|
|
602
|
+
json: () => Promise<TData>;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
|
|
606
|
+
(keyof TAllResponses extends never
|
|
607
|
+
? SuccessResponse<TSuccess, number>
|
|
608
|
+
: {
|
|
609
|
+
[K in keyof TAllResponses]: K extends string
|
|
610
|
+
? K extends \`\${infer TStatusCode extends number}\`
|
|
611
|
+
? TStatusCode extends SuccessStatusCode
|
|
612
|
+
? SuccessResponse<TSuccess, TStatusCode>
|
|
613
|
+
: ErrorResponse<TAllResponses[K], TStatusCode>
|
|
614
|
+
: never
|
|
615
|
+
: K extends number
|
|
616
|
+
? K extends SuccessStatusCode
|
|
617
|
+
? SuccessResponse<TSuccess, K>
|
|
618
|
+
: ErrorResponse<TAllResponses[K], K>
|
|
619
|
+
: never;
|
|
620
|
+
}[keyof TAllResponses]);
|
|
621
|
+
|
|
622
|
+
export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
|
|
623
|
+
? TResponses extends Record<string, unknown>
|
|
624
|
+
? TypedApiResponse<TSuccess, TResponses>
|
|
625
|
+
: SuccessResponse<TSuccess, number>
|
|
626
|
+
: TEndpoint extends { response: infer TSuccess }
|
|
627
|
+
? SuccessResponse<TSuccess, number>
|
|
628
|
+
: never;
|
|
629
|
+
|
|
630
|
+
export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
|
|
631
|
+
|
|
490
632
|
type RequiredKeys<T> = {
|
|
491
633
|
[P in keyof T]-?: undefined extends T[P] ? never : P;
|
|
492
634
|
}[keyof T];
|
|
@@ -496,9 +638,23 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
|
|
|
496
638
|
// </ApiClientTypes>
|
|
497
639
|
`;
|
|
498
640
|
const apiClient = `
|
|
641
|
+
// <TypedResponseError>
|
|
642
|
+
export class TypedResponseError extends Error {
|
|
643
|
+
response: ErrorResponse<unknown, ErrorStatusCode>;
|
|
644
|
+
status: number;
|
|
645
|
+
constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
|
|
646
|
+
super(\`HTTP \${response.status}: \${response.statusText}\`);
|
|
647
|
+
this.name = 'TypedResponseError';
|
|
648
|
+
this.response = response;
|
|
649
|
+
this.status = response.status;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// </TypedResponseError>
|
|
499
653
|
// <ApiClient>
|
|
500
654
|
export class ApiClient {
|
|
501
655
|
baseUrl: string = "";
|
|
656
|
+
successStatusCodes = successStatusCodes;
|
|
657
|
+
errorStatusCodes = errorStatusCodes;
|
|
502
658
|
|
|
503
659
|
constructor(public fetcher: Fetcher) {}
|
|
504
660
|
|
|
@@ -521,10 +677,44 @@ export class ApiClient {
|
|
|
521
677
|
return endpointByMethod.length ? `// <ApiClient.${method}>
|
|
522
678
|
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
523
679
|
path: Path,
|
|
524
|
-
...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)}>
|
|
525
|
-
): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["response"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`).otherwise(() => `TEndpoint["response"]`)}
|
|
526
|
-
|
|
527
|
-
|
|
680
|
+
...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
|
|
681
|
+
): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["response"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`).otherwise(() => `TEndpoint["response"]`)}>;
|
|
682
|
+
|
|
683
|
+
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
684
|
+
path: Path,
|
|
685
|
+
...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)} & { withResponse: true; throwOnStatusError?: boolean }>
|
|
686
|
+
): Promise<SafeApiResponse<TEndpoint>>;
|
|
687
|
+
|
|
688
|
+
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
689
|
+
path: Path,
|
|
690
|
+
...params: MaybeOptionalArg<any>
|
|
691
|
+
): Promise<any> {
|
|
692
|
+
const requestParams = params[0];
|
|
693
|
+
const withResponse = requestParams?.withResponse;
|
|
694
|
+
const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
|
|
695
|
+
|
|
696
|
+
const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
|
|
697
|
+
.then(async (response) => {
|
|
698
|
+
const data = await this.parseResponse(response);
|
|
699
|
+
const typedResponse = Object.assign(response, {
|
|
700
|
+
data: data,
|
|
701
|
+
json: () => Promise.resolve(data)
|
|
702
|
+
}) as SafeApiResponse<TEndpoint>;
|
|
703
|
+
|
|
704
|
+
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
705
|
+
throw new TypedResponseError(typedResponse as never);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return withResponse ? typedResponse : data;
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`).with(
|
|
712
|
+
"arktype",
|
|
713
|
+
"io-ts",
|
|
714
|
+
"typebox",
|
|
715
|
+
"valibot",
|
|
716
|
+
() => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`
|
|
717
|
+
).otherwise(() => `as Promise<TEndpoint["response"]>`)}
|
|
528
718
|
}
|
|
529
719
|
// </ApiClient.${method}>
|
|
530
720
|
` : "";
|
|
@@ -552,11 +742,8 @@ export class ApiClient {
|
|
|
552
742
|
"valibot",
|
|
553
743
|
() => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`
|
|
554
744
|
).otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
|
|
555
|
-
: Promise<
|
|
556
|
-
|
|
557
|
-
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
|
|
558
|
-
}> {
|
|
559
|
-
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
|
|
745
|
+
: Promise<SafeApiResponse<TEndpoint>> {
|
|
746
|
+
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
|
|
560
747
|
}
|
|
561
748
|
// </ApiClient.request>
|
|
562
749
|
}
|
|
@@ -574,11 +761,26 @@ export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
|
|
|
574
761
|
api.get("/users").then((users) => console.log(users));
|
|
575
762
|
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
|
|
576
763
|
api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
|
|
764
|
+
|
|
765
|
+
// With error handling
|
|
766
|
+
const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
|
|
767
|
+
if (result.ok) {
|
|
768
|
+
// Access data directly
|
|
769
|
+
const user = result.data;
|
|
770
|
+
console.log(user);
|
|
771
|
+
|
|
772
|
+
// Or use the json() method for compatibility
|
|
773
|
+
const userFromJson = await result.json();
|
|
774
|
+
console.log(userFromJson);
|
|
775
|
+
} else {
|
|
776
|
+
const error = result.data;
|
|
777
|
+
console.error(\`Error \${result.status}:\`, error);
|
|
778
|
+
}
|
|
577
779
|
*/
|
|
578
780
|
|
|
579
|
-
// </ApiClient
|
|
781
|
+
// </ApiClient>
|
|
580
782
|
`;
|
|
581
|
-
return
|
|
783
|
+
return apiClientTypes + apiClient;
|
|
582
784
|
};
|
|
583
785
|
|
|
584
786
|
// src/ref-resolver.ts
|
|
@@ -944,14 +1146,45 @@ var mapOpenApiEndpoints = (doc, options) => {
|
|
|
944
1146
|
endpoint.parameters = Object.keys(params).length ? params : void 0;
|
|
945
1147
|
}
|
|
946
1148
|
let responseObject;
|
|
1149
|
+
const allResponses = {};
|
|
947
1150
|
Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
|
|
948
1151
|
const statusCode = Number(status);
|
|
949
|
-
|
|
950
|
-
|
|
1152
|
+
const responseObj = refs.unwrap(responseOrRef);
|
|
1153
|
+
const content2 = responseObj?.content;
|
|
1154
|
+
if (content2) {
|
|
1155
|
+
const matchingMediaType = Object.keys(content2).find(isResponseMediaType);
|
|
1156
|
+
if (matchingMediaType && content2[matchingMediaType]) {
|
|
1157
|
+
allResponses[status] = openApiSchemaToTs({
|
|
1158
|
+
schema: content2[matchingMediaType]?.schema ?? {},
|
|
1159
|
+
ctx
|
|
1160
|
+
});
|
|
1161
|
+
} else {
|
|
1162
|
+
allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
|
|
1163
|
+
}
|
|
1164
|
+
} else {
|
|
1165
|
+
allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
|
|
1166
|
+
}
|
|
1167
|
+
if (statusCode >= 200 && statusCode < 300 && !responseObject) {
|
|
1168
|
+
responseObject = responseObj;
|
|
951
1169
|
}
|
|
952
1170
|
});
|
|
953
1171
|
if (!responseObject && operation.responses?.default) {
|
|
954
1172
|
responseObject = refs.unwrap(operation.responses.default);
|
|
1173
|
+
if (!allResponses["default"]) {
|
|
1174
|
+
const content2 = responseObject?.content;
|
|
1175
|
+
if (content2) {
|
|
1176
|
+
const matchingMediaType = Object.keys(content2).find(isResponseMediaType);
|
|
1177
|
+
if (matchingMediaType && content2[matchingMediaType]) {
|
|
1178
|
+
allResponses["default"] = openApiSchemaToTs({
|
|
1179
|
+
schema: content2[matchingMediaType]?.schema ?? {},
|
|
1180
|
+
ctx
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (Object.keys(allResponses).length > 0) {
|
|
1187
|
+
endpoint.responses = allResponses;
|
|
955
1188
|
}
|
|
956
1189
|
const content = responseObject?.content;
|
|
957
1190
|
if (content) {
|
|
@@ -998,7 +1231,8 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
998
1231
|
const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
|
|
999
1232
|
const file = `
|
|
1000
1233
|
import { queryOptions } from "@tanstack/react-query"
|
|
1001
|
-
import type { EndpointByMethod, ApiClient } from "${ctx.relativeApiClientPath}"
|
|
1234
|
+
import type { EndpointByMethod, ApiClient, SuccessStatusCode, ErrorStatusCode, InferResponseByStatus } from "${ctx.relativeApiClientPath}"
|
|
1235
|
+
import { errorStatusCodes, TypedResponseError } from "${ctx.relativeApiClientPath}"
|
|
1002
1236
|
|
|
1003
1237
|
type EndpointQueryKey<TOptions extends EndpointParameters> = [
|
|
1004
1238
|
TOptions & {
|
|
@@ -1062,30 +1296,36 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
1062
1296
|
path: Path,
|
|
1063
1297
|
...params: MaybeOptionalArg<TEndpoint["parameters"]>
|
|
1064
1298
|
) {
|
|
1065
|
-
const queryKey = createQueryKey(path, params[0]);
|
|
1299
|
+
const queryKey = createQueryKey(path as string, params[0]);
|
|
1066
1300
|
const query = {
|
|
1067
1301
|
/** type-only property if you need easy access to the endpoint params */
|
|
1068
1302
|
"~endpoint": {} as TEndpoint,
|
|
1069
1303
|
queryKey,
|
|
1304
|
+
queryFn: {} as "You need to pass .queryOptions to the useQuery hook",
|
|
1070
1305
|
queryOptions: queryOptions({
|
|
1071
1306
|
queryFn: async ({ queryKey, signal, }) => {
|
|
1072
|
-
const
|
|
1073
|
-
...params,
|
|
1074
|
-
...queryKey[0],
|
|
1307
|
+
const requestParams = {
|
|
1308
|
+
...(params[0] || {}),
|
|
1309
|
+
...(queryKey[0] || {}),
|
|
1075
1310
|
signal,
|
|
1076
|
-
|
|
1311
|
+
withResponse: false as const
|
|
1312
|
+
};
|
|
1313
|
+
const res = await this.client.${method}(path, requestParams);
|
|
1077
1314
|
return res as TEndpoint["response"];
|
|
1078
1315
|
},
|
|
1079
1316
|
queryKey: queryKey
|
|
1080
1317
|
}),
|
|
1318
|
+
mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
|
|
1081
1319
|
mutationOptions: {
|
|
1082
1320
|
mutationKey: queryKey,
|
|
1083
1321
|
mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
|
|
1084
|
-
const
|
|
1085
|
-
...params,
|
|
1086
|
-
...queryKey[0],
|
|
1087
|
-
...localOptions,
|
|
1088
|
-
|
|
1322
|
+
const requestParams = {
|
|
1323
|
+
...(params[0] || {}),
|
|
1324
|
+
...(queryKey[0] || {}),
|
|
1325
|
+
...(localOptions || {}),
|
|
1326
|
+
withResponse: false as const
|
|
1327
|
+
};
|
|
1328
|
+
const res = await this.client.${method}(path, requestParams);
|
|
1089
1329
|
return res as TEndpoint["response"];
|
|
1090
1330
|
}
|
|
1091
1331
|
}
|
|
@@ -1099,32 +1339,67 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
1099
1339
|
|
|
1100
1340
|
// <ApiClient.request>
|
|
1101
1341
|
/**
|
|
1102
|
-
* Generic mutation method with full type-safety for any endpoint
|
|
1342
|
+
* Generic mutation method with full type-safety for any endpoint; it doesnt require parameters to be passed initially
|
|
1343
|
+
* but instead will require them to be passed when calling the mutation.mutate() method
|
|
1103
1344
|
*/
|
|
1104
1345
|
mutation<
|
|
1105
1346
|
TMethod extends keyof EndpointByMethod,
|
|
1106
1347
|
TPath extends keyof EndpointByMethod[TMethod],
|
|
1107
1348
|
TEndpoint extends EndpointByMethod[TMethod][TPath],
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1349
|
+
TWithResponse extends boolean = false,
|
|
1350
|
+
TSelection = TWithResponse extends true
|
|
1351
|
+
? InferResponseByStatus<TEndpoint, SuccessStatusCode>
|
|
1352
|
+
: TEndpoint extends { response: infer Res } ? Res : never,
|
|
1353
|
+
TError = TEndpoint extends { responses: infer TResponses }
|
|
1354
|
+
? TResponses extends Record<string | number, unknown>
|
|
1355
|
+
? InferResponseByStatus<TEndpoint, ErrorStatusCode>
|
|
1356
|
+
: Error
|
|
1357
|
+
: Error
|
|
1358
|
+
>(method: TMethod, path: TPath, options?: {
|
|
1359
|
+
withResponse?: TWithResponse;
|
|
1360
|
+
selectFn?: (res: TWithResponse extends true
|
|
1361
|
+
? InferResponseByStatus<TEndpoint, SuccessStatusCode>
|
|
1362
|
+
: TEndpoint extends { response: infer Res } ? Res : never
|
|
1363
|
+
) => TSelection;
|
|
1364
|
+
throwOnStatusError?: boolean
|
|
1365
|
+
}) {
|
|
1113
1366
|
const mutationKey = [{ method, path }] as const;
|
|
1114
1367
|
return {
|
|
1115
1368
|
/** type-only property if you need easy access to the endpoint params */
|
|
1116
1369
|
"~endpoint": {} as TEndpoint,
|
|
1117
1370
|
mutationKey: mutationKey,
|
|
1371
|
+
mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
|
|
1118
1372
|
mutationOptions: {
|
|
1119
1373
|
mutationKey: mutationKey,
|
|
1120
|
-
mutationFn: async
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1374
|
+
mutationFn: async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
|
|
1375
|
+
? InferResponseByStatus<TEndpoint, SuccessStatusCode>
|
|
1376
|
+
: TEndpoint extends { response: infer Res }
|
|
1377
|
+
? Res
|
|
1378
|
+
: never>
|
|
1379
|
+
(params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
|
|
1380
|
+
withResponse?: TLocalWithResponse;
|
|
1381
|
+
throwOnStatusError?: boolean;
|
|
1382
|
+
}): Promise<TLocalSelection> => {
|
|
1383
|
+
const withResponse = params.withResponse ??options?.withResponse ?? false;
|
|
1384
|
+
const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
|
|
1385
|
+
const selectFn = options?.selectFn;
|
|
1386
|
+
const response = await (this.client as any)[method](path, { ...params as any, withResponse: true, throwOnStatusError: false });
|
|
1387
|
+
|
|
1388
|
+
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
1389
|
+
throw new TypedResponseError(response as never);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Return just the data if withResponse is false, otherwise return the full response
|
|
1393
|
+
const finalResponse = withResponse ? response : response.data;
|
|
1394
|
+
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
|
|
1395
|
+
return res as never;
|
|
1396
|
+
}
|
|
1397
|
+
} satisfies import("@tanstack/react-query").UseMutationOptions<TSelection, TError, (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
|
|
1398
|
+
withResponse?: boolean;
|
|
1399
|
+
throwOnStatusError?: boolean;
|
|
1400
|
+
}>,
|
|
1127
1401
|
}
|
|
1402
|
+
}
|
|
1128
1403
|
// </ApiClient.request>
|
|
1129
1404
|
}
|
|
1130
1405
|
`;
|
|
@@ -1136,6 +1411,8 @@ export {
|
|
|
1136
1411
|
createFactory,
|
|
1137
1412
|
createBoxFactory,
|
|
1138
1413
|
openApiSchemaToTs,
|
|
1414
|
+
DEFAULT_SUCCESS_STATUS_CODES,
|
|
1415
|
+
DEFAULT_ERROR_STATUS_CODES,
|
|
1139
1416
|
allowedRuntimes,
|
|
1140
1417
|
generateFile,
|
|
1141
1418
|
createRefResolver,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_ERROR_STATUS_CODES,
|
|
3
|
+
DEFAULT_SUCCESS_STATUS_CODES,
|
|
2
4
|
allowedRuntimes,
|
|
3
5
|
generateFile,
|
|
4
6
|
generateTanstackQueryFile,
|
|
5
7
|
mapOpenApiEndpoints
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-E6A7N4ND.js";
|
|
7
9
|
import {
|
|
8
10
|
prettify
|
|
9
11
|
} from "./chunk-KAEXXJ7X.js";
|
|
@@ -26,20 +28,28 @@ var optionsSchema = type({
|
|
|
26
28
|
"output?": "string",
|
|
27
29
|
runtime: allowedRuntimes,
|
|
28
30
|
tanstack: "boolean | string",
|
|
29
|
-
schemasOnly: "boolean"
|
|
31
|
+
schemasOnly: "boolean",
|
|
32
|
+
"includeClient?": "boolean | 'true' | 'false'",
|
|
33
|
+
"successStatusCodes?": "string",
|
|
34
|
+
"errorStatusCodes?": "string"
|
|
30
35
|
});
|
|
31
36
|
async function generateClientFiles(input, options) {
|
|
32
37
|
const openApiDoc = await SwaggerParser.bundle(input);
|
|
33
38
|
const ctx = mapOpenApiEndpoints(openApiDoc, options);
|
|
34
39
|
console.log(`Found ${ctx.endpointList.length} endpoints`);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const successStatusCodes = options.successStatusCodes ? options.successStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) : void 0;
|
|
41
|
+
const errorStatusCodes = options.errorStatusCodes ? options.errorStatusCodes.split(",").map((code) => parseInt(code.trim(), 10)) : void 0;
|
|
42
|
+
const includeClient = options.includeClient === "false" ? false : options.includeClient === "true" ? true : options.includeClient;
|
|
43
|
+
const generatorOptions = {
|
|
44
|
+
...ctx,
|
|
45
|
+
runtime: options.runtime,
|
|
46
|
+
schemasOnly: options.schemasOnly,
|
|
47
|
+
nameTransform: options.nameTransform,
|
|
48
|
+
includeClient: includeClient ?? true,
|
|
49
|
+
successStatusCodes: successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
|
|
50
|
+
errorStatusCodes: errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES
|
|
51
|
+
};
|
|
52
|
+
const content = await prettify(generateFile(generatorOptions));
|
|
43
53
|
const outputPath = join(
|
|
44
54
|
cwd,
|
|
45
55
|
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`
|
|
@@ -49,7 +59,7 @@ async function generateClientFiles(input, options) {
|
|
|
49
59
|
await writeFile(outputPath, content);
|
|
50
60
|
if (options.tanstack) {
|
|
51
61
|
const tanstackContent = await generateTanstackQueryFile({
|
|
52
|
-
...
|
|
62
|
+
...generatorOptions,
|
|
53
63
|
relativeApiClientPath: "./" + basename(outputPath)
|
|
54
64
|
});
|
|
55
65
|
const tanstackOutputPath = join(
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateClientFiles
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MCVYB63W.js";
|
|
4
4
|
import {
|
|
5
5
|
allowedRuntimes
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-E6A7N4ND.js";
|
|
7
7
|
import "./chunk-KAEXXJ7X.js";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
@@ -12,10 +12,13 @@ import { readFileSync } from "fs";
|
|
|
12
12
|
var { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
13
13
|
var cli = cac(name);
|
|
14
14
|
cli.command("<input>", "Generate").option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)").option(
|
|
15
|
-
"-r, --runtime <
|
|
15
|
+
"-r, --runtime <n>",
|
|
16
16
|
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.toString()}`,
|
|
17
17
|
{ default: "none" }
|
|
18
|
-
).option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false }).option(
|
|
18
|
+
).option("--schemas-only", "Only generate schemas, skipping client generation (defaults to false)", { default: false }).option("--include-client", "Include API client types and implementation (defaults to true)", { default: true }).option(
|
|
19
|
+
"--success-status-codes <codes>",
|
|
20
|
+
"Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)"
|
|
21
|
+
).option("--error-status-codes <codes>", "Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)").option(
|
|
19
22
|
"--tanstack [name]",
|
|
20
23
|
"Generate tanstack client, defaults to false, can optionally specify a name for the generated file"
|
|
21
24
|
).action(async (input, _options) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReferenceObject } from 'openapi3-ts/oas31';
|
|
2
|
-
import { S as StringOrBox, O as OpenapiSchemaConvertContext, L as LibSchemaObject, B as BoxFactory, m as mapOpenApiEndpoints, N as NameTransformOptions, a as OpenapiSchemaConvertArgs, b as Box, A as AnyBoxDef } from './types-
|
|
3
|
-
export { q as AnyBox, j as BoxArray, f as BoxDefinition, i as BoxIntersection, o as BoxKeyword, n as BoxLiteral, p as BoxObject, k as BoxOptional, g as BoxParams, l as BoxRef, h as BoxUnion, c as Endpoint, E as EndpointParameters, F as FactoryCreator, G as GenericFactory, M as Method, R as RefInfo, e as RefResolver, W as WithSchema, d as createRefResolver } from './types-
|
|
2
|
+
import { S as StringOrBox, O as OpenapiSchemaConvertContext, L as LibSchemaObject, B as BoxFactory, m as mapOpenApiEndpoints, N as NameTransformOptions, a as OpenapiSchemaConvertArgs, b as Box, A as AnyBoxDef } from './types-DsI2d-HE.js';
|
|
3
|
+
export { q as AnyBox, j as BoxArray, f as BoxDefinition, i as BoxIntersection, o as BoxKeyword, n as BoxLiteral, p as BoxObject, k as BoxOptional, g as BoxParams, l as BoxRef, h as BoxUnion, c as Endpoint, E as EndpointParameters, F as FactoryCreator, G as GenericFactory, M as Method, R as RefInfo, e as RefResolver, W as WithSchema, d as createRefResolver } from './types-DsI2d-HE.js';
|
|
4
4
|
import * as arktype_internal_methods_string_ts from 'arktype/internal/methods/string.ts';
|
|
5
5
|
import * as Codegen from '@sinclair/typebox-codegen';
|
|
6
6
|
import 'openapi3-ts/oas30';
|
|
@@ -16,6 +16,9 @@ type GeneratorOptions$1 = ReturnType<typeof mapOpenApiEndpoints> & {
|
|
|
16
16
|
runtime?: "none" | keyof typeof runtimeValidationGenerator;
|
|
17
17
|
schemasOnly?: boolean;
|
|
18
18
|
nameTransform?: NameTransformOptions | undefined;
|
|
19
|
+
successStatusCodes?: readonly number[];
|
|
20
|
+
errorStatusCodes?: readonly number[];
|
|
21
|
+
includeClient?: boolean;
|
|
19
22
|
};
|
|
20
23
|
declare const allowedRuntimes: arktype_internal_methods_string_ts.StringType<"none" | "arktype" | "io-ts" | "typebox" | "valibot" | "yup" | "zod", {}>;
|
|
21
24
|
type OutputRuntime = typeof allowedRuntimes.infer;
|
|
@@ -30,7 +33,9 @@ declare const runtimeValidationGenerator: {
|
|
|
30
33
|
declare const generateFile: (options: GeneratorOptions$1) => string;
|
|
31
34
|
|
|
32
35
|
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints>;
|
|
33
|
-
type GeneratorContext = Required<GeneratorOptions
|
|
36
|
+
type GeneratorContext = Required<GeneratorOptions> & {
|
|
37
|
+
errorStatusCodes?: readonly number[];
|
|
38
|
+
};
|
|
34
39
|
declare const generateTanstackQueryFile: (ctx: GeneratorContext & {
|
|
35
40
|
relativeApiClientPath: string;
|
|
36
41
|
}) => Promise<string>;
|