@rexeus/typeweaver-clients 0.4.1 → 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 +261 -117
- 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,49 +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
|
-
|
|
71
|
-
|
|
72
|
+
throw new Error("Base URL must be provided");
|
|
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");
|
|
72
80
|
}
|
|
73
|
-
this.axiosInstance.defaults.baseURL = undefined;
|
|
74
81
|
|
|
75
82
|
this.unknownResponseHandling = props.unknownResponseHandling ?? "throw";
|
|
76
83
|
this.isSuccessStatusCode =
|
|
77
84
|
props.isSuccessStatusCode ??
|
|
78
85
|
((statusCode: number) => statusCode >= 200 && statusCode < 300);
|
|
86
|
+
this.timeoutMs = props.timeoutMs;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
/**
|
|
82
|
-
* Gets the process response options for this client instance.
|
|
83
|
-
*
|
|
84
|
-
* @returns The configuration options for processing responses
|
|
85
|
-
* @protected
|
|
86
|
-
*/
|
|
87
89
|
protected get processResponseOptions(): ProcessResponseOptions {
|
|
88
90
|
return {
|
|
89
91
|
unknownResponseHandling: this.unknownResponseHandling,
|
|
@@ -91,79 +93,247 @@ export abstract class ApiClient {
|
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
/**
|
|
95
|
-
* Executes an HTTP request using the provided command.
|
|
96
|
-
*
|
|
97
|
-
* This method:
|
|
98
|
-
* 1. Substitutes path parameters (e.g., `:id` with actual values)
|
|
99
|
-
* 2. Builds the complete URL with query parameters
|
|
100
|
-
* 3. Sends the HTTP request via Axios
|
|
101
|
-
* 4. Normalizes the response format
|
|
102
|
-
* 5. Handles network errors with specific error messages
|
|
103
|
-
*
|
|
104
|
-
* @param request - The request command containing all HTTP parameters
|
|
105
|
-
* @returns Promise resolving to the HTTP response
|
|
106
|
-
* @throws {Error} Network errors with specific messages (connection refused, timeout, etc.)
|
|
107
|
-
* @protected
|
|
108
|
-
*/
|
|
109
96
|
protected async execute(request: RequestCommand): Promise<IHttpResponse> {
|
|
110
97
|
const { method, path, header, query, param, body } = request;
|
|
111
98
|
|
|
112
|
-
const headers = this.createHeader(header);
|
|
113
99
|
const pathWithParam = this.createPath(path, param);
|
|
114
|
-
const
|
|
100
|
+
const relativeUrl = this.createUrl(pathWithParam, query);
|
|
101
|
+
const fullUrl = this.buildFullUrl(relativeUrl);
|
|
115
102
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} catch (error) {
|
|
126
|
-
if (error instanceof AxiosError) {
|
|
127
|
-
if (error.response) {
|
|
128
|
-
return this.createResponse(error.response);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// TODO: improve network error handling
|
|
132
|
-
if (error.code === "ECONNREFUSED") {
|
|
133
|
-
throw new Error("Network error: Connection refused");
|
|
134
|
-
}
|
|
135
|
-
if (error.code === "ECONNRESET") {
|
|
136
|
-
throw new Error("Network error: Connection reset by peer");
|
|
137
|
-
}
|
|
138
|
-
if (error.code === "ENOTFOUND") {
|
|
139
|
-
throw new Error("Network error: DNS lookup failed");
|
|
140
|
-
}
|
|
141
|
-
if (error.code === "ETIMEDOUT") {
|
|
142
|
-
throw new Error("Network error: Connection timed out");
|
|
143
|
-
}
|
|
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
|
+
});
|
|
144
112
|
|
|
145
|
-
|
|
146
|
-
|
|
113
|
+
return await this.createResponse(response, method, fullUrl);
|
|
114
|
+
}
|
|
147
115
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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);
|
|
151
125
|
}
|
|
152
126
|
}
|
|
153
127
|
|
|
154
|
-
private createResponse(
|
|
128
|
+
private async createResponse(
|
|
129
|
+
response: Response,
|
|
130
|
+
method: string,
|
|
131
|
+
url: string
|
|
132
|
+
): Promise<IHttpResponse> {
|
|
155
133
|
const header: IHttpHeader = {};
|
|
156
|
-
|
|
157
|
-
|
|
134
|
+
response.headers.forEach((value, key) => {
|
|
135
|
+
header[key] = value;
|
|
158
136
|
});
|
|
159
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
|
+
|
|
160
147
|
return {
|
|
161
|
-
body
|
|
148
|
+
body,
|
|
162
149
|
header,
|
|
163
150
|
statusCode: response.status,
|
|
164
151
|
};
|
|
165
152
|
}
|
|
166
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
|
+
|
|
167
337
|
private createPath(path: string, param?: IHttpParam): string {
|
|
168
338
|
if (!param) {
|
|
169
339
|
return path;
|
|
@@ -173,8 +343,10 @@ export abstract class ApiClient {
|
|
|
173
343
|
const result = acc.replace(`:${key}`, encodeURIComponent(value));
|
|
174
344
|
|
|
175
345
|
if (result === acc) {
|
|
176
|
-
throw new
|
|
177
|
-
`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
|
|
178
350
|
);
|
|
179
351
|
}
|
|
180
352
|
|
|
@@ -183,12 +355,9 @@ export abstract class ApiClient {
|
|
|
183
355
|
}
|
|
184
356
|
|
|
185
357
|
private createUrl(path: string, query?: IHttpQuery): string {
|
|
186
|
-
const base = this.baseUrl.replace(/\/+$/, "");
|
|
187
358
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
188
359
|
const queryString = this.buildQueryString(query);
|
|
189
|
-
return queryString
|
|
190
|
-
? `${base}${normalizedPath}?${queryString}`
|
|
191
|
-
: `${base}${normalizedPath}`;
|
|
360
|
+
return queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
|
|
192
361
|
}
|
|
193
362
|
|
|
194
363
|
private buildQueryString(query?: IHttpQuery): string {
|
|
@@ -213,29 +382,4 @@ export abstract class ApiClient {
|
|
|
213
382
|
}
|
|
214
383
|
return params.toString();
|
|
215
384
|
}
|
|
216
|
-
|
|
217
|
-
private createHeader(header: any): any {
|
|
218
|
-
return header;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private addMultiValue(
|
|
222
|
-
record: Record<string, string | string[]>,
|
|
223
|
-
key: string,
|
|
224
|
-
value: AxiosHeaderValue
|
|
225
|
-
): void {
|
|
226
|
-
const existing = record[key];
|
|
227
|
-
const preparedValue = Array.isArray(value)
|
|
228
|
-
? value.map(String)
|
|
229
|
-
: [String(value)];
|
|
230
|
-
if (existing) {
|
|
231
|
-
if (Array.isArray(existing)) {
|
|
232
|
-
existing.push(...preparedValue);
|
|
233
|
-
} else {
|
|
234
|
-
record[key] = [existing, ...preparedValue];
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
record[key] =
|
|
238
|
-
preparedValue.length > 1 ? preparedValue : (preparedValue[0] as string);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
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",
|