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.
@@ -5,7 +5,7 @@ import {
5
5
  generateFile,
6
6
  generateTanstackQueryFile,
7
7
  mapOpenApiEndpoints
8
- } from "./chunk-X4A4YQN7.js";
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 './generated/api' with your actual generated file path
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
- // @ts-ignore
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
- export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
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 (params?.query) {
61
- const searchParams = new URLSearchParams();
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(params?.body)
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 (params?.header) {
86
- Object.entries(params.header).forEach(([key, value]) => {
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 type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
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
- parseResponse = async <T>(response: Response): Promise<T> => {
680
- const contentType = response.headers.get('content-type');
681
- if (contentType?.includes('application/json')) {
682
- return response.json();
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
- return response.text() as unknown as T;
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<${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 }>
694
- ): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`)).with(
695
- "arktype",
696
- "io-ts",
697
- "typebox",
698
- "valibot",
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<${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 }>
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, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
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
- const requestParams = params[0];
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<${match(ctx.runtime).with(
754
- "zod",
755
- "yup",
756
- () => inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`)
757
- ).with(
758
- "arktype",
759
- "io-ts",
760
- "typebox",
761
- "valibot",
762
- () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`
763
- ).otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
764
- : Promise<SafeApiResponse<TEndpoint>> {
765
- return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
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 allHeaders = {};
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
- ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
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
- ...Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]],
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
- allHeaders[status] = t.object(mappedHeaders);
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(allHeaders).length) {
1230
- endpoint.responseHeaders = allHeaders;
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}<Path extends keyof ${capitalize4(method)}Endpoints, TEndpoint extends ${capitalize4(method)}Endpoints[Path]>(
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: async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
1397
- ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
1398
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>
1399
- (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
1400
- withResponse?: TLocalWithResponse;
1401
- throwOnStatusError?: boolean;
1402
- }): Promise<TLocalSelection> => {
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
- } satisfies import("@tanstack/react-query").UseMutationOptions<TSelection, TError, (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
1418
- withResponse?: boolean;
1419
- throwOnStatusError?: boolean;
1420
- }>,
1532
+ >, "mutationFn"> & {
1533
+ mutationFn: typeof mutationFn
1534
+ },
1421
1535
  }
1422
1536
  }
1423
1537
  // </ApiClient.request>
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-UNKLDND3.js";
3
+ } from "./chunk-IB6GEWS7.js";
4
4
  import {
5
5
  allowedRuntimes
6
- } from "./chunk-X4A4YQN7.js";
6
+ } from "./chunk-QGMS7IBQ.js";
7
7
  import "./chunk-KAEXXJ7X.js";
8
8
 
9
9
  // src/cli.ts
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  openApiSchemaToTs,
9
9
  tsFactory,
10
10
  unwrap
11
- } from "./chunk-X4A4YQN7.js";
11
+ } from "./chunk-QGMS7IBQ.js";
12
12
  import "./chunk-KAEXXJ7X.js";
13
13
  export {
14
14
  createBoxFactory,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateClientFiles
3
- } from "./chunk-UNKLDND3.js";
4
- import "./chunk-X4A4YQN7.js";
3
+ } from "./chunk-IB6GEWS7.js";
4
+ import "./chunk-QGMS7IBQ.js";
5
5
  import "./chunk-KAEXXJ7X.js";
6
6
  export {
7
7
  generateClientFiles
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-openapi",
3
3
  "type": "module",
4
- "version": "2.1.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 './generated/api' with your actual generated file path
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
- // @ts-ignore
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
- export const ${fetcherName}: Fetcher = async (method, apiUrl, params) => {
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 (params?.query) {
48
- const searchParams = new URLSearchParams();
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(params?.body)
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 (params?.header) {
73
- Object.entries(params.header).forEach(([key, value]) => {
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 type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
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
- parseResponse = async <T>(response: Response): Promise<T> => {
436
- const contentType = response.headers.get('content-type');
437
- if (contentType?.includes('application/json')) {
438
- return response.json();
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
- return response.text() as unknown as T;
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<${match(ctx.runtime)
453
- .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
454
- .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
455
- .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
456
- ): Promise<${match(ctx.runtime)
457
- .with("zod", "yup", () => infer(`InferResponseByStatus<TEndpoint, SuccessStatusCode>`))
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<${match(ctx.runtime)
470
- .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
471
- .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
472
- .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse: true; throwOnStatusError?: boolean }>
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, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
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
- const requestParams = params[0];
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<${match(ctx.runtime)
527
- .with("zod", "yup", () =>
528
- inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`),
529
- )
530
- .with(
531
- "arktype",
532
- "io-ts",
533
- "typebox",
534
- "valibot",
535
- () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`,
536
- )
537
- .otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
538
- : Promise<SafeApiResponse<TEndpoint>> {
539
- return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
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 { AnyBox, BoxRef, OpenapiSchemaConvertContext, type BoxObject, type LibSchemaObject } from "./types.ts";
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 allHeaders: Record<string, Box<BoxObject>> = {};
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
- ...(Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]]),
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
- ...(Array.isArray(allResponses[status]) ? allResponses[status] : [allResponses[status]]),
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
- allHeaders[status] = t.object(mappedHeaders);
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(allHeaders).length) {
207
- endpoint.responseHeaders = allHeaders;
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}<Path extends keyof ${capitalize(method)}Endpoints, TEndpoint extends ${capitalize(method)}Endpoints[Path]>(
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: async <TLocalWithResponse extends boolean = TWithResponse, TLocalSelection = TLocalWithResponse extends true
160
- ? InferResponseByStatus<TEndpoint, SuccessStatusCode>
161
- : Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>
162
- (params: (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
163
- withResponse?: TLocalWithResponse;
164
- throwOnStatusError?: boolean;
165
- }): Promise<TLocalSelection> => {
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
- } satisfies import("@tanstack/react-query").UseMutationOptions<TSelection, TError, (TEndpoint extends { parameters: infer Parameters } ? Parameters : {}) & {
181
- withResponse?: boolean;
182
- throwOnStatusError?: boolean;
183
- }>,
184
+ >, "mutationFn"> & {
185
+ mutationFn: typeof mutationFn
186
+ },
184
187
  }
185
188
  }
186
189
  // </ApiClient.request>