@rexeus/typeweaver-clients 0.4.2 → 0.5.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/README.md +6 -4
- package/dist/lib/ApiClient.ts +262 -116
- package/dist/lib/NetworkError.ts +39 -0
- package/dist/lib/PathParameterError.ts +26 -0
- package/dist/lib/RequestCommand.ts +1 -2
- package/dist/lib/ResponseParseError.ts +26 -0
- package/dist/lib/index.ts +3 -0
- package/package.json +6 -8
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ npm install @rexeus/typeweaver-core
|
|
|
31
31
|
npx typeweaver generate --input ./api/definitions --output ./api/generated --plugins clients
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
More on the CLI in
|
|
34
|
+
More on the CLI in
|
|
35
|
+
[@rexeus/typeweaver](https://github.com/rexeus/typeweaver/tree/main/packages/cli/README.md#️-cli).
|
|
35
36
|
|
|
36
37
|
## 📂 Generated Output
|
|
37
38
|
|
|
@@ -48,7 +49,8 @@ Resource-specific HTTP clients are generated as `<ResourceName>Client.ts` files,
|
|
|
48
49
|
|
|
49
50
|
- **Type-safe HTTP methods** - Method overloads for each operation ensuring compile-time type
|
|
50
51
|
checking
|
|
51
|
-
- **
|
|
52
|
+
- **fetch based** - Zero dependencies, uses the native fetch API. Supports custom fetch functions
|
|
53
|
+
for middleware and testing
|
|
52
54
|
- **Response type mapping** - Each response is automatically mapped to the associated response class
|
|
53
55
|
and an instance of the class is returned. This ensures that all responses are in the defined
|
|
54
56
|
format and it is type-safe.
|
|
@@ -66,8 +68,8 @@ Resource-specific HTTP clients are generated as `<ResourceName>Client.ts` files,
|
|
|
66
68
|
import { TodoClient } from "path/to/generated/output";
|
|
67
69
|
|
|
68
70
|
const client = new TodoClient({
|
|
69
|
-
|
|
70
|
-
baseUrl: "https://api.example.com", // Base URL for all requests
|
|
71
|
+
fetchFn: customFetch, // Custom fetch function (optional, defaults to globalThis.fetch)
|
|
72
|
+
baseUrl: "https://api.example.com", // Base URL for all requests (required)
|
|
71
73
|
unknownResponseHandling: "throw", // "throw" | "passthrough" for unknown responses
|
|
72
74
|
// -> In "passthrough" mode, the received status code determines if the response is thrown
|
|
73
75
|
isSuccessStatusCode: code => code < 400, // Custom success status code predicate, determines whether the response is successful or should be thrown
|
package/dist/lib/ApiClient.ts
CHANGED
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
* @generated by @rexeus/typeweaver
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import axios, { AxiosError } from "axios";
|
|
9
8
|
import type {
|
|
10
9
|
IHttpHeader,
|
|
11
10
|
IHttpParam,
|
|
12
11
|
IHttpQuery,
|
|
13
12
|
IHttpResponse,
|
|
14
13
|
} from "@rexeus/typeweaver-core";
|
|
14
|
+
import { NetworkError } from "./NetworkError";
|
|
15
|
+
import { PathParameterError } from "./PathParameterError";
|
|
15
16
|
import { RequestCommand } from "./RequestCommand";
|
|
17
|
+
import { ResponseParseError } from "./ResponseParseError";
|
|
18
|
+
import type { NetworkErrorCode } from "./NetworkError";
|
|
16
19
|
import type { ProcessResponseOptions } from "./RequestCommand";
|
|
17
|
-
import type { AxiosHeaderValue, AxiosInstance, AxiosResponse } from "axios";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Configuration options for handling unknown responses.
|
|
@@ -25,14 +27,25 @@ export type UnknownResponseHandling = "throw" | "passthrough";
|
|
|
25
27
|
* Configuration options for ApiClient initialization.
|
|
26
28
|
*/
|
|
27
29
|
export type ApiClientProps = {
|
|
28
|
-
/** Custom
|
|
29
|
-
|
|
30
|
-
/** Base URL for API requests
|
|
31
|
-
baseUrl
|
|
30
|
+
/** Custom fetch function for HTTP requests. Defaults to globalThis.fetch */
|
|
31
|
+
readonly fetchFn?: typeof globalThis.fetch;
|
|
32
|
+
/** Base URL for API requests */
|
|
33
|
+
readonly baseUrl: string;
|
|
32
34
|
/** How to handle unknown responses. Defaults to "throw" */
|
|
33
|
-
unknownResponseHandling?: UnknownResponseHandling;
|
|
35
|
+
readonly unknownResponseHandling?: UnknownResponseHandling;
|
|
34
36
|
/** Predicate to determine if a status code represents success. Defaults to 2xx status codes */
|
|
35
|
-
isSuccessStatusCode?: (statusCode: number) => boolean;
|
|
37
|
+
readonly isSuccessStatusCode?: (statusCode: number) => boolean;
|
|
38
|
+
/** Request timeout in milliseconds. When set, requests will be aborted after this duration */
|
|
39
|
+
readonly timeoutMs?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const NETWORK_ERROR_MESSAGES: Readonly<
|
|
43
|
+
Partial<Record<NetworkErrorCode, string>>
|
|
44
|
+
> = {
|
|
45
|
+
ECONNREFUSED: "Connection refused",
|
|
46
|
+
ECONNRESET: "Connection reset by peer",
|
|
47
|
+
ENOTFOUND: "DNS lookup failed",
|
|
48
|
+
ETIMEDOUT: "Connection timed out",
|
|
36
49
|
};
|
|
37
50
|
|
|
38
51
|
/**
|
|
@@ -41,47 +54,38 @@ export type ApiClientProps = {
|
|
|
41
54
|
* This class provides HTTP request execution with:
|
|
42
55
|
* - Automatic path parameter substitution (`:param` style)
|
|
43
56
|
* - Query string building with array support
|
|
44
|
-
* - Header normalization (converts to Header-Case)
|
|
45
57
|
* - Network error handling with specific error messages
|
|
46
58
|
* - Integration with RequestCommand pattern for type safety
|
|
47
59
|
*/
|
|
48
60
|
export abstract class ApiClient {
|
|
49
|
-
|
|
50
|
-
public readonly axiosInstance: AxiosInstance;
|
|
51
|
-
/** The base URL for all API requests */
|
|
61
|
+
private readonly fetchFn: typeof globalThis.fetch;
|
|
52
62
|
public readonly baseUrl: string;
|
|
53
|
-
/** How to handle unknown responses */
|
|
54
63
|
public readonly unknownResponseHandling: UnknownResponseHandling;
|
|
55
|
-
/** Predicate to determine if a status code represents success */
|
|
56
64
|
public readonly isSuccessStatusCode: (statusCode: number) => boolean;
|
|
65
|
+
private readonly timeoutMs: number | undefined;
|
|
57
66
|
|
|
58
|
-
/**
|
|
59
|
-
* Creates a new ApiClient instance.
|
|
60
|
-
*
|
|
61
|
-
* @param props - Configuration options
|
|
62
|
-
* @throws {Error} If no base URL is provided in props or axios instance
|
|
63
|
-
*/
|
|
64
67
|
protected constructor(props: ApiClientProps) {
|
|
65
|
-
this.
|
|
68
|
+
this.fetchFn = props.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
69
|
+
this.baseUrl = props.baseUrl;
|
|
66
70
|
|
|
67
|
-
this.baseUrl = props.baseUrl ?? this.axiosInstance.defaults.baseURL ?? "";
|
|
68
71
|
if (!this.baseUrl) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
"Base URL must be provided either in axios instance or in constructor"
|
|
71
|
-
);
|
|
72
|
+
throw new Error("Base URL must be provided");
|
|
72
73
|
}
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
props.timeoutMs !== undefined &&
|
|
77
|
+
(props.timeoutMs <= 0 || !Number.isFinite(props.timeoutMs))
|
|
78
|
+
) {
|
|
79
|
+
throw new Error("timeoutMs must be a positive finite number");
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
this.unknownResponseHandling = props.unknownResponseHandling ?? "throw";
|
|
74
83
|
this.isSuccessStatusCode =
|
|
75
84
|
props.isSuccessStatusCode ??
|
|
76
85
|
((statusCode: number) => statusCode >= 200 && statusCode < 300);
|
|
86
|
+
this.timeoutMs = props.timeoutMs;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
/**
|
|
80
|
-
* Gets the process response options for this client instance.
|
|
81
|
-
*
|
|
82
|
-
* @returns The configuration options for processing responses
|
|
83
|
-
* @protected
|
|
84
|
-
*/
|
|
85
89
|
protected get processResponseOptions(): ProcessResponseOptions {
|
|
86
90
|
return {
|
|
87
91
|
unknownResponseHandling: this.unknownResponseHandling,
|
|
@@ -89,80 +93,247 @@ export abstract class ApiClient {
|
|
|
89
93
|
};
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
/**
|
|
93
|
-
* Executes an HTTP request using the provided command.
|
|
94
|
-
*
|
|
95
|
-
* This method:
|
|
96
|
-
* 1. Substitutes path parameters (e.g., `:id` with actual values)
|
|
97
|
-
* 2. Builds the complete URL with query parameters
|
|
98
|
-
* 3. Sends the HTTP request via Axios
|
|
99
|
-
* 4. Normalizes the response format
|
|
100
|
-
* 5. Handles network errors with specific error messages
|
|
101
|
-
*
|
|
102
|
-
* @param request - The request command containing all HTTP parameters
|
|
103
|
-
* @returns Promise resolving to the HTTP response
|
|
104
|
-
* @throws {Error} Network errors with specific messages (connection refused, timeout, etc.)
|
|
105
|
-
* @protected
|
|
106
|
-
*/
|
|
107
96
|
protected async execute(request: RequestCommand): Promise<IHttpResponse> {
|
|
108
97
|
const { method, path, header, query, param, body } = request;
|
|
109
98
|
|
|
110
|
-
const headers = this.createHeader(header);
|
|
111
99
|
const pathWithParam = this.createPath(path, param);
|
|
112
|
-
const
|
|
100
|
+
const relativeUrl = this.createUrl(pathWithParam, query);
|
|
101
|
+
const fullUrl = this.buildFullUrl(relativeUrl);
|
|
113
102
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return this.createResponse(response);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
if (error instanceof AxiosError) {
|
|
126
|
-
if (error.response) {
|
|
127
|
-
return this.createResponse(error.response);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// TODO: improve network error handling
|
|
131
|
-
if (error.code === "ECONNREFUSED") {
|
|
132
|
-
throw new Error("Network error: Connection refused");
|
|
133
|
-
}
|
|
134
|
-
if (error.code === "ECONNRESET") {
|
|
135
|
-
throw new Error("Network error: Connection reset by peer");
|
|
136
|
-
}
|
|
137
|
-
if (error.code === "ENOTFOUND") {
|
|
138
|
-
throw new Error("Network error: DNS lookup failed");
|
|
139
|
-
}
|
|
140
|
-
if (error.code === "ETIMEDOUT") {
|
|
141
|
-
throw new Error("Network error: Connection timed out");
|
|
142
|
-
}
|
|
103
|
+
const response = await this.performFetch(method, fullUrl, {
|
|
104
|
+
method,
|
|
105
|
+
headers: this.flattenHeaders(header),
|
|
106
|
+
body: this.serializeBody(body),
|
|
107
|
+
signal:
|
|
108
|
+
this.timeoutMs !== undefined
|
|
109
|
+
? AbortSignal.timeout(this.timeoutMs)
|
|
110
|
+
: undefined,
|
|
111
|
+
});
|
|
143
112
|
|
|
144
|
-
|
|
145
|
-
|
|
113
|
+
return await this.createResponse(response, method, fullUrl);
|
|
114
|
+
}
|
|
146
115
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
116
|
+
private async performFetch(
|
|
117
|
+
method: string,
|
|
118
|
+
url: string,
|
|
119
|
+
init: RequestInit
|
|
120
|
+
): Promise<Response> {
|
|
121
|
+
try {
|
|
122
|
+
return await this.fetchFn(url, init);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw this.createNetworkError(error, method, url);
|
|
150
125
|
}
|
|
151
126
|
}
|
|
152
127
|
|
|
153
|
-
private createResponse(
|
|
128
|
+
private async createResponse(
|
|
129
|
+
response: Response,
|
|
130
|
+
method: string,
|
|
131
|
+
url: string
|
|
132
|
+
): Promise<IHttpResponse> {
|
|
154
133
|
const header: IHttpHeader = {};
|
|
155
|
-
|
|
156
|
-
|
|
134
|
+
response.headers.forEach((value, key) => {
|
|
135
|
+
header[key] = value;
|
|
157
136
|
});
|
|
158
137
|
|
|
138
|
+
if (typeof response.headers.getSetCookie === "function") {
|
|
139
|
+
const cookies = response.headers.getSetCookie();
|
|
140
|
+
if (cookies.length > 1) {
|
|
141
|
+
header["set-cookie"] = cookies;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const body = await this.parseResponseBody(response, method, url);
|
|
146
|
+
|
|
159
147
|
return {
|
|
160
|
-
body
|
|
148
|
+
body,
|
|
161
149
|
header,
|
|
162
150
|
statusCode: response.status,
|
|
163
151
|
};
|
|
164
152
|
}
|
|
165
153
|
|
|
154
|
+
private async readBody<T>(
|
|
155
|
+
read: () => Promise<T>,
|
|
156
|
+
response: Response,
|
|
157
|
+
method: string,
|
|
158
|
+
url: string
|
|
159
|
+
): Promise<T> {
|
|
160
|
+
try {
|
|
161
|
+
return await read();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new ResponseParseError(
|
|
164
|
+
`Failed to read response body (${method} ${url})`,
|
|
165
|
+
response.status,
|
|
166
|
+
"",
|
|
167
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async parseResponseBody(
|
|
173
|
+
response: Response,
|
|
174
|
+
method: string,
|
|
175
|
+
url: string
|
|
176
|
+
): Promise<unknown> {
|
|
177
|
+
if (response.status === 204 || response.status === 304) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const contentType = response.headers.get("content-type");
|
|
182
|
+
|
|
183
|
+
if (this.isJsonContentType(contentType)) {
|
|
184
|
+
const text = await this.readBody(
|
|
185
|
+
() => response.text(),
|
|
186
|
+
response,
|
|
187
|
+
method,
|
|
188
|
+
url
|
|
189
|
+
);
|
|
190
|
+
if (!text) return undefined;
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(text);
|
|
193
|
+
} catch (parseError) {
|
|
194
|
+
throw new ResponseParseError(
|
|
195
|
+
"Failed to parse JSON response",
|
|
196
|
+
response.status,
|
|
197
|
+
text.slice(0, 200),
|
|
198
|
+
{
|
|
199
|
+
cause: parseError instanceof Error ? parseError : undefined,
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (this.isTextContentType(contentType) || !contentType) {
|
|
206
|
+
const text = await this.readBody(
|
|
207
|
+
() => response.text(),
|
|
208
|
+
response,
|
|
209
|
+
method,
|
|
210
|
+
url
|
|
211
|
+
);
|
|
212
|
+
if (!text) return undefined;
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return await this.readBody(
|
|
217
|
+
() => response.arrayBuffer(),
|
|
218
|
+
response,
|
|
219
|
+
method,
|
|
220
|
+
url
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private isTextContentType(contentType: string | null): boolean {
|
|
225
|
+
if (!contentType) return false;
|
|
226
|
+
return contentType.includes("text/");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private createNetworkError(
|
|
230
|
+
error: unknown,
|
|
231
|
+
method: string,
|
|
232
|
+
url: string
|
|
233
|
+
): NetworkError {
|
|
234
|
+
const context = `(${method} ${url})`;
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
(error instanceof DOMException || error instanceof Error) &&
|
|
238
|
+
error.name === "TimeoutError"
|
|
239
|
+
) {
|
|
240
|
+
return new NetworkError(
|
|
241
|
+
`Network error: Request timed out ${context}`,
|
|
242
|
+
"TIMEOUT",
|
|
243
|
+
method,
|
|
244
|
+
url,
|
|
245
|
+
{ cause: error }
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (
|
|
250
|
+
(error instanceof DOMException || error instanceof Error) &&
|
|
251
|
+
error.name === "AbortError"
|
|
252
|
+
) {
|
|
253
|
+
return new NetworkError(
|
|
254
|
+
`Network error: Request aborted ${context}`,
|
|
255
|
+
"ABORT",
|
|
256
|
+
method,
|
|
257
|
+
url,
|
|
258
|
+
{ cause: error }
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (error instanceof TypeError) {
|
|
263
|
+
const cause = (error as TypeError & { cause?: { code?: string } }).cause;
|
|
264
|
+
const code = cause?.code;
|
|
265
|
+
|
|
266
|
+
if (code && code in NETWORK_ERROR_MESSAGES) {
|
|
267
|
+
const message = NETWORK_ERROR_MESSAGES[code as NetworkErrorCode];
|
|
268
|
+
return new NetworkError(
|
|
269
|
+
`Network error: ${message} ${context}`,
|
|
270
|
+
code as NetworkErrorCode,
|
|
271
|
+
method,
|
|
272
|
+
url,
|
|
273
|
+
{ cause: error }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return new NetworkError(
|
|
279
|
+
`Network error: ${error instanceof Error ? error.message : String(error)} ${context}`,
|
|
280
|
+
"UNKNOWN",
|
|
281
|
+
method,
|
|
282
|
+
url,
|
|
283
|
+
{ cause: error }
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private flattenHeaders(
|
|
288
|
+
header: IHttpHeader
|
|
289
|
+
): Record<string, string> | undefined {
|
|
290
|
+
if (header === undefined) return undefined;
|
|
291
|
+
|
|
292
|
+
const flattened: Record<string, string> = {};
|
|
293
|
+
for (const [key, value] of Object.entries(header)) {
|
|
294
|
+
flattened[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
295
|
+
}
|
|
296
|
+
return flattened;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private serializeBody(
|
|
300
|
+
body: unknown
|
|
301
|
+
): NonNullable<RequestInit["body"]> | undefined {
|
|
302
|
+
if (body === null || body === undefined) return undefined;
|
|
303
|
+
if (typeof body === "string") return body;
|
|
304
|
+
if (this.isNativeBody(body))
|
|
305
|
+
return body as NonNullable<RequestInit["body"]>;
|
|
306
|
+
return JSON.stringify(body);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private isNativeBody(body: unknown): boolean {
|
|
310
|
+
return (
|
|
311
|
+
body instanceof Blob ||
|
|
312
|
+
body instanceof ArrayBuffer ||
|
|
313
|
+
body instanceof FormData ||
|
|
314
|
+
body instanceof URLSearchParams ||
|
|
315
|
+
body instanceof ReadableStream ||
|
|
316
|
+
ArrayBuffer.isView(body)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private isJsonContentType(contentType: string | null): boolean {
|
|
321
|
+
if (!contentType) return false;
|
|
322
|
+
return (
|
|
323
|
+
contentType.includes("application/json") || contentType.includes("+json")
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private buildFullUrl(relativePath: string): string {
|
|
328
|
+
const base = this.baseUrl.endsWith("/")
|
|
329
|
+
? this.baseUrl.slice(0, -1)
|
|
330
|
+
: this.baseUrl;
|
|
331
|
+
const path = relativePath.startsWith("/")
|
|
332
|
+
? relativePath
|
|
333
|
+
: `/${relativePath}`;
|
|
334
|
+
return `${base}${path}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
166
337
|
private createPath(path: string, param?: IHttpParam): string {
|
|
167
338
|
if (!param) {
|
|
168
339
|
return path;
|
|
@@ -172,8 +343,10 @@ export abstract class ApiClient {
|
|
|
172
343
|
const result = acc.replace(`:${key}`, encodeURIComponent(value));
|
|
173
344
|
|
|
174
345
|
if (result === acc) {
|
|
175
|
-
throw new
|
|
176
|
-
`Path parameter '${key}' is not found in path '${path}'
|
|
346
|
+
throw new PathParameterError(
|
|
347
|
+
`Path parameter '${key}' is not found in path '${path}'`,
|
|
348
|
+
key,
|
|
349
|
+
path
|
|
177
350
|
);
|
|
178
351
|
}
|
|
179
352
|
|
|
@@ -184,9 +357,7 @@ export abstract class ApiClient {
|
|
|
184
357
|
private createUrl(path: string, query?: IHttpQuery): string {
|
|
185
358
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
186
359
|
const queryString = this.buildQueryString(query);
|
|
187
|
-
return queryString
|
|
188
|
-
? `${normalizedPath}?${queryString}`
|
|
189
|
-
: normalizedPath;
|
|
360
|
+
return queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
|
|
190
361
|
}
|
|
191
362
|
|
|
192
363
|
private buildQueryString(query?: IHttpQuery): string {
|
|
@@ -211,29 +382,4 @@ export abstract class ApiClient {
|
|
|
211
382
|
}
|
|
212
383
|
return params.toString();
|
|
213
384
|
}
|
|
214
|
-
|
|
215
|
-
private createHeader(header: any): any {
|
|
216
|
-
return header;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private addMultiValue(
|
|
220
|
-
record: Record<string, string | string[]>,
|
|
221
|
-
key: string,
|
|
222
|
-
value: AxiosHeaderValue
|
|
223
|
-
): void {
|
|
224
|
-
const existing = record[key];
|
|
225
|
-
const preparedValue = Array.isArray(value)
|
|
226
|
-
? value.map(String)
|
|
227
|
-
: [String(value)];
|
|
228
|
-
if (existing) {
|
|
229
|
-
if (Array.isArray(existing)) {
|
|
230
|
-
existing.push(...preparedValue);
|
|
231
|
-
} else {
|
|
232
|
-
record[key] = [existing, ...preparedValue];
|
|
233
|
-
}
|
|
234
|
-
} else {
|
|
235
|
-
record[key] =
|
|
236
|
-
preparedValue.length > 1 ? preparedValue : (preparedValue[0] as string);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
385
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Discriminated error codes for network-level failures.
|
|
10
|
+
*/
|
|
11
|
+
export type NetworkErrorCode =
|
|
12
|
+
| "TIMEOUT"
|
|
13
|
+
| "ABORT"
|
|
14
|
+
| "ECONNREFUSED"
|
|
15
|
+
| "ECONNRESET"
|
|
16
|
+
| "ENOTFOUND"
|
|
17
|
+
| "ETIMEDOUT"
|
|
18
|
+
| "UNKNOWN";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Typed error for network-level failures during HTTP requests.
|
|
22
|
+
*
|
|
23
|
+
* Provides a discriminated `code` property for programmatic error handling
|
|
24
|
+
* instead of message string parsing. Thrown by ApiClient when the underlying
|
|
25
|
+
* fetch call fails before receiving a response.
|
|
26
|
+
*/
|
|
27
|
+
export class NetworkError extends Error {
|
|
28
|
+
public override readonly name = "NetworkError";
|
|
29
|
+
|
|
30
|
+
public constructor(
|
|
31
|
+
message: string,
|
|
32
|
+
public readonly code: NetworkErrorCode,
|
|
33
|
+
public readonly method: string,
|
|
34
|
+
public readonly url: string,
|
|
35
|
+
options?: ErrorOptions
|
|
36
|
+
) {
|
|
37
|
+
super(message, options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Typed error for path parameter substitution failures.
|
|
10
|
+
*
|
|
11
|
+
* Thrown when a path parameter key does not match any placeholder in the
|
|
12
|
+
* URL path pattern. Exposes `paramName` and `path` properties for
|
|
13
|
+
* programmatic error handling.
|
|
14
|
+
*/
|
|
15
|
+
export class PathParameterError extends Error {
|
|
16
|
+
public override readonly name = "PathParameterError";
|
|
17
|
+
|
|
18
|
+
public constructor(
|
|
19
|
+
message: string,
|
|
20
|
+
public readonly paramName: string,
|
|
21
|
+
public readonly path: string,
|
|
22
|
+
options?: ErrorOptions
|
|
23
|
+
) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -47,8 +47,7 @@ export abstract class RequestCommand<
|
|
|
47
47
|
Param extends IHttpParam = IHttpParam | undefined,
|
|
48
48
|
Query extends IHttpQuery = IHttpQuery | undefined,
|
|
49
49
|
Body extends IHttpBody = IHttpBody | undefined,
|
|
50
|
-
> implements IHttpRequest
|
|
51
|
-
{
|
|
50
|
+
> implements IHttpRequest {
|
|
52
51
|
/** The HTTP method for this request */
|
|
53
52
|
public readonly method!: HttpMethod;
|
|
54
53
|
/** The URL path pattern with parameter placeholders (e.g., '/users/:id') */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Typed error for response body parsing failures.
|
|
10
|
+
*
|
|
11
|
+
* Thrown when a JSON response body cannot be parsed. Provides structured
|
|
12
|
+
* metadata via `statusCode` and `bodyPreview` properties for diagnostics
|
|
13
|
+
* without relying on message string parsing.
|
|
14
|
+
*/
|
|
15
|
+
export class ResponseParseError extends Error {
|
|
16
|
+
public override readonly name = "ResponseParseError";
|
|
17
|
+
|
|
18
|
+
public constructor(
|
|
19
|
+
message: string,
|
|
20
|
+
public readonly statusCode: number,
|
|
21
|
+
public readonly bodyPreview: string,
|
|
22
|
+
options?: ErrorOptions
|
|
23
|
+
) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/lib/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexeus/typeweaver-clients",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Generates HTTP clients directly from your API definitions. Powered by Typeweaver 🧵✨",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -47,22 +47,20 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/rexeus/typeweaver#readme",
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"axios": "^1.13.0",
|
|
51
50
|
"zod": "^4.3.0",
|
|
52
|
-
"@rexeus/typeweaver-
|
|
53
|
-
"@rexeus/typeweaver-
|
|
51
|
+
"@rexeus/typeweaver-gen": "^0.5.0",
|
|
52
|
+
"@rexeus/typeweaver-core": "^0.5.0"
|
|
54
53
|
},
|
|
55
54
|
"devDependencies": {
|
|
56
55
|
"@hono/node-server": "^1.19.7",
|
|
57
|
-
"axios": "^1.13.2",
|
|
58
56
|
"test-utils": "file:../test-utils",
|
|
59
57
|
"zod": "^4.3.6",
|
|
60
|
-
"@rexeus/typeweaver-core": "^0.
|
|
61
|
-
"@rexeus/typeweaver-gen": "^0.
|
|
58
|
+
"@rexeus/typeweaver-core": "^0.5.0",
|
|
59
|
+
"@rexeus/typeweaver-gen": "^0.5.0"
|
|
62
60
|
},
|
|
63
61
|
"dependencies": {
|
|
64
62
|
"case": "^1.6.3",
|
|
65
|
-
"@rexeus/typeweaver-zod-to-ts": "^0.
|
|
63
|
+
"@rexeus/typeweaver-zod-to-ts": "^0.5.0"
|
|
66
64
|
},
|
|
67
65
|
"scripts": {
|
|
68
66
|
"typecheck": "tsc --noEmit",
|