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