zlient 1.0.3 → 1.0.5
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 +86 -3
- package/dist/auth.d.ts +60 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/endpoint/BaseEndpoint.d.ts +45 -2
- package/dist/endpoint/BaseEndpoint.d.ts.map +1 -1
- package/dist/http/HttpClient.d.ts +131 -0
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/index.cjs +634 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -87
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +63 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/metrics.d.ts +73 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/schemas/common.d.ts +57 -5
- package/dist/schemas/common.d.ts.map +1 -1
- package/dist/types.d.ts +125 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +39 -0
- package/dist/validation.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
//#region rolldown:runtime
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __esm = (fn, res) => function() {
|
|
9
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
-
};
|
|
11
|
-
var __export = (all) => {
|
|
12
|
-
let target = {};
|
|
13
|
-
for (var name in all) __defProp(target, name, {
|
|
14
|
-
get: all[name],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return target;
|
|
18
|
-
};
|
|
19
|
-
var __copyProps = (to, from, except, desc) => {
|
|
20
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
21
|
-
key = keys[i];
|
|
22
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
23
|
-
get: ((k) => from[k]).bind(null, key),
|
|
24
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
return to;
|
|
28
|
-
};
|
|
29
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
3
|
//#region lib/types.ts
|
|
33
4
|
const HTTPMethod = {
|
|
34
5
|
GET: "GET",
|
|
@@ -39,7 +10,19 @@ const HTTPMethod = {
|
|
|
39
10
|
HEAD: "HEAD",
|
|
40
11
|
OPTIONS: "OPTIONS"
|
|
41
12
|
};
|
|
42
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Custom error class for API-related errors.
|
|
15
|
+
* Includes HTTP status codes, response details, and validation errors.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* throw new ApiError('Invalid request', {
|
|
20
|
+
* status: 400,
|
|
21
|
+
* details: { field: 'email', message: 'Invalid format' }
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
var ApiError = class ApiError extends Error {
|
|
43
26
|
status;
|
|
44
27
|
details;
|
|
45
28
|
zodError;
|
|
@@ -50,14 +33,62 @@ var ApiError = class extends Error {
|
|
|
50
33
|
this.details = options?.details;
|
|
51
34
|
this.cause = options?.cause;
|
|
52
35
|
this.zodError = options?.zodError;
|
|
36
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, ApiError);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if this is a validation error (has zodError)
|
|
40
|
+
*/
|
|
41
|
+
isValidationError() {
|
|
42
|
+
return !!this.zodError;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if this is a client error (4xx status)
|
|
46
|
+
*/
|
|
47
|
+
isClientError() {
|
|
48
|
+
return !!this.status && this.status >= 400 && this.status < 500;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if this is a server error (5xx status)
|
|
52
|
+
*/
|
|
53
|
+
isServerError() {
|
|
54
|
+
return !!this.status && this.status >= 500;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get a formatted error message with all available details
|
|
58
|
+
*/
|
|
59
|
+
toJSON() {
|
|
60
|
+
return {
|
|
61
|
+
name: this.name,
|
|
62
|
+
message: this.message,
|
|
63
|
+
status: this.status,
|
|
64
|
+
details: this.details,
|
|
65
|
+
zodError: this.zodError?.issues,
|
|
66
|
+
stack: this.stack
|
|
67
|
+
};
|
|
53
68
|
}
|
|
54
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Schema for paginated responses
|
|
72
|
+
*/
|
|
55
73
|
const PaginationSchema = z.object({
|
|
56
74
|
items: z.array(z.unknown()),
|
|
57
75
|
total: z.number().int().nonnegative(),
|
|
58
76
|
page: z.number().int().nonnegative(),
|
|
59
77
|
pageSize: z.number().int().positive()
|
|
60
78
|
});
|
|
79
|
+
/**
|
|
80
|
+
* Converts query parameters to a URL query string.
|
|
81
|
+
* Filters out undefined values automatically.
|
|
82
|
+
*
|
|
83
|
+
* @param q - Query parameters as URLSearchParams or object
|
|
84
|
+
* @returns Query string with leading '?' or empty string
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* toQueryString({ page: 1, filter: 'active' }) // "?page=1&filter=active"
|
|
89
|
+
* toQueryString({ optional: undefined }) // ""
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
61
92
|
function toQueryString(q) {
|
|
62
93
|
if (!q) return "";
|
|
63
94
|
if (q instanceof URLSearchParams) {
|
|
@@ -74,48 +105,93 @@ function toQueryString(q) {
|
|
|
74
105
|
|
|
75
106
|
//#endregion
|
|
76
107
|
//#region lib/auth.ts
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
108
|
+
/**
|
|
109
|
+
* No-op authentication provider (no authentication applied).
|
|
110
|
+
* Use this when you don't need authentication.
|
|
111
|
+
*/
|
|
112
|
+
var NoAuth = class {
|
|
113
|
+
async apply() {}
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* API Key authentication provider.
|
|
117
|
+
* Supports both header-based and query parameter-based authentication.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* // Header-based
|
|
122
|
+
* const auth = new ApiKeyAuth({ header: 'X-API-Key', value: 'secret' });
|
|
123
|
+
*
|
|
124
|
+
* // Query parameter-based
|
|
125
|
+
* const auth = new ApiKeyAuth({ query: 'apiKey', value: 'secret' });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
var ApiKeyAuth = class {
|
|
129
|
+
constructor(opts) {
|
|
130
|
+
this.opts = opts;
|
|
131
|
+
if (!opts.header && !opts.query) throw new Error("ApiKeyAuth requires either \"header\" or \"query\" option");
|
|
132
|
+
if (opts.header && opts.query) throw new Error("ApiKeyAuth cannot use both \"header\" and \"query\" options");
|
|
133
|
+
}
|
|
134
|
+
apply({ url, init }) {
|
|
135
|
+
if (this.opts.header) init.headers = {
|
|
136
|
+
...init.headers,
|
|
137
|
+
[this.opts.header]: this.opts.value
|
|
138
|
+
};
|
|
139
|
+
else if (this.opts.query) {
|
|
140
|
+
const u = new URL(url);
|
|
141
|
+
u.searchParams.set(this.opts.query, this.opts.value);
|
|
142
|
+
init.__urlOverride = u.toString();
|
|
113
143
|
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Bearer token authentication provider.
|
|
148
|
+
* Supports both static tokens and dynamic token fetching (e.g., for OAuth2 refresh).
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* // Static token
|
|
153
|
+
* const auth = new BearerTokenAuth(() => 'my-token');
|
|
154
|
+
*
|
|
155
|
+
* // Dynamic token with refresh
|
|
156
|
+
* const auth = new BearerTokenAuth(async () => {
|
|
157
|
+
* return await refreshAccessToken();
|
|
158
|
+
* });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
var BearerTokenAuth = class {
|
|
162
|
+
constructor(getToken) {
|
|
163
|
+
this.getToken = getToken;
|
|
164
|
+
}
|
|
165
|
+
async apply({ init }) {
|
|
166
|
+
const token = await this.getToken();
|
|
167
|
+
if (!token) throw new Error("BearerTokenAuth: token is empty or undefined");
|
|
168
|
+
init.headers = {
|
|
169
|
+
...init.headers,
|
|
170
|
+
Authorization: `Bearer ${token}`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
};
|
|
116
174
|
|
|
117
175
|
//#endregion
|
|
118
176
|
//#region lib/validation.ts
|
|
177
|
+
/**
|
|
178
|
+
* Safely parse data with a Zod schema without throwing.
|
|
179
|
+
* Returns a result object with success status and data or error.
|
|
180
|
+
*
|
|
181
|
+
* @param schema - Zod schema to validate against
|
|
182
|
+
* @param data - Data to validate
|
|
183
|
+
* @returns Result object with success flag and data/error
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* const result = safeParse(UserSchema, userData);
|
|
188
|
+
* if (result.success) {
|
|
189
|
+
* console.log(result.data);
|
|
190
|
+
* } else {
|
|
191
|
+
* console.error(result.error);
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
119
195
|
function safeParse(schema, data) {
|
|
120
196
|
const res = schema.safeParse(data);
|
|
121
197
|
if (res.success) return {
|
|
@@ -127,14 +203,220 @@ function safeParse(schema, data) {
|
|
|
127
203
|
error: res.error
|
|
128
204
|
};
|
|
129
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Parse data with a Zod schema, throwing an ApiError on validation failure.
|
|
208
|
+
* Use this when you want to fail fast on invalid data.
|
|
209
|
+
*
|
|
210
|
+
* @param schema - Zod schema to validate against
|
|
211
|
+
* @param data - Data to validate
|
|
212
|
+
* @returns Validated and typed data
|
|
213
|
+
* @throws {ApiError} If validation fails
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* try {
|
|
218
|
+
* const user = parseOrThrow(UserSchema, userData);
|
|
219
|
+
* console.log(user);
|
|
220
|
+
* } catch (error) {
|
|
221
|
+
* if (error instanceof ApiError && error.zodError) {
|
|
222
|
+
* console.error('Validation failed:', error.zodError.issues);
|
|
223
|
+
* }
|
|
224
|
+
* }
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
130
227
|
function parseOrThrow(schema, data) {
|
|
131
228
|
const res = schema.safeParse(data);
|
|
132
229
|
if (!res.success) throw new ApiError("Response validation failed", { zodError: res.error });
|
|
133
230
|
return res.data;
|
|
134
231
|
}
|
|
135
232
|
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region lib/logger.ts
|
|
235
|
+
/**
|
|
236
|
+
* Log levels for structured logging.
|
|
237
|
+
*/
|
|
238
|
+
let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
|
|
239
|
+
LogLevel$1["DEBUG"] = "debug";
|
|
240
|
+
LogLevel$1["INFO"] = "info";
|
|
241
|
+
LogLevel$1["WARN"] = "warn";
|
|
242
|
+
LogLevel$1["ERROR"] = "error";
|
|
243
|
+
return LogLevel$1;
|
|
244
|
+
}({});
|
|
245
|
+
/**
|
|
246
|
+
* Default console logger implementation.
|
|
247
|
+
* Formats log entries as JSON for easy parsing.
|
|
248
|
+
*/
|
|
249
|
+
var ConsoleLogger = class {
|
|
250
|
+
constructor(minLevel = LogLevel.INFO) {
|
|
251
|
+
this.minLevel = minLevel;
|
|
252
|
+
}
|
|
253
|
+
log(entry) {
|
|
254
|
+
const levels = [
|
|
255
|
+
LogLevel.DEBUG,
|
|
256
|
+
LogLevel.INFO,
|
|
257
|
+
LogLevel.WARN,
|
|
258
|
+
LogLevel.ERROR
|
|
259
|
+
];
|
|
260
|
+
if (levels.indexOf(entry.level) < levels.indexOf(this.minLevel)) return;
|
|
261
|
+
const output = {
|
|
262
|
+
...entry,
|
|
263
|
+
error: entry.error ? {
|
|
264
|
+
message: entry.error.message,
|
|
265
|
+
stack: entry.error.stack,
|
|
266
|
+
name: entry.error.name
|
|
267
|
+
} : void 0
|
|
268
|
+
};
|
|
269
|
+
switch (entry.level) {
|
|
270
|
+
case LogLevel.DEBUG:
|
|
271
|
+
console.debug(JSON.stringify(output));
|
|
272
|
+
break;
|
|
273
|
+
case LogLevel.INFO:
|
|
274
|
+
console.info(JSON.stringify(output));
|
|
275
|
+
break;
|
|
276
|
+
case LogLevel.WARN:
|
|
277
|
+
console.warn(JSON.stringify(output));
|
|
278
|
+
break;
|
|
279
|
+
case LogLevel.ERROR:
|
|
280
|
+
console.error(JSON.stringify(output));
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* No-op logger that discards all log entries.
|
|
287
|
+
* Use this in production if you don't want any logging.
|
|
288
|
+
*/
|
|
289
|
+
var NoOpLogger = class {
|
|
290
|
+
log(_entry) {}
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Utility class for creating structured log entries.
|
|
294
|
+
*/
|
|
295
|
+
var LoggerUtil = class {
|
|
296
|
+
constructor(logger) {
|
|
297
|
+
this.logger = logger;
|
|
298
|
+
}
|
|
299
|
+
debug(message, context) {
|
|
300
|
+
this.logger.log({
|
|
301
|
+
level: LogLevel.DEBUG,
|
|
302
|
+
message,
|
|
303
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
304
|
+
context
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
info(message, context) {
|
|
308
|
+
this.logger.log({
|
|
309
|
+
level: LogLevel.INFO,
|
|
310
|
+
message,
|
|
311
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
312
|
+
context
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
warn(message, context) {
|
|
316
|
+
this.logger.log({
|
|
317
|
+
level: LogLevel.WARN,
|
|
318
|
+
message,
|
|
319
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
320
|
+
context
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
error(message, error, context) {
|
|
324
|
+
this.logger.log({
|
|
325
|
+
level: LogLevel.ERROR,
|
|
326
|
+
message,
|
|
327
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
328
|
+
context,
|
|
329
|
+
error
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region lib/metrics.ts
|
|
336
|
+
/**
|
|
337
|
+
* No-op metrics collector that discards all metrics.
|
|
338
|
+
*/
|
|
339
|
+
var NoOpMetricsCollector = class {
|
|
340
|
+
collect(_metrics) {}
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* In-memory metrics collector for testing and development.
|
|
344
|
+
* Stores metrics in memory with configurable retention.
|
|
345
|
+
*/
|
|
346
|
+
var InMemoryMetricsCollector = class {
|
|
347
|
+
metrics = [];
|
|
348
|
+
maxEntries;
|
|
349
|
+
constructor(maxEntries = 1e3) {
|
|
350
|
+
this.maxEntries = maxEntries;
|
|
351
|
+
}
|
|
352
|
+
collect(metrics) {
|
|
353
|
+
this.metrics.push(metrics);
|
|
354
|
+
if (this.metrics.length > this.maxEntries) this.metrics.shift();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get all collected metrics.
|
|
358
|
+
*/
|
|
359
|
+
getMetrics() {
|
|
360
|
+
return [...this.metrics];
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get metrics summary statistics.
|
|
364
|
+
*/
|
|
365
|
+
getSummary() {
|
|
366
|
+
if (this.metrics.length === 0) return {
|
|
367
|
+
total: 0,
|
|
368
|
+
successful: 0,
|
|
369
|
+
failed: 0,
|
|
370
|
+
avgDurationMs: 0,
|
|
371
|
+
minDurationMs: 0,
|
|
372
|
+
maxDurationMs: 0
|
|
373
|
+
};
|
|
374
|
+
const successful = this.metrics.filter((m) => m.success).length;
|
|
375
|
+
const durations = this.metrics.map((m) => m.durationMs);
|
|
376
|
+
const sum = durations.reduce((a, b) => a + b, 0);
|
|
377
|
+
return {
|
|
378
|
+
total: this.metrics.length,
|
|
379
|
+
successful,
|
|
380
|
+
failed: this.metrics.length - successful,
|
|
381
|
+
avgDurationMs: sum / this.metrics.length,
|
|
382
|
+
minDurationMs: Math.min(...durations),
|
|
383
|
+
maxDurationMs: Math.max(...durations)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Clear all collected metrics.
|
|
388
|
+
*/
|
|
389
|
+
clear() {
|
|
390
|
+
this.metrics = [];
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Console-based metrics collector for debugging.
|
|
395
|
+
*/
|
|
396
|
+
var ConsoleMetricsCollector = class {
|
|
397
|
+
collect(metrics) {
|
|
398
|
+
console.log("[METRICS]", JSON.stringify(metrics));
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
136
402
|
//#endregion
|
|
137
403
|
//#region lib/http/HttpClient.ts
|
|
404
|
+
/**
|
|
405
|
+
* HTTP client with built-in retry logic, authentication, and interceptors.
|
|
406
|
+
* Supports multiple base URLs, type-safe requests, and comprehensive error handling.
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```ts
|
|
410
|
+
* const client = new HttpClient({
|
|
411
|
+
* baseUrls: { default: 'https://api.example.com' },
|
|
412
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
413
|
+
* retry: { maxRetries: 3, baseDelayMs: 1000 },
|
|
414
|
+
* timeout: { requestTimeoutMs: 30000 }
|
|
415
|
+
* });
|
|
416
|
+
*
|
|
417
|
+
* const { data } = await client.request('GET', '/users', undefined, { query: { page: 1 } });
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
138
420
|
var HttpClient = class {
|
|
139
421
|
fetchImpl;
|
|
140
422
|
baseUrls;
|
|
@@ -143,9 +425,19 @@ var HttpClient = class {
|
|
|
143
425
|
retry;
|
|
144
426
|
timeoutMs;
|
|
145
427
|
auth;
|
|
428
|
+
logger;
|
|
429
|
+
metrics;
|
|
430
|
+
/**
|
|
431
|
+
* Creates a new HTTP client instance.
|
|
432
|
+
*
|
|
433
|
+
* @param opts - Client configuration options
|
|
434
|
+
* @throws {Error} If no fetch implementation is available
|
|
435
|
+
*/
|
|
146
436
|
constructor(opts) {
|
|
147
437
|
this.fetchImpl = opts.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
148
438
|
if (!this.fetchImpl) throw new Error("No fetch implementation found. Pass one via options.fetch.");
|
|
439
|
+
if (!opts.baseUrls || typeof opts.baseUrls !== "object") throw new Error("baseUrls must be provided and must be an object");
|
|
440
|
+
if (!opts.baseUrls.default) throw new Error("baseUrls must include a \"default\" key");
|
|
149
441
|
this.baseUrls = opts.baseUrls;
|
|
150
442
|
this.headers = opts.headers ?? { "Content-Type": "application/json" };
|
|
151
443
|
this.interceptors = opts.interceptors ?? {};
|
|
@@ -155,21 +447,47 @@ var HttpClient = class {
|
|
|
155
447
|
jitter: .2,
|
|
156
448
|
retryMethods: ["GET", "HEAD"]
|
|
157
449
|
};
|
|
450
|
+
if (this.retry.maxRetries < 0) throw new Error("retry.maxRetries must be non-negative");
|
|
451
|
+
if (this.retry.baseDelayMs < 0) throw new Error("retry.baseDelayMs must be non-negative");
|
|
452
|
+
if (this.retry.jitter !== void 0 && (this.retry.jitter < 0 || this.retry.jitter > 1)) throw new Error("retry.jitter must be between 0 and 1");
|
|
158
453
|
this.timeoutMs = opts.timeout?.requestTimeoutMs;
|
|
159
|
-
this.
|
|
454
|
+
if (this.timeoutMs !== void 0 && this.timeoutMs < 0) throw new Error("timeout.requestTimeoutMs must be non-negative");
|
|
455
|
+
this.auth = opts["auth"] ?? new NoAuth();
|
|
456
|
+
this.logger = new LoggerUtil(opts.logger ?? new NoOpLogger());
|
|
457
|
+
this.metrics = opts.metrics ?? new NoOpMetricsCollector();
|
|
160
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Set or update the authentication provider.
|
|
461
|
+
*
|
|
462
|
+
* @param auth - Authentication provider instance
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* client.setAuth(new BearerTokenAuth(() => getToken()));
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
161
468
|
setAuth(auth) {
|
|
162
469
|
this.auth = auth;
|
|
163
470
|
}
|
|
164
471
|
resolveBaseUrl(key) {
|
|
165
472
|
const k = key || "default";
|
|
166
473
|
const url = this.baseUrls[k];
|
|
167
|
-
if (!url)
|
|
474
|
+
if (!url) {
|
|
475
|
+
const availableKeys = Object.keys(this.baseUrls).join(", ");
|
|
476
|
+
throw new Error(`Unknown baseUrl key: "${k}". Available keys: ${availableKeys}`);
|
|
477
|
+
}
|
|
168
478
|
return url.replace(/\/$/, "");
|
|
169
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Sleep for a specified duration (used for retry backoff).
|
|
482
|
+
* @private
|
|
483
|
+
*/
|
|
170
484
|
sleep(ms) {
|
|
171
485
|
return new Promise((res) => setTimeout(res, ms));
|
|
172
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Execute a function with retry logic and exponential backoff.
|
|
489
|
+
* @private
|
|
490
|
+
*/
|
|
173
491
|
async withRetry(fn, canRetry) {
|
|
174
492
|
let attempt = 0;
|
|
175
493
|
const { maxRetries, baseDelayMs, jitter = .2 } = this.retry;
|
|
@@ -186,12 +504,20 @@ var HttpClient = class {
|
|
|
186
504
|
attempt++;
|
|
187
505
|
}
|
|
188
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Run all registered before-request hooks.
|
|
509
|
+
* @private
|
|
510
|
+
*/
|
|
189
511
|
async runBeforeHooks(url, init) {
|
|
190
512
|
for (const h of this.interceptors.beforeRequest ?? []) await h({
|
|
191
513
|
url,
|
|
192
514
|
init
|
|
193
515
|
});
|
|
194
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Run all registered after-response hooks.
|
|
519
|
+
* @private
|
|
520
|
+
*/
|
|
195
521
|
async runAfterHooks(req, res, parsed) {
|
|
196
522
|
for (const h of this.interceptors.afterResponse ?? []) await h({
|
|
197
523
|
request: req,
|
|
@@ -199,8 +525,33 @@ var HttpClient = class {
|
|
|
199
525
|
parsed
|
|
200
526
|
});
|
|
201
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Make an HTTP request with automatic retry, authentication, and validation.
|
|
530
|
+
*
|
|
531
|
+
* @param method - HTTP method (GET, POST, PUT, etc.)
|
|
532
|
+
* @param path - Request path (will be appended to base URL)
|
|
533
|
+
* @param body - Request body (will be JSON.stringify'd if Content-Type is json)
|
|
534
|
+
* @param options - Additional request options (headers, query params, etc.)
|
|
535
|
+
* @returns Promise resolving to response data and Response object
|
|
536
|
+
* @throws {ApiError} If request fails or response validation fails
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* ```ts
|
|
540
|
+
* const { data, response } = await client.request('GET', '/users', undefined, {
|
|
541
|
+
* query: { page: 1, limit: 10 },
|
|
542
|
+
* headers: { 'X-Custom': 'value' }
|
|
543
|
+
* });
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
202
546
|
async request(method, path, body, options) {
|
|
547
|
+
const startTime = Date.now();
|
|
203
548
|
let url = `${this.resolveBaseUrl(options?.baseUrlKey)}${path}${toQueryString(options?.query)}`;
|
|
549
|
+
this.logger.debug("HTTP request initiated", {
|
|
550
|
+
method,
|
|
551
|
+
path,
|
|
552
|
+
baseUrlKey: options?.baseUrlKey,
|
|
553
|
+
hasBody: body !== void 0
|
|
554
|
+
});
|
|
204
555
|
const headers = {
|
|
205
556
|
...this.headers,
|
|
206
557
|
...options?.headers ?? {}
|
|
@@ -210,7 +561,7 @@ var HttpClient = class {
|
|
|
210
561
|
const init = {
|
|
211
562
|
method,
|
|
212
563
|
headers,
|
|
213
|
-
body: body != null ? headers["Content-Type"]?.includes("json") ? JSON.stringify(body) : body : void 0,
|
|
564
|
+
body: body != null ? headers["Content-Type"]?.includes("json") ? JSON.stringify(body) : String(body) : void 0,
|
|
214
565
|
signal
|
|
215
566
|
};
|
|
216
567
|
await this.auth.apply({
|
|
@@ -222,16 +573,21 @@ var HttpClient = class {
|
|
|
222
573
|
await this.runBeforeHooks(url, init);
|
|
223
574
|
const doFetch = async () => {
|
|
224
575
|
let timeoutId;
|
|
225
|
-
if (this.timeoutMs && !options?.signal) timeoutId = setTimeout(() =>
|
|
576
|
+
if (this.timeoutMs && !options?.signal) timeoutId = setTimeout(() => {
|
|
577
|
+
const timeoutError = /* @__PURE__ */ new Error("Request timeout");
|
|
578
|
+
timeoutError.name = "TimeoutError";
|
|
579
|
+
controller.abort(timeoutError);
|
|
580
|
+
}, this.timeoutMs);
|
|
226
581
|
try {
|
|
227
582
|
const req = new Request(url, init);
|
|
228
|
-
console.log("HTTP Request:", req.method, req.url, req);
|
|
229
583
|
const res = await this.fetchImpl(req);
|
|
230
584
|
if (!res.ok) {
|
|
231
585
|
let text = "";
|
|
232
586
|
try {
|
|
233
587
|
text = await res.text();
|
|
234
|
-
} catch {
|
|
588
|
+
} catch (readError) {
|
|
589
|
+
text = `Failed to read response: ${readError instanceof Error ? readError.message : String(readError)}`;
|
|
590
|
+
}
|
|
235
591
|
throw new ApiError(`HTTP ${res.status}: ${res.statusText}`, {
|
|
236
592
|
status: res.status,
|
|
237
593
|
details: text
|
|
@@ -239,33 +595,151 @@ var HttpClient = class {
|
|
|
239
595
|
}
|
|
240
596
|
const data = (res.headers.get("content-type") || "").includes("json") ? await res.json() : await res.text();
|
|
241
597
|
await this.runAfterHooks(new Request(url, init), res, data);
|
|
598
|
+
const duration = Date.now() - startTime;
|
|
599
|
+
this.logger.info("HTTP request successful", {
|
|
600
|
+
method,
|
|
601
|
+
url,
|
|
602
|
+
status: res.status,
|
|
603
|
+
durationMs: duration
|
|
604
|
+
});
|
|
605
|
+
this.metrics.collect({
|
|
606
|
+
method,
|
|
607
|
+
path,
|
|
608
|
+
status: res.status,
|
|
609
|
+
durationMs: duration,
|
|
610
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
611
|
+
success: true
|
|
612
|
+
});
|
|
242
613
|
return {
|
|
243
614
|
data,
|
|
244
615
|
response: res
|
|
245
616
|
};
|
|
617
|
+
} catch (error) {
|
|
618
|
+
const duration = Date.now() - startTime;
|
|
619
|
+
this.logger.error("HTTP request failed", error, {
|
|
620
|
+
method,
|
|
621
|
+
url,
|
|
622
|
+
durationMs: duration
|
|
623
|
+
});
|
|
624
|
+
this.metrics.collect({
|
|
625
|
+
method,
|
|
626
|
+
path,
|
|
627
|
+
status: error instanceof ApiError ? error.status : void 0,
|
|
628
|
+
durationMs: duration,
|
|
629
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
630
|
+
success: false,
|
|
631
|
+
error: error instanceof Error ? error.message : String(error)
|
|
632
|
+
});
|
|
633
|
+
throw error;
|
|
246
634
|
} finally {
|
|
247
635
|
if (timeoutId) clearTimeout(timeoutId);
|
|
248
636
|
}
|
|
249
637
|
};
|
|
250
638
|
const canRetry = ({ response, error }) => {
|
|
639
|
+
if (error && typeof error === "object" && "name" in error) {
|
|
640
|
+
const errorName = error.name;
|
|
641
|
+
if (errorName === "AbortError" || errorName === "TimeoutError") return false;
|
|
642
|
+
}
|
|
251
643
|
if (error instanceof ApiError && error.status && error.status >= 500) return true;
|
|
252
|
-
if (error
|
|
253
|
-
if (!error?.status && !response) return true;
|
|
644
|
+
if (error && !response) return true;
|
|
254
645
|
return false;
|
|
255
646
|
};
|
|
256
647
|
if (!this.retry.retryMethods?.includes(method)) return doFetch();
|
|
257
648
|
return this.withRetry(doFetch, canRetry);
|
|
258
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Convenience method for GET requests.
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```ts
|
|
655
|
+
* const { data } = await client.get('/users', { query: { page: 1 } });
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
async get(path, options) {
|
|
659
|
+
return this.request("GET", path, void 0, options);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Convenience method for POST requests.
|
|
663
|
+
*
|
|
664
|
+
* @example
|
|
665
|
+
* ```ts
|
|
666
|
+
* const { data } = await client.post('/users', { name: 'John' });
|
|
667
|
+
* ```
|
|
668
|
+
*/
|
|
669
|
+
async post(path, body, options) {
|
|
670
|
+
return this.request("POST", path, body, options);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Convenience method for PUT requests.
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* ```ts
|
|
677
|
+
* const { data } = await client.put('/users/1', { name: 'John Updated' });
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
async put(path, body, options) {
|
|
681
|
+
return this.request("PUT", path, body, options);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Convenience method for PATCH requests.
|
|
685
|
+
*
|
|
686
|
+
* @example
|
|
687
|
+
* ```ts
|
|
688
|
+
* const { data } = await client.patch('/users/1', { name: 'John' });
|
|
689
|
+
* ```
|
|
690
|
+
*/
|
|
691
|
+
async patch(path, body, options) {
|
|
692
|
+
return this.request("PATCH", path, body, options);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Convenience method for DELETE requests.
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* ```ts
|
|
699
|
+
* const { data } = await client.delete('/users/1');
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
async delete(path, options) {
|
|
703
|
+
return this.request("DELETE", path, void 0, options);
|
|
704
|
+
}
|
|
259
705
|
};
|
|
260
706
|
|
|
261
707
|
//#endregion
|
|
262
708
|
//#region lib/endpoint/BaseEndpoint.ts
|
|
263
709
|
/**
|
|
264
|
-
* Generic, strongly-typed endpoint with Zod schemas for request and response.
|
|
710
|
+
* Generic, strongly-typed endpoint with Zod schemas for request and response validation.
|
|
711
|
+
* Extend this class to create type-safe API endpoints.
|
|
712
|
+
*
|
|
713
|
+
* @template ReqSchema - Zod schema for request validation
|
|
714
|
+
* @template ResSchema - Zod schema for response validation
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* ```ts
|
|
718
|
+
* const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
719
|
+
* const CreateUserSchema = z.object({ name: z.string() });
|
|
720
|
+
*
|
|
721
|
+
* class GetUser extends BaseEndpoint<typeof CreateUserSchema, typeof UserSchema> {
|
|
722
|
+
* protected method = 'GET' as const;
|
|
723
|
+
* protected path = (args: z.infer<typeof CreateUserSchema>) => `/users/${args.id}`;
|
|
724
|
+
*
|
|
725
|
+
* constructor(client: HttpClient) {
|
|
726
|
+
* super(client, {
|
|
727
|
+
* requestSchema: CreateUserSchema,
|
|
728
|
+
* responseSchema: UserSchema
|
|
729
|
+
* });
|
|
730
|
+
* }
|
|
731
|
+
* }
|
|
732
|
+
* ```
|
|
265
733
|
*/
|
|
266
734
|
var BaseEndpoint = class {
|
|
735
|
+
/** Optional request schema for validation */
|
|
267
736
|
requestSchema;
|
|
737
|
+
/** Response schema for validation */
|
|
268
738
|
responseSchema;
|
|
739
|
+
/**
|
|
740
|
+
* @param client - HttpClient instance
|
|
741
|
+
* @param cfg - Configuration with request and response schemas
|
|
742
|
+
*/
|
|
269
743
|
constructor(client, cfg) {
|
|
270
744
|
this.client = client;
|
|
271
745
|
this.requestSchema = cfg.requestSchema;
|
|
@@ -273,6 +747,19 @@ var BaseEndpoint = class {
|
|
|
273
747
|
}
|
|
274
748
|
/**
|
|
275
749
|
* Call the endpoint with strong typing derived from schemas.
|
|
750
|
+
* Validates request data before sending and response data after receiving.
|
|
751
|
+
*
|
|
752
|
+
* @param args - Request arguments (typed by ReqSchema)
|
|
753
|
+
* @param options - Additional request options
|
|
754
|
+
* @returns Promise resolving to validated response data (typed by ResSchema)
|
|
755
|
+
* @throws {ZodError} If request validation fails
|
|
756
|
+
* @throws {ApiError} If response validation fails or request fails
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* const endpoint = new GetUser(client);
|
|
761
|
+
* const user = await endpoint.call({ id: 1 });
|
|
762
|
+
* ```
|
|
276
763
|
*/
|
|
277
764
|
async call(args, options) {
|
|
278
765
|
if (this.requestSchema) {
|
|
@@ -280,10 +767,16 @@ var BaseEndpoint = class {
|
|
|
280
767
|
if (!parsed.success) throw parsed.error;
|
|
281
768
|
}
|
|
282
769
|
const path = typeof this.path === "function" ? this.path(args) : this.path;
|
|
283
|
-
const body = this.method
|
|
770
|
+
const body = this.method !== "GET" && this.method !== "HEAD" ? args : void 0;
|
|
771
|
+
const queryFromArgs = args?.query;
|
|
772
|
+
let mergedQuery;
|
|
773
|
+
if (options?.query || queryFromArgs) mergedQuery = {
|
|
774
|
+
...typeof options?.query === "object" && !(options?.query instanceof URLSearchParams) ? options.query : {},
|
|
775
|
+
...typeof queryFromArgs === "object" && queryFromArgs !== null ? queryFromArgs : {}
|
|
776
|
+
};
|
|
284
777
|
const { data } = await this.client.request(this.method, path, body, {
|
|
285
778
|
...options,
|
|
286
|
-
query: (
|
|
779
|
+
query: mergedQuery && Object.keys(mergedQuery).length > 0 ? mergedQuery : options?.query
|
|
287
780
|
});
|
|
288
781
|
return parseOrThrow(this.responseSchema, data);
|
|
289
782
|
}
|
|
@@ -291,29 +784,81 @@ var BaseEndpoint = class {
|
|
|
291
784
|
|
|
292
785
|
//#endregion
|
|
293
786
|
//#region lib/schemas/common.ts
|
|
787
|
+
/**
|
|
788
|
+
* Common ID type that supports strings, numbers, or UUIDs.
|
|
789
|
+
* Use this for entity identifiers in your schemas.
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* ```ts
|
|
793
|
+
* const UserSchema = z.object({ id: Id, name: z.string() });
|
|
794
|
+
* ```
|
|
795
|
+
*/
|
|
294
796
|
const Id = z.union([
|
|
295
797
|
z.string().min(1),
|
|
296
798
|
z.number(),
|
|
297
799
|
z.uuid({ version: "v4" })
|
|
298
800
|
]);
|
|
801
|
+
/**
|
|
802
|
+
* Common timestamp fields for entities.
|
|
803
|
+
* Use this for database models with creation/update tracking.
|
|
804
|
+
*
|
|
805
|
+
* @example
|
|
806
|
+
* ```ts
|
|
807
|
+
* const UserSchema = z.object({
|
|
808
|
+
* id: Id,
|
|
809
|
+
* name: z.string(),
|
|
810
|
+
* ...Timestamps.shape
|
|
811
|
+
* });
|
|
812
|
+
* ```
|
|
813
|
+
*/
|
|
299
814
|
const Timestamps = z.object({
|
|
300
|
-
createdAt: z.
|
|
301
|
-
updatedAt: z.
|
|
815
|
+
createdAt: z.string().datetime(),
|
|
816
|
+
updatedAt: z.string().datetime()
|
|
302
817
|
});
|
|
818
|
+
/**
|
|
819
|
+
* Metadata information typically included in API responses.
|
|
820
|
+
* Contains request tracking and debugging information.
|
|
821
|
+
*/
|
|
303
822
|
const Meta = z.object({
|
|
304
823
|
requestId: z.string().optional(),
|
|
305
|
-
timestamp: z.
|
|
824
|
+
timestamp: z.string().datetime().optional(),
|
|
306
825
|
traceId: z.string().optional()
|
|
307
826
|
});
|
|
827
|
+
/**
|
|
828
|
+
* Detailed error information for a specific field or path.
|
|
829
|
+
*/
|
|
308
830
|
const ErrorDetail = z.object({
|
|
309
831
|
path: z.string().optional(),
|
|
310
832
|
message: z.string()
|
|
311
833
|
});
|
|
834
|
+
/**
|
|
835
|
+
* Standard API error response schema.
|
|
836
|
+
* Use this for consistent error handling across your API.
|
|
837
|
+
*/
|
|
312
838
|
const ApiErrorSchema = z.object({
|
|
313
839
|
code: z.string(),
|
|
314
840
|
message: z.string(),
|
|
315
841
|
details: z.array(ErrorDetail).optional()
|
|
316
842
|
});
|
|
843
|
+
/**
|
|
844
|
+
* Generic envelope wrapper for API responses.
|
|
845
|
+
* Provides consistent structure with success flag, data, error, and metadata.
|
|
846
|
+
*
|
|
847
|
+
* @param inner - Zod schema for the response data
|
|
848
|
+
* @returns Envelope schema wrapping the inner schema
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```ts
|
|
852
|
+
* const UserResponseSchema = Envelope(z.object({ id: Id, name: z.string() }));
|
|
853
|
+
*
|
|
854
|
+
* // Response structure:
|
|
855
|
+
* // {
|
|
856
|
+
* // success: true,
|
|
857
|
+
* // data: { id: 1, name: 'John' },
|
|
858
|
+
* // meta: { requestId: '...' }
|
|
859
|
+
* // }
|
|
860
|
+
* ```
|
|
861
|
+
*/
|
|
317
862
|
const Envelope = (inner) => z.object({
|
|
318
863
|
success: z.boolean(),
|
|
319
864
|
data: inner.optional(),
|
|
@@ -322,9 +867,5 @@ const Envelope = (inner) => z.object({
|
|
|
322
867
|
});
|
|
323
868
|
|
|
324
869
|
//#endregion
|
|
325
|
-
|
|
326
|
-
init_auth();
|
|
327
|
-
|
|
328
|
-
//#endregion
|
|
329
|
-
export { ApiError, ApiErrorSchema, ApiKeyAuth, BaseEndpoint, BearerTokenAuth, Envelope, ErrorDetail, HTTPMethod, HttpClient, Id, Meta, NoAuth, PaginationSchema, Timestamps, parseOrThrow, safeParse, toQueryString };
|
|
870
|
+
export { ApiError, ApiErrorSchema, ApiKeyAuth, BaseEndpoint, BearerTokenAuth, ConsoleLogger, ConsoleMetricsCollector, Envelope, ErrorDetail, HTTPMethod, HttpClient, Id, InMemoryMetricsCollector, LogLevel, LoggerUtil, Meta, NoAuth, NoOpLogger, NoOpMetricsCollector, PaginationSchema, Timestamps, parseOrThrow, safeParse, toQueryString };
|
|
330
871
|
//# sourceMappingURL=index.js.map
|