typed-openapi 2.1.2 → 2.2.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-UNKLDND3.js → chunk-IB6GEWS7.js} +15 -41
- package/dist/{chunk-X4A4YQN7.js → chunk-QGMS7IBQ.js} +219 -105
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/node.export.js +2 -2
- package/package.json +3 -1
- package/src/default-fetcher.generator.ts +13 -39
- package/src/generate-client-files.ts +1 -1
- package/src/generator.ts +156 -74
- package/src/map-openapi-endpoints.ts +42 -11
- package/src/tanstack-query.generator.ts +44 -41
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
generateFile,
|
|
6
6
|
generateTanstackQueryFile,
|
|
7
7
|
mapOpenApiEndpoints
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QGMS7IBQ.js";
|
|
9
9
|
import {
|
|
10
10
|
prettify
|
|
11
11
|
} from "./chunk-KAEXXJ7X.js";
|
|
@@ -35,13 +35,12 @@ var generateDefaultFetcher = (options) => {
|
|
|
35
35
|
* - Basic error handling
|
|
36
36
|
*
|
|
37
37
|
* Usage:
|
|
38
|
-
* 1. Replace '
|
|
38
|
+
* 1. Replace './${clientPath}' with your actual generated file path
|
|
39
39
|
* 2. Set your ${envApiBaseUrl}
|
|
40
40
|
* 3. Customize error handling and headers as needed
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
import { type Fetcher, createApiClient } from "./${clientPath}";
|
|
43
|
+
import { type Fetcher, createApiClient } from "${clientPath}";
|
|
45
44
|
|
|
46
45
|
// Basic configuration
|
|
47
46
|
const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example.com";
|
|
@@ -49,32 +48,17 @@ const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example
|
|
|
49
48
|
/**
|
|
50
49
|
* Simple fetcher implementation without external dependencies
|
|
51
50
|
*/
|
|
52
|
-
|
|
51
|
+
const ${fetcherName}: Fetcher["fetch"] = async (input) => {
|
|
53
52
|
const headers = new Headers();
|
|
54
53
|
|
|
55
|
-
// Replace path parameters (supports both {param} and :param formats)
|
|
56
|
-
const actualUrl = replacePathParams(apiUrl, (params?.path ?? {}) as Record<string, string>);
|
|
57
|
-
const url = new URL(actualUrl);
|
|
58
|
-
|
|
59
54
|
// Handle query parameters
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
Object.entries(params.query).forEach(([key, value]) => {
|
|
63
|
-
if (value != null) {
|
|
64
|
-
// Skip null/undefined values
|
|
65
|
-
if (Array.isArray(value)) {
|
|
66
|
-
value.forEach((val) => val != null && searchParams.append(key, String(val)));
|
|
67
|
-
} else {
|
|
68
|
-
searchParams.append(key, String(value));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
url.search = searchParams.toString();
|
|
55
|
+
if (input.urlSearchParams) {
|
|
56
|
+
input.url.search = input.urlSearchParams.toString();
|
|
73
57
|
}
|
|
74
58
|
|
|
75
59
|
// Handle request body for mutation methods
|
|
76
|
-
const body = ["post", "put", "patch", "delete"].includes(method.toLowerCase())
|
|
77
|
-
? JSON.stringify(
|
|
60
|
+
const body = ["post", "put", "patch", "delete"].includes(input.method.toLowerCase())
|
|
61
|
+
? JSON.stringify(input.parameters?.body)
|
|
78
62
|
: undefined;
|
|
79
63
|
|
|
80
64
|
if (body) {
|
|
@@ -82,35 +66,25 @@ export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
|
|
|
82
66
|
}
|
|
83
67
|
|
|
84
68
|
// Add custom headers
|
|
85
|
-
if (
|
|
86
|
-
Object.entries(
|
|
69
|
+
if (input.parameters?.header) {
|
|
70
|
+
Object.entries(input.parameters.header).forEach(([key, value]) => {
|
|
87
71
|
if (value != null) {
|
|
88
72
|
headers.set(key, String(value));
|
|
89
73
|
}
|
|
90
74
|
});
|
|
91
75
|
}
|
|
92
76
|
|
|
93
|
-
const response = await fetch(url, {
|
|
94
|
-
method: method.toUpperCase(),
|
|
77
|
+
const response = await fetch(input.url, {
|
|
78
|
+
method: input.method.toUpperCase(),
|
|
95
79
|
...(body && { body }),
|
|
96
80
|
headers,
|
|
81
|
+
...input.overrides,
|
|
97
82
|
});
|
|
98
83
|
|
|
99
84
|
return response;
|
|
100
85
|
};
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
* Replace path parameters in URL
|
|
104
|
-
* Supports both OpenAPI format {param} and Express format :param
|
|
105
|
-
*/
|
|
106
|
-
export function replacePathParams(url: string, params: Record<string, string>): string {
|
|
107
|
-
return url
|
|
108
|
-
.replace(/{(\\w+)}/g, function(_, key) { return params[key] || '{' + key + '}'; })
|
|
109
|
-
.replace(/:([a-zA-Z0-9_]+)/g, function(_, key) { return params[key] || ':' + key; });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export const ${apiName} = createApiClient(${fetcherName}, API_BASE_URL);
|
|
113
|
-
`;
|
|
87
|
+
export const ${apiName} = createApiClient({ fetch: ${fetcherName} }, API_BASE_URL);`;
|
|
114
88
|
};
|
|
115
89
|
|
|
116
90
|
// src/generate-client-files.ts
|
|
@@ -183,7 +157,7 @@ async function generateClientFiles(input, options) {
|
|
|
183
157
|
if (options.defaultFetcher) {
|
|
184
158
|
const defaultFetcherContent = generateDefaultFetcher({
|
|
185
159
|
envApiBaseUrl: options.defaultFetcher.envApiBaseUrl,
|
|
186
|
-
clientPath: options.defaultFetcher.clientPath ?? basename(outputPath),
|
|
160
|
+
clientPath: options.defaultFetcher.clientPath ?? join(dirname(outputPath), basename(outputPath)),
|
|
187
161
|
fetcherName: options.defaultFetcher.fetcherName,
|
|
188
162
|
apiName: options.defaultFetcher.apiName
|
|
189
163
|
});
|
|
@@ -574,7 +574,21 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
574
574
|
responseHeaders?: TConfig["responseHeaders"]
|
|
575
575
|
};
|
|
576
576
|
|
|
577
|
-
export
|
|
577
|
+
export interface Fetcher {
|
|
578
|
+
decodePathParams?: (path: string, pathParams: Record<string, string>) => string
|
|
579
|
+
encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
|
|
580
|
+
//
|
|
581
|
+
fetch: (input: {
|
|
582
|
+
method: Method;
|
|
583
|
+
url: URL;
|
|
584
|
+
urlSearchParams?: URLSearchParams | undefined;
|
|
585
|
+
parameters?: EndpointParameters | undefined;
|
|
586
|
+
path: string;
|
|
587
|
+
overrides?: RequestInit;
|
|
588
|
+
throwOnStatusError?: boolean
|
|
589
|
+
}) => Promise<Response>;
|
|
590
|
+
parseResponseData?: (response: Response) => Promise<unknown>
|
|
591
|
+
}
|
|
578
592
|
|
|
579
593
|
export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
|
|
580
594
|
export type SuccessStatusCode = typeof successStatusCodes[number];
|
|
@@ -647,9 +661,12 @@ type RequiredKeys<T> = {
|
|
|
647
661
|
}[keyof T];
|
|
648
662
|
|
|
649
663
|
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
|
|
664
|
+
type NotNever<T> = [T] extends [never] ? false : true;
|
|
650
665
|
|
|
651
666
|
// </ApiClientTypes>
|
|
652
667
|
`;
|
|
668
|
+
const infer = inferByRuntime[ctx.runtime];
|
|
669
|
+
const InferTEndpoint = match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`)).otherwise(() => `TEndpoint`);
|
|
653
670
|
const apiClient = `
|
|
654
671
|
// <TypedResponseError>
|
|
655
672
|
export class TypedResponseError extends Error {
|
|
@@ -663,6 +680,7 @@ export class TypedResponseError extends Error {
|
|
|
663
680
|
}
|
|
664
681
|
}
|
|
665
682
|
// </TypedResponseError>
|
|
683
|
+
|
|
666
684
|
// <ApiClient>
|
|
667
685
|
export class ApiClient {
|
|
668
686
|
baseUrl: string = "";
|
|
@@ -676,64 +694,86 @@ export class ApiClient {
|
|
|
676
694
|
return this;
|
|
677
695
|
}
|
|
678
696
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Replace path parameters in URL
|
|
699
|
+
* Supports both OpenAPI format {param} and Express format :param
|
|
700
|
+
*/
|
|
701
|
+
defaultDecodePathParams = (url: string, params: Record<string, string>): string => {
|
|
702
|
+
return url
|
|
703
|
+
.replace(/{(\\w+)}/g, (_, key: string) => params[key] || \`{\${key}}\`)
|
|
704
|
+
.replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || \`:\${key}\`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Uses URLSearchParams, skips null/undefined values */
|
|
708
|
+
defaultEncodeSearchParams = (queryParams: Record<string, unknown> | undefined): URLSearchParams | undefined => {
|
|
709
|
+
if (!queryParams) return;
|
|
710
|
+
|
|
711
|
+
const searchParams = new URLSearchParams();
|
|
712
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
713
|
+
if (value != null) {
|
|
714
|
+
// Skip null/undefined values
|
|
715
|
+
if (Array.isArray(value)) {
|
|
716
|
+
value.forEach((val) => val != null && searchParams.append(key, String(val)));
|
|
717
|
+
} else {
|
|
718
|
+
searchParams.append(key, String(value));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
return searchParams;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
defaultParseResponseData = async (response: Response): Promise<unknown> => {
|
|
727
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
728
|
+
if (contentType.startsWith("text/")) {
|
|
729
|
+
return (await response.text())
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (contentType === "application/octet-stream") {
|
|
733
|
+
return (await response.arrayBuffer())
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (
|
|
737
|
+
contentType.includes("application/json") ||
|
|
738
|
+
(contentType.includes("application/") && contentType.includes("json")) ||
|
|
739
|
+
contentType === "*/*"
|
|
740
|
+
) {
|
|
741
|
+
try {
|
|
742
|
+
return await response.json();
|
|
743
|
+
} catch {
|
|
744
|
+
return undefined
|
|
745
|
+
}
|
|
683
746
|
}
|
|
684
|
-
|
|
747
|
+
|
|
748
|
+
return
|
|
685
749
|
}
|
|
686
750
|
|
|
687
751
|
${Object.entries(byMethods).map(([method, endpointByMethod]) => {
|
|
688
752
|
const capitalizedMethod = capitalize2(method);
|
|
689
|
-
const infer = inferByRuntime[ctx.runtime];
|
|
690
753
|
return endpointByMethod.length ? `// <ApiClient.${method}>
|
|
691
754
|
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
692
755
|
path: Path,
|
|
693
|
-
...params: MaybeOptionalArg
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
() => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`
|
|
700
|
-
).otherwise(() => `Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]`)}>;
|
|
756
|
+
...params: MaybeOptionalArg<
|
|
757
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
758
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
759
|
+
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
|
|
760
|
+
>
|
|
761
|
+
): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>;
|
|
701
762
|
|
|
702
763
|
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
703
764
|
path: Path,
|
|
704
|
-
...params: MaybeOptionalArg
|
|
765
|
+
...params: MaybeOptionalArg<
|
|
766
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
767
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
|
768
|
+
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
|
|
769
|
+
>
|
|
705
770
|
): Promise<SafeApiResponse<TEndpoint>>;
|
|
706
771
|
|
|
707
|
-
${method}<Path extends keyof ${capitalizedMethod}Endpoints,
|
|
772
|
+
${method}<Path extends keyof ${capitalizedMethod}Endpoints, _TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
708
773
|
path: Path,
|
|
709
774
|
...params: MaybeOptionalArg<any>
|
|
710
775
|
): Promise<any> {
|
|
711
|
-
|
|
712
|
-
const withResponse = requestParams?.withResponse;
|
|
713
|
-
const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
|
|
714
|
-
|
|
715
|
-
const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
|
|
716
|
-
.then(async (response) => {
|
|
717
|
-
const data = await this.parseResponse(response);
|
|
718
|
-
const typedResponse = Object.assign(response, {
|
|
719
|
-
data: data,
|
|
720
|
-
json: () => Promise.resolve(data)
|
|
721
|
-
}) as SafeApiResponse<TEndpoint>;
|
|
722
|
-
|
|
723
|
-
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
724
|
-
throw new TypedResponseError(typedResponse as never);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
return withResponse ? typedResponse : data;
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
return promise ${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`).with(
|
|
731
|
-
"arktype",
|
|
732
|
-
"io-ts",
|
|
733
|
-
"typebox",
|
|
734
|
-
"valibot",
|
|
735
|
-
() => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`
|
|
736
|
-
).otherwise(() => `as Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>`)}
|
|
776
|
+
return this.request("${method}", path, ...params);
|
|
737
777
|
}
|
|
738
778
|
// </ApiClient.${method}>
|
|
739
779
|
` : "";
|
|
@@ -750,19 +790,74 @@ export class ApiClient {
|
|
|
750
790
|
>(
|
|
751
791
|
method: TMethod,
|
|
752
792
|
path: TPath,
|
|
753
|
-
...params: MaybeOptionalArg
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
"
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
793
|
+
...params: MaybeOptionalArg<
|
|
794
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
795
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
796
|
+
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
|
|
797
|
+
>
|
|
798
|
+
): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>
|
|
799
|
+
|
|
800
|
+
request<
|
|
801
|
+
TMethod extends keyof EndpointByMethod,
|
|
802
|
+
TPath extends keyof EndpointByMethod[TMethod],
|
|
803
|
+
TEndpoint extends EndpointByMethod[TMethod][TPath]
|
|
804
|
+
>(
|
|
805
|
+
method: TMethod,
|
|
806
|
+
path: TPath,
|
|
807
|
+
...params: MaybeOptionalArg<
|
|
808
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
809
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
|
810
|
+
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
|
|
811
|
+
>
|
|
812
|
+
): Promise<SafeApiResponse<TEndpoint>>;
|
|
813
|
+
|
|
814
|
+
request<
|
|
815
|
+
TMethod extends keyof EndpointByMethod,
|
|
816
|
+
TPath extends keyof EndpointByMethod[TMethod],
|
|
817
|
+
TEndpoint extends EndpointByMethod[TMethod][TPath]
|
|
818
|
+
>(
|
|
819
|
+
method: TMethod,
|
|
820
|
+
path: TPath,
|
|
821
|
+
...params: MaybeOptionalArg<any>
|
|
822
|
+
): Promise<any> {
|
|
823
|
+
const requestParams = params[0];
|
|
824
|
+
const withResponse = requestParams?.withResponse;
|
|
825
|
+
const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {};
|
|
826
|
+
|
|
827
|
+
const parametersToSend: EndpointParameters = {};
|
|
828
|
+
if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body;
|
|
829
|
+
if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query;
|
|
830
|
+
if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header;
|
|
831
|
+
if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path;
|
|
832
|
+
|
|
833
|
+
const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)(this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record<string, string>);
|
|
834
|
+
const url = new URL(resolvedPath);
|
|
835
|
+
const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query);
|
|
836
|
+
|
|
837
|
+
const promise = this.fetcher.fetch({
|
|
838
|
+
method: method,
|
|
839
|
+
path: (path as string),
|
|
840
|
+
url,
|
|
841
|
+
urlSearchParams,
|
|
842
|
+
parameters: Object.keys(fetchParams).length ? fetchParams : undefined,
|
|
843
|
+
overrides,
|
|
844
|
+
throwOnStatusError
|
|
845
|
+
})
|
|
846
|
+
.then(async (response) => {
|
|
847
|
+
const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response);
|
|
848
|
+
const typedResponse = Object.assign(response, {
|
|
849
|
+
data: data,
|
|
850
|
+
json: () => Promise.resolve(data)
|
|
851
|
+
}) as SafeApiResponse<TEndpoint>;
|
|
852
|
+
|
|
853
|
+
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
854
|
+
throw new TypedResponseError(typedResponse as never);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return withResponse ? typedResponse : data;
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
return promise as Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]
|
|
766
861
|
}
|
|
767
862
|
// </ApiClient.request>
|
|
768
863
|
}
|
|
@@ -1171,7 +1266,7 @@ var mapOpenApiEndpoints = (doc, options) => {
|
|
|
1171
1266
|
endpoint.parameters = Object.keys(params).length ? params : void 0;
|
|
1172
1267
|
}
|
|
1173
1268
|
const allResponses = {};
|
|
1174
|
-
const
|
|
1269
|
+
const allResponseHeaders = {};
|
|
1175
1270
|
Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
|
|
1176
1271
|
const responseObj = refs.unwrap(responseOrRef);
|
|
1177
1272
|
const content = responseObj?.content;
|
|
@@ -1179,11 +1274,19 @@ var mapOpenApiEndpoints = (doc, options) => {
|
|
|
1179
1274
|
if (content && mediaTypes.length) {
|
|
1180
1275
|
mediaTypes.forEach((mediaType) => {
|
|
1181
1276
|
const schema = content[mediaType] ? content[mediaType].schema ?? {} : {};
|
|
1182
|
-
const t2 = createBoxFactory(schema, ctx);
|
|
1183
1277
|
const mediaTypeResponse = openApiSchemaToTs({ schema, ctx });
|
|
1184
1278
|
if (allResponses[status]) {
|
|
1279
|
+
const t2 = createBoxFactory(
|
|
1280
|
+
{
|
|
1281
|
+
oneOf: [
|
|
1282
|
+
...allResponses[status].schema.oneOf ? allResponses[status].schema.oneOf : [allResponses[status].schema],
|
|
1283
|
+
schema
|
|
1284
|
+
]
|
|
1285
|
+
},
|
|
1286
|
+
ctx
|
|
1287
|
+
);
|
|
1185
1288
|
allResponses[status] = t2.union([
|
|
1186
|
-
...
|
|
1289
|
+
...allResponses[status].type === "union" ? allResponses[status].params.types : [allResponses[status]],
|
|
1187
1290
|
mediaTypeResponse
|
|
1188
1291
|
]);
|
|
1189
1292
|
} else {
|
|
@@ -1193,10 +1296,18 @@ var mapOpenApiEndpoints = (doc, options) => {
|
|
|
1193
1296
|
} else {
|
|
1194
1297
|
const schema = {};
|
|
1195
1298
|
const unknown = openApiSchemaToTs({ schema: {}, ctx });
|
|
1196
|
-
const t2 = createBoxFactory(schema, ctx);
|
|
1197
1299
|
if (allResponses[status]) {
|
|
1300
|
+
const t2 = createBoxFactory(
|
|
1301
|
+
{
|
|
1302
|
+
oneOf: [
|
|
1303
|
+
...allResponses[status].schema.oneOf ? allResponses[status].schema.oneOf : [allResponses[status].schema],
|
|
1304
|
+
schema
|
|
1305
|
+
]
|
|
1306
|
+
},
|
|
1307
|
+
ctx
|
|
1308
|
+
);
|
|
1198
1309
|
allResponses[status] = t2.union([
|
|
1199
|
-
...
|
|
1310
|
+
...allResponses[status].type === "union" ? allResponses[status].params.types : [allResponses[status]],
|
|
1200
1311
|
unknown
|
|
1201
1312
|
]);
|
|
1202
1313
|
} else {
|
|
@@ -1219,15 +1330,15 @@ var mapOpenApiEndpoints = (doc, options) => {
|
|
|
1219
1330
|
{}
|
|
1220
1331
|
);
|
|
1221
1332
|
if (Object.keys(mappedHeaders).length) {
|
|
1222
|
-
|
|
1333
|
+
allResponseHeaders[status] = t.object(mappedHeaders);
|
|
1223
1334
|
}
|
|
1224
1335
|
}
|
|
1225
1336
|
});
|
|
1226
1337
|
if (Object.keys(allResponses).length > 0) {
|
|
1227
1338
|
endpoint.responses = allResponses;
|
|
1228
1339
|
}
|
|
1229
|
-
if (Object.keys(
|
|
1230
|
-
endpoint.responseHeaders =
|
|
1340
|
+
if (Object.keys(allResponseHeaders).length) {
|
|
1341
|
+
endpoint.responseHeaders = allResponseHeaders;
|
|
1231
1342
|
}
|
|
1232
1343
|
endpointList.push(endpoint);
|
|
1233
1344
|
});
|
|
@@ -1314,7 +1425,10 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
1314
1425
|
${Array.from(endpointMethods).map(
|
|
1315
1426
|
(method) => `
|
|
1316
1427
|
// <ApiClient.${method}>
|
|
1317
|
-
${method}<
|
|
1428
|
+
${method}<
|
|
1429
|
+
Path extends keyof ${capitalize4(method)}Endpoints,
|
|
1430
|
+
TEndpoint extends ${capitalize4(method)}Endpoints[Path]
|
|
1431
|
+
>(
|
|
1318
1432
|
path: Path,
|
|
1319
1433
|
...params: MaybeOptionalArg<TEndpoint["parameters"]>
|
|
1320
1434
|
) {
|
|
@@ -1329,28 +1443,14 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
1329
1443
|
const requestParams = {
|
|
1330
1444
|
...(params[0] || {}),
|
|
1331
1445
|
...(queryKey[0] || {}),
|
|
1332
|
-
signal,
|
|
1446
|
+
overrides: { signal },
|
|
1333
1447
|
withResponse: false as const
|
|
1334
1448
|
};
|
|
1335
|
-
const res = await this.client.${method}(path, requestParams);
|
|
1449
|
+
const res = await this.client.${method}(path, requestParams as never);
|
|
1336
1450
|
return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
|
|
1337
1451
|
},
|
|
1338
1452
|
queryKey: queryKey
|
|
1339
1453
|
}),
|
|
1340
|
-
mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
|
|
1341
|
-
mutationOptions: {
|
|
1342
|
-
mutationKey: queryKey,
|
|
1343
|
-
mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
|
|
1344
|
-
const requestParams = {
|
|
1345
|
-
...(params[0] || {}),
|
|
1346
|
-
...(queryKey[0] || {}),
|
|
1347
|
-
...(localOptions || {}),
|
|
1348
|
-
withResponse: false as const
|
|
1349
|
-
};
|
|
1350
|
-
const res = await this.client.${method}(path, requestParams);
|
|
1351
|
-
return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
1454
|
};
|
|
1355
1455
|
|
|
1356
1456
|
return query
|
|
@@ -1384,40 +1484,54 @@ var generateTanstackQueryFile = async (ctx) => {
|
|
|
1384
1484
|
: Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]
|
|
1385
1485
|
) => TSelection;
|
|
1386
1486
|
throwOnStatusError?: boolean
|
|
1487
|
+
throwOnError?: boolean | ((error: TError) => boolean)
|
|
1387
1488
|
}) {
|
|
1388
1489
|
const mutationKey = [{ method, path }] as const;
|
|
1490
|
+
const mutationFn = async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
|
|
1491
|
+
? InferResponseByStatus<TEndpoint, SuccessStatusCode>
|
|
1492
|
+
: Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>
|
|
1493
|
+
(params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
|
|
1494
|
+
withResponse?: TLocalWithResponse;
|
|
1495
|
+
throwOnStatusError?: boolean;
|
|
1496
|
+
overrides?: RequestInit;
|
|
1497
|
+
}): Promise<TLocalSelection> => {
|
|
1498
|
+
const withResponse = params.withResponse ??options?.withResponse ?? false;
|
|
1499
|
+
const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
|
|
1500
|
+
const selectFn = options?.selectFn;
|
|
1501
|
+
const response = await (this.client as any)[method](path, {
|
|
1502
|
+
...params as any,
|
|
1503
|
+
withResponse: true,
|
|
1504
|
+
throwOnStatusError: false,
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
1508
|
+
throw new TypedResponseError(response as never);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Return just the data if withResponse is false, otherwise return the full response
|
|
1512
|
+
const finalResponse = withResponse ? response : response.data;
|
|
1513
|
+
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
|
|
1514
|
+
return res as never;
|
|
1515
|
+
};
|
|
1389
1516
|
return {
|
|
1390
1517
|
/** type-only property if you need easy access to the endpoint params */
|
|
1391
1518
|
"~endpoint": {} as TEndpoint,
|
|
1392
1519
|
mutationKey: mutationKey,
|
|
1393
1520
|
mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
|
|
1394
1521
|
mutationOptions: {
|
|
1522
|
+
throwOnError: options?.throwOnError as boolean | ((error: TError) => boolean),
|
|
1395
1523
|
mutationKey: mutationKey,
|
|
1396
|
-
mutationFn:
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const withResponse = params.withResponse ??options?.withResponse ?? false;
|
|
1404
|
-
const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
|
|
1405
|
-
const selectFn = options?.selectFn;
|
|
1406
|
-
const response = await (this.client as any)[method](path, { ...params as any, withResponse: true, throwOnStatusError: false });
|
|
1407
|
-
|
|
1408
|
-
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
1409
|
-
throw new TypedResponseError(response as never);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Return just the data if withResponse is false, otherwise return the full response
|
|
1413
|
-
const finalResponse = withResponse ? response : response.data;
|
|
1414
|
-
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
|
|
1415
|
-
return res as never;
|
|
1524
|
+
mutationFn: mutationFn,
|
|
1525
|
+
} as Omit<import("@tanstack/react-query").UseMutationOptions<
|
|
1526
|
+
TSelection,
|
|
1527
|
+
TError,
|
|
1528
|
+
(TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
|
|
1529
|
+
withResponse?: boolean;
|
|
1530
|
+
throwOnStatusError?: boolean;
|
|
1416
1531
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
}>,
|
|
1532
|
+
>, "mutationFn"> & {
|
|
1533
|
+
mutationFn: typeof mutationFn
|
|
1534
|
+
},
|
|
1421
1535
|
}
|
|
1422
1536
|
}
|
|
1423
1537
|
// </ApiClient.request>
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/dist/node.export.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typed-openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@types/node": "^22.15.17",
|
|
35
35
|
"@types/prettier": "3.0.0",
|
|
36
36
|
"msw": "2.10.5",
|
|
37
|
+
"tstyche": "4.3.0",
|
|
37
38
|
"tsup": "^8.4.0",
|
|
38
39
|
"typescript": "^5.8.3",
|
|
39
40
|
"vitest": "^3.1.3",
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"dev": "tsup --watch",
|
|
68
69
|
"build": "tsup",
|
|
69
70
|
"test": "vitest",
|
|
71
|
+
"test:types": "tstyche",
|
|
70
72
|
"gen:runtime": "node bin.js ./tests/samples/petstore.yaml --output ./tmp/generated-client.ts --tanstack generated-tanstack.ts --default-fetcher",
|
|
71
73
|
"test:runtime:run": "vitest run tests/integration-runtime-msw.test.ts",
|
|
72
74
|
"test:runtime": "pnpm run gen:runtime && pnpm run test:runtime:run",
|
|
@@ -22,13 +22,12 @@ export const generateDefaultFetcher = (options: {
|
|
|
22
22
|
* - Basic error handling
|
|
23
23
|
*
|
|
24
24
|
* Usage:
|
|
25
|
-
* 1. Replace '
|
|
25
|
+
* 1. Replace './${clientPath}' with your actual generated file path
|
|
26
26
|
* 2. Set your ${envApiBaseUrl}
|
|
27
27
|
* 3. Customize error handling and headers as needed
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
import { type Fetcher, createApiClient } from "./${clientPath}";
|
|
30
|
+
import { type Fetcher, createApiClient } from "${clientPath}";
|
|
32
31
|
|
|
33
32
|
// Basic configuration
|
|
34
33
|
const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example.com";
|
|
@@ -36,32 +35,17 @@ const ${envApiBaseUrl} = process.env["${envApiBaseUrl}"] || "https://api.example
|
|
|
36
35
|
/**
|
|
37
36
|
* Simple fetcher implementation without external dependencies
|
|
38
37
|
*/
|
|
39
|
-
|
|
38
|
+
const ${fetcherName}: Fetcher["fetch"] = async (input) => {
|
|
40
39
|
const headers = new Headers();
|
|
41
40
|
|
|
42
|
-
// Replace path parameters (supports both {param} and :param formats)
|
|
43
|
-
const actualUrl = replacePathParams(apiUrl, (params?.path ?? {}) as Record<string, string>);
|
|
44
|
-
const url = new URL(actualUrl);
|
|
45
|
-
|
|
46
41
|
// Handle query parameters
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
Object.entries(params.query).forEach(([key, value]) => {
|
|
50
|
-
if (value != null) {
|
|
51
|
-
// Skip null/undefined values
|
|
52
|
-
if (Array.isArray(value)) {
|
|
53
|
-
value.forEach((val) => val != null && searchParams.append(key, String(val)));
|
|
54
|
-
} else {
|
|
55
|
-
searchParams.append(key, String(value));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
url.search = searchParams.toString();
|
|
42
|
+
if (input.urlSearchParams) {
|
|
43
|
+
input.url.search = input.urlSearchParams.toString();
|
|
60
44
|
}
|
|
61
45
|
|
|
62
46
|
// Handle request body for mutation methods
|
|
63
|
-
const body = ["post", "put", "patch", "delete"].includes(method.toLowerCase())
|
|
64
|
-
? JSON.stringify(
|
|
47
|
+
const body = ["post", "put", "patch", "delete"].includes(input.method.toLowerCase())
|
|
48
|
+
? JSON.stringify(input.parameters?.body)
|
|
65
49
|
: undefined;
|
|
66
50
|
|
|
67
51
|
if (body) {
|
|
@@ -69,33 +53,23 @@ export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
|
|
|
69
53
|
}
|
|
70
54
|
|
|
71
55
|
// Add custom headers
|
|
72
|
-
if (
|
|
73
|
-
Object.entries(
|
|
56
|
+
if (input.parameters?.header) {
|
|
57
|
+
Object.entries(input.parameters.header).forEach(([key, value]) => {
|
|
74
58
|
if (value != null) {
|
|
75
59
|
headers.set(key, String(value));
|
|
76
60
|
}
|
|
77
61
|
});
|
|
78
62
|
}
|
|
79
63
|
|
|
80
|
-
const response = await fetch(url, {
|
|
81
|
-
method: method.toUpperCase(),
|
|
64
|
+
const response = await fetch(input.url, {
|
|
65
|
+
method: input.method.toUpperCase(),
|
|
82
66
|
...(body && { body }),
|
|
83
67
|
headers,
|
|
68
|
+
...input.overrides,
|
|
84
69
|
});
|
|
85
70
|
|
|
86
71
|
return response;
|
|
87
72
|
};
|
|
88
73
|
|
|
89
|
-
|
|
90
|
-
* Replace path parameters in URL
|
|
91
|
-
* Supports both OpenAPI format {param} and Express format :param
|
|
92
|
-
*/
|
|
93
|
-
export function replacePathParams(url: string, params: Record<string, string>): string {
|
|
94
|
-
return url
|
|
95
|
-
.replace(/\{(\\w+)\}/g, function(_, key) { return params[key] || '{' + key + '}'; })
|
|
96
|
-
.replace(/:([a-zA-Z0-9_]+)/g, function(_, key) { return params[key] || ':' + key; });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export const ${apiName} = createApiClient(${fetcherName}, API_BASE_URL);
|
|
100
|
-
`;
|
|
74
|
+
export const ${apiName} = createApiClient({ fetch: ${fetcherName} }, API_BASE_URL);`;
|
|
101
75
|
};
|
|
@@ -110,7 +110,7 @@ export async function generateClientFiles(input: string, options: GenerateClient
|
|
|
110
110
|
if (options.defaultFetcher) {
|
|
111
111
|
const defaultFetcherContent = generateDefaultFetcher({
|
|
112
112
|
envApiBaseUrl: options.defaultFetcher.envApiBaseUrl,
|
|
113
|
-
clientPath: options.defaultFetcher.clientPath ?? basename(outputPath),
|
|
113
|
+
clientPath: options.defaultFetcher.clientPath ?? join(dirname(outputPath), basename(outputPath)),
|
|
114
114
|
fetcherName: options.defaultFetcher.fetcherName,
|
|
115
115
|
apiName: options.defaultFetcher.apiName,
|
|
116
116
|
});
|
package/src/generator.ts
CHANGED
|
@@ -233,11 +233,7 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
|
|
|
233
233
|
: "parameters: never,"
|
|
234
234
|
}
|
|
235
235
|
${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
|
|
236
|
-
${
|
|
237
|
-
endpoint.responseHeaders
|
|
238
|
-
? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},`
|
|
239
|
-
: ""
|
|
240
|
-
}
|
|
236
|
+
${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders)},` : ""}
|
|
241
237
|
}\n`;
|
|
242
238
|
});
|
|
243
239
|
|
|
@@ -329,7 +325,21 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
|
329
325
|
responseHeaders?: TConfig["responseHeaders"]
|
|
330
326
|
};
|
|
331
327
|
|
|
332
|
-
export
|
|
328
|
+
export interface Fetcher {
|
|
329
|
+
decodePathParams?: (path: string, pathParams: Record<string, string>) => string
|
|
330
|
+
encodeSearchParams?: (searchParams: Record<string, unknown> | undefined) => URLSearchParams
|
|
331
|
+
//
|
|
332
|
+
fetch: (input: {
|
|
333
|
+
method: Method;
|
|
334
|
+
url: URL;
|
|
335
|
+
urlSearchParams?: URLSearchParams | undefined;
|
|
336
|
+
parameters?: EndpointParameters | undefined;
|
|
337
|
+
path: string;
|
|
338
|
+
overrides?: RequestInit;
|
|
339
|
+
throwOnStatusError?: boolean
|
|
340
|
+
}) => Promise<Response>;
|
|
341
|
+
parseResponseData?: (response: Response) => Promise<unknown>
|
|
342
|
+
}
|
|
333
343
|
|
|
334
344
|
export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
|
|
335
345
|
export type SuccessStatusCode = typeof successStatusCodes[number];
|
|
@@ -402,10 +412,17 @@ type RequiredKeys<T> = {
|
|
|
402
412
|
}[keyof T];
|
|
403
413
|
|
|
404
414
|
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
|
|
415
|
+
type NotNever<T> = [T] extends [never] ? false : true;
|
|
405
416
|
|
|
406
417
|
// </ApiClientTypes>
|
|
407
418
|
`;
|
|
408
419
|
|
|
420
|
+
const infer = inferByRuntime[ctx.runtime];
|
|
421
|
+
const InferTEndpoint = match(ctx.runtime)
|
|
422
|
+
.with("zod", "yup", () => infer(`TEndpoint`))
|
|
423
|
+
.with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`))
|
|
424
|
+
.otherwise(() => `TEndpoint`);
|
|
425
|
+
|
|
409
426
|
const apiClient = `
|
|
410
427
|
// <TypedResponseError>
|
|
411
428
|
export class TypedResponseError extends Error {
|
|
@@ -419,6 +436,7 @@ export class TypedResponseError extends Error {
|
|
|
419
436
|
}
|
|
420
437
|
}
|
|
421
438
|
// </TypedResponseError>
|
|
439
|
+
|
|
422
440
|
// <ApiClient>
|
|
423
441
|
export class ApiClient {
|
|
424
442
|
baseUrl: string = "";
|
|
@@ -432,79 +450,89 @@ export class ApiClient {
|
|
|
432
450
|
return this;
|
|
433
451
|
}
|
|
434
452
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Replace path parameters in URL
|
|
455
|
+
* Supports both OpenAPI format {param} and Express format :param
|
|
456
|
+
*/
|
|
457
|
+
defaultDecodePathParams = (url: string, params: Record<string, string>): string => {
|
|
458
|
+
return url
|
|
459
|
+
.replace(/{(\\w+)}/g, (_, key: string) => params[key] || \`{\${key}}\`)
|
|
460
|
+
.replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || \`:\${key}\`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Uses URLSearchParams, skips null/undefined values */
|
|
464
|
+
defaultEncodeSearchParams = (queryParams: Record<string, unknown> | undefined): URLSearchParams | undefined => {
|
|
465
|
+
if (!queryParams) return;
|
|
466
|
+
|
|
467
|
+
const searchParams = new URLSearchParams();
|
|
468
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
469
|
+
if (value != null) {
|
|
470
|
+
// Skip null/undefined values
|
|
471
|
+
if (Array.isArray(value)) {
|
|
472
|
+
value.forEach((val) => val != null && searchParams.append(key, String(val)));
|
|
473
|
+
} else {
|
|
474
|
+
searchParams.append(key, String(value));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return searchParams;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
defaultParseResponseData = async (response: Response): Promise<unknown> => {
|
|
483
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
484
|
+
if (contentType.startsWith("text/")) {
|
|
485
|
+
return (await response.text())
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (contentType === "application/octet-stream") {
|
|
489
|
+
return (await response.arrayBuffer())
|
|
439
490
|
}
|
|
440
|
-
|
|
491
|
+
|
|
492
|
+
if (
|
|
493
|
+
contentType.includes("application/json") ||
|
|
494
|
+
(contentType.includes("application/") && contentType.includes("json")) ||
|
|
495
|
+
contentType === "*/*"
|
|
496
|
+
) {
|
|
497
|
+
try {
|
|
498
|
+
return await response.json();
|
|
499
|
+
} catch {
|
|
500
|
+
return undefined
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return
|
|
441
505
|
}
|
|
442
506
|
|
|
443
507
|
${Object.entries(byMethods)
|
|
444
508
|
.map(([method, endpointByMethod]) => {
|
|
445
509
|
const capitalizedMethod = capitalize(method);
|
|
446
|
-
const infer = inferByRuntime[ctx.runtime];
|
|
447
510
|
|
|
448
511
|
return endpointByMethod.length
|
|
449
512
|
? `// <ApiClient.${method}>
|
|
450
513
|
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
451
514
|
path: Path,
|
|
452
|
-
...params: MaybeOptionalArg
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
.with(
|
|
459
|
-
"arktype",
|
|
460
|
-
"io-ts",
|
|
461
|
-
"typebox",
|
|
462
|
-
"valibot",
|
|
463
|
-
() => `InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]`,
|
|
464
|
-
)
|
|
465
|
-
.otherwise(() => `Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]`)}>;
|
|
515
|
+
...params: MaybeOptionalArg<
|
|
516
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
517
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
518
|
+
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
|
|
519
|
+
>
|
|
520
|
+
): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>;
|
|
466
521
|
|
|
467
522
|
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
468
523
|
path: Path,
|
|
469
|
-
...params: MaybeOptionalArg
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
524
|
+
...params: MaybeOptionalArg<
|
|
525
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
526
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
|
527
|
+
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
|
|
528
|
+
>
|
|
473
529
|
): Promise<SafeApiResponse<TEndpoint>>;
|
|
474
530
|
|
|
475
|
-
${method}<Path extends keyof ${capitalizedMethod}Endpoints,
|
|
531
|
+
${method}<Path extends keyof ${capitalizedMethod}Endpoints, _TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
|
|
476
532
|
path: Path,
|
|
477
533
|
...params: MaybeOptionalArg<any>
|
|
478
534
|
): Promise<any> {
|
|
479
|
-
|
|
480
|
-
const withResponse = requestParams?.withResponse;
|
|
481
|
-
const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
|
|
482
|
-
|
|
483
|
-
const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
|
|
484
|
-
.then(async (response) => {
|
|
485
|
-
const data = await this.parseResponse(response);
|
|
486
|
-
const typedResponse = Object.assign(response, {
|
|
487
|
-
data: data,
|
|
488
|
-
json: () => Promise.resolve(data)
|
|
489
|
-
}) as SafeApiResponse<TEndpoint>;
|
|
490
|
-
|
|
491
|
-
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
492
|
-
throw new TypedResponseError(typedResponse as never);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return withResponse ? typedResponse : data;
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
return promise ${match(ctx.runtime)
|
|
499
|
-
.with("zod", "yup", () => `as Promise<${infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)}>`)
|
|
500
|
-
.with(
|
|
501
|
-
"arktype",
|
|
502
|
-
"io-ts",
|
|
503
|
-
"typebox",
|
|
504
|
-
"valibot",
|
|
505
|
-
() => `as Promise<InferResponseByStatus<${infer(`TEndpoint`)}, SuccessStatusCode>["data"]>`,
|
|
506
|
-
)
|
|
507
|
-
.otherwise(() => `as Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>`)}
|
|
535
|
+
return this.request("${method}", path, ...params);
|
|
508
536
|
}
|
|
509
537
|
// </ApiClient.${method}>
|
|
510
538
|
`
|
|
@@ -523,20 +551,74 @@ export class ApiClient {
|
|
|
523
551
|
>(
|
|
524
552
|
method: TMethod,
|
|
525
553
|
path: TPath,
|
|
526
|
-
...params: MaybeOptionalArg
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
554
|
+
...params: MaybeOptionalArg<
|
|
555
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
556
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
|
557
|
+
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean })
|
|
558
|
+
>
|
|
559
|
+
): Promise<Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]>
|
|
560
|
+
|
|
561
|
+
request<
|
|
562
|
+
TMethod extends keyof EndpointByMethod,
|
|
563
|
+
TPath extends keyof EndpointByMethod[TMethod],
|
|
564
|
+
TEndpoint extends EndpointByMethod[TMethod][TPath]
|
|
565
|
+
>(
|
|
566
|
+
method: TMethod,
|
|
567
|
+
path: TPath,
|
|
568
|
+
...params: MaybeOptionalArg<
|
|
569
|
+
(TEndpoint extends { parameters: infer UParams }
|
|
570
|
+
? NotNever<UParams> extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
|
571
|
+
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean })
|
|
572
|
+
>
|
|
573
|
+
): Promise<SafeApiResponse<TEndpoint>>;
|
|
574
|
+
|
|
575
|
+
request<
|
|
576
|
+
TMethod extends keyof EndpointByMethod,
|
|
577
|
+
TPath extends keyof EndpointByMethod[TMethod],
|
|
578
|
+
TEndpoint extends EndpointByMethod[TMethod][TPath]
|
|
579
|
+
>(
|
|
580
|
+
method: TMethod,
|
|
581
|
+
path: TPath,
|
|
582
|
+
...params: MaybeOptionalArg<any>
|
|
583
|
+
): Promise<any> {
|
|
584
|
+
const requestParams = params[0];
|
|
585
|
+
const withResponse = requestParams?.withResponse;
|
|
586
|
+
const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {};
|
|
587
|
+
|
|
588
|
+
const parametersToSend: EndpointParameters = {};
|
|
589
|
+
if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body;
|
|
590
|
+
if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query;
|
|
591
|
+
if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header;
|
|
592
|
+
if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path;
|
|
593
|
+
|
|
594
|
+
const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)(this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record<string, string>);
|
|
595
|
+
const url = new URL(resolvedPath);
|
|
596
|
+
const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query);
|
|
597
|
+
|
|
598
|
+
const promise = this.fetcher.fetch({
|
|
599
|
+
method: method,
|
|
600
|
+
path: (path as string),
|
|
601
|
+
url,
|
|
602
|
+
urlSearchParams,
|
|
603
|
+
parameters: Object.keys(fetchParams).length ? fetchParams : undefined,
|
|
604
|
+
overrides,
|
|
605
|
+
throwOnStatusError
|
|
606
|
+
})
|
|
607
|
+
.then(async (response) => {
|
|
608
|
+
const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response);
|
|
609
|
+
const typedResponse = Object.assign(response, {
|
|
610
|
+
data: data,
|
|
611
|
+
json: () => Promise.resolve(data)
|
|
612
|
+
}) as SafeApiResponse<TEndpoint>;
|
|
613
|
+
|
|
614
|
+
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
615
|
+
throw new TypedResponseError(typedResponse as never);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return withResponse ? typedResponse : data;
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
return promise as Extract<InferResponseByStatus<${InferTEndpoint}, SuccessStatusCode>, { data: {} }>["data"]
|
|
540
622
|
}
|
|
541
623
|
// </ApiClient.request>
|
|
542
624
|
}
|
|
@@ -6,7 +6,14 @@ import { createBoxFactory } from "./box-factory.ts";
|
|
|
6
6
|
import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
|
|
7
7
|
import { createRefResolver } from "./ref-resolver.ts";
|
|
8
8
|
import { tsFactory } from "./ts-factory.ts";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AnyBox,
|
|
11
|
+
BoxRef,
|
|
12
|
+
OpenapiSchemaConvertContext,
|
|
13
|
+
type BoxObject,
|
|
14
|
+
type BoxUnion,
|
|
15
|
+
type LibSchemaObject,
|
|
16
|
+
} from "./types.ts";
|
|
10
17
|
import { pathToVariableName } from "./string-utils.ts";
|
|
11
18
|
import { NameTransformOptions } from "./types.ts";
|
|
12
19
|
import { match, P } from "ts-pattern";
|
|
@@ -134,7 +141,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
|
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
const allResponses: Record<string, AnyBox> = {};
|
|
137
|
-
const
|
|
144
|
+
const allResponseHeaders: Record<string, Box<BoxObject>> = {};
|
|
138
145
|
|
|
139
146
|
Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
|
|
140
147
|
const responseObj = refs.unwrap<ResponseObject>(responseOrRef);
|
|
@@ -146,14 +153,26 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
|
|
|
146
153
|
mediaTypes.forEach((mediaType) => {
|
|
147
154
|
// If no JSON content, use unknown type
|
|
148
155
|
const schema = content[mediaType] ? (content[mediaType].schema ?? {}) : {};
|
|
149
|
-
const t = createBoxFactory(schema as LibSchemaObject, ctx);
|
|
150
156
|
const mediaTypeResponse = openApiSchemaToTs({ schema, ctx });
|
|
151
157
|
|
|
152
158
|
if (allResponses[status]) {
|
|
159
|
+
const t = createBoxFactory(
|
|
160
|
+
{
|
|
161
|
+
oneOf: [
|
|
162
|
+
...((allResponses[status].schema as LibSchemaObject).oneOf
|
|
163
|
+
? (allResponses[status].schema as LibSchemaObject).oneOf!
|
|
164
|
+
: [allResponses[status].schema]),
|
|
165
|
+
schema,
|
|
166
|
+
],
|
|
167
|
+
} as LibSchemaObject,
|
|
168
|
+
ctx,
|
|
169
|
+
);
|
|
153
170
|
allResponses[status] = t.union([
|
|
154
|
-
...(
|
|
171
|
+
...(allResponses[status].type === "union"
|
|
172
|
+
? (allResponses[status] as Box<BoxUnion>).params.types
|
|
173
|
+
: [allResponses[status]]),
|
|
155
174
|
mediaTypeResponse,
|
|
156
|
-
]);
|
|
175
|
+
] as Box[]);
|
|
157
176
|
} else {
|
|
158
177
|
allResponses[status] = mediaTypeResponse;
|
|
159
178
|
}
|
|
@@ -162,13 +181,25 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
|
|
|
162
181
|
// If no content defined, use unknown type
|
|
163
182
|
const schema = {};
|
|
164
183
|
const unknown = openApiSchemaToTs({ schema: {}, ctx });
|
|
165
|
-
const t = createBoxFactory(schema as LibSchemaObject, ctx);
|
|
166
184
|
|
|
167
185
|
if (allResponses[status]) {
|
|
186
|
+
const t = createBoxFactory(
|
|
187
|
+
{
|
|
188
|
+
oneOf: [
|
|
189
|
+
...((allResponses[status].schema as LibSchemaObject).oneOf
|
|
190
|
+
? (allResponses[status].schema as LibSchemaObject).oneOf!
|
|
191
|
+
: [allResponses[status].schema]),
|
|
192
|
+
schema,
|
|
193
|
+
],
|
|
194
|
+
} as LibSchemaObject,
|
|
195
|
+
ctx,
|
|
196
|
+
);
|
|
168
197
|
allResponses[status] = t.union([
|
|
169
|
-
...(
|
|
198
|
+
...(allResponses[status].type === "union"
|
|
199
|
+
? (allResponses[status] as Box<BoxUnion>).params.types
|
|
200
|
+
: [allResponses[status]]),
|
|
170
201
|
unknown,
|
|
171
|
-
]);
|
|
202
|
+
] as Box[]);
|
|
172
203
|
} else {
|
|
173
204
|
allResponses[status] = unknown;
|
|
174
205
|
}
|
|
@@ -193,7 +224,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
|
|
|
193
224
|
);
|
|
194
225
|
|
|
195
226
|
if (Object.keys(mappedHeaders).length) {
|
|
196
|
-
|
|
227
|
+
allResponseHeaders[status] = t.object(mappedHeaders);
|
|
197
228
|
}
|
|
198
229
|
}
|
|
199
230
|
});
|
|
@@ -203,8 +234,8 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransfor
|
|
|
203
234
|
endpoint.responses = allResponses;
|
|
204
235
|
}
|
|
205
236
|
|
|
206
|
-
if (Object.keys(
|
|
207
|
-
endpoint.responseHeaders =
|
|
237
|
+
if (Object.keys(allResponseHeaders).length) {
|
|
238
|
+
endpoint.responseHeaders = allResponseHeaders;
|
|
208
239
|
}
|
|
209
240
|
|
|
210
241
|
endpointList.push(endpoint);
|
|
@@ -76,7 +76,10 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
76
76
|
.map(
|
|
77
77
|
(method) => `
|
|
78
78
|
// <ApiClient.${method}>
|
|
79
|
-
${method}<
|
|
79
|
+
${method}<
|
|
80
|
+
Path extends keyof ${capitalize(method)}Endpoints,
|
|
81
|
+
TEndpoint extends ${capitalize(method)}Endpoints[Path]
|
|
82
|
+
>(
|
|
80
83
|
path: Path,
|
|
81
84
|
...params: MaybeOptionalArg<TEndpoint["parameters"]>
|
|
82
85
|
) {
|
|
@@ -91,28 +94,14 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
91
94
|
const requestParams = {
|
|
92
95
|
...(params[0] || {}),
|
|
93
96
|
...(queryKey[0] || {}),
|
|
94
|
-
signal,
|
|
97
|
+
overrides: { signal },
|
|
95
98
|
withResponse: false as const
|
|
96
99
|
};
|
|
97
|
-
const res = await this.client.${method}(path, requestParams);
|
|
100
|
+
const res = await this.client.${method}(path, requestParams as never);
|
|
98
101
|
return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
|
|
99
102
|
},
|
|
100
103
|
queryKey: queryKey
|
|
101
104
|
}),
|
|
102
|
-
mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
|
|
103
|
-
mutationOptions: {
|
|
104
|
-
mutationKey: queryKey,
|
|
105
|
-
mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
|
|
106
|
-
const requestParams = {
|
|
107
|
-
...(params[0] || {}),
|
|
108
|
-
...(queryKey[0] || {}),
|
|
109
|
-
...(localOptions || {}),
|
|
110
|
-
withResponse: false as const
|
|
111
|
-
};
|
|
112
|
-
const res = await this.client.${method}(path, requestParams);
|
|
113
|
-
return res as Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"];
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
105
|
};
|
|
117
106
|
|
|
118
107
|
return query
|
|
@@ -147,40 +136,54 @@ export const generateTanstackQueryFile = async (ctx: GeneratorContext & { relati
|
|
|
147
136
|
: Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]
|
|
148
137
|
) => TSelection;
|
|
149
138
|
throwOnStatusError?: boolean
|
|
139
|
+
throwOnError?: boolean | ((error: TError) => boolean)
|
|
150
140
|
}) {
|
|
151
141
|
const mutationKey = [{ method, path }] as const;
|
|
142
|
+
const mutationFn = async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
|
|
143
|
+
? InferResponseByStatus<TEndpoint, SuccessStatusCode>
|
|
144
|
+
: Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>
|
|
145
|
+
(params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
|
|
146
|
+
withResponse?: TLocalWithResponse;
|
|
147
|
+
throwOnStatusError?: boolean;
|
|
148
|
+
overrides?: RequestInit;
|
|
149
|
+
}): Promise<TLocalSelection> => {
|
|
150
|
+
const withResponse = params.withResponse ??options?.withResponse ?? false;
|
|
151
|
+
const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
|
|
152
|
+
const selectFn = options?.selectFn;
|
|
153
|
+
const response = await (this.client as any)[method](path, {
|
|
154
|
+
...params as any,
|
|
155
|
+
withResponse: true,
|
|
156
|
+
throwOnStatusError: false,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
160
|
+
throw new TypedResponseError(response as never);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Return just the data if withResponse is false, otherwise return the full response
|
|
164
|
+
const finalResponse = withResponse ? response : response.data;
|
|
165
|
+
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
|
|
166
|
+
return res as never;
|
|
167
|
+
};
|
|
152
168
|
return {
|
|
153
169
|
/** type-only property if you need easy access to the endpoint params */
|
|
154
170
|
"~endpoint": {} as TEndpoint,
|
|
155
171
|
mutationKey: mutationKey,
|
|
156
172
|
mutationFn: {} as "You need to pass .mutationOptions to the useMutation hook",
|
|
157
173
|
mutationOptions: {
|
|
174
|
+
throwOnError: options?.throwOnError as boolean | ((error: TError) => boolean),
|
|
158
175
|
mutationKey: mutationKey,
|
|
159
|
-
mutationFn:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const withResponse = params.withResponse ??options?.withResponse ?? false;
|
|
167
|
-
const throwOnStatusError = params.throwOnStatusError ?? options?.throwOnStatusError ?? (withResponse ? false : true);
|
|
168
|
-
const selectFn = options?.selectFn;
|
|
169
|
-
const response = await (this.client as any)[method](path, { ...params as any, withResponse: true, throwOnStatusError: false });
|
|
170
|
-
|
|
171
|
-
if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
|
|
172
|
-
throw new TypedResponseError(response as never);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Return just the data if withResponse is false, otherwise return the full response
|
|
176
|
-
const finalResponse = withResponse ? response : response.data;
|
|
177
|
-
const res = selectFn ? selectFn(finalResponse as any) : finalResponse;
|
|
178
|
-
return res as never;
|
|
176
|
+
mutationFn: mutationFn,
|
|
177
|
+
} as Omit<import("@tanstack/react-query").UseMutationOptions<
|
|
178
|
+
TSelection,
|
|
179
|
+
TError,
|
|
180
|
+
(TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
|
|
181
|
+
withResponse?: boolean;
|
|
182
|
+
throwOnStatusError?: boolean;
|
|
179
183
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}>,
|
|
184
|
+
>, "mutationFn"> & {
|
|
185
|
+
mutationFn: typeof mutationFn
|
|
186
|
+
},
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
189
|
// </ApiClient.request>
|