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/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
- var ApiError = class extends Error {
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
- var auth_exports = /* @__PURE__ */ __export({
78
- ApiKeyAuth: () => ApiKeyAuth,
79
- BearerTokenAuth: () => BearerTokenAuth,
80
- NoAuth: () => NoAuth
81
- });
82
- var NoAuth, ApiKeyAuth, BearerTokenAuth;
83
- var init_auth = __esm({ "lib/auth.ts": (() => {
84
- NoAuth = class {
85
- async apply() {}
86
- };
87
- ApiKeyAuth = class {
88
- constructor(opts) {
89
- this.opts = opts;
90
- }
91
- apply({ url, init }) {
92
- if (this.opts.header) init.headers = {
93
- ...init.headers,
94
- [this.opts.header]: this.opts.value
95
- };
96
- else if (this.opts.query) {
97
- const u = new URL(url);
98
- u.searchParams.set(this.opts.query, this.opts.value);
99
- init.__urlOverride = u.toString();
100
- }
101
- }
102
- };
103
- BearerTokenAuth = class {
104
- constructor(getToken) {
105
- this.getToken = getToken;
106
- }
107
- async apply({ init }) {
108
- const token = await this.getToken();
109
- init.headers = {
110
- ...init.headers,
111
- Authorization: `Bearer ${token}`
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.auth = opts["auth"] ?? new (init_auth(), __toCommonJS(auth_exports)).NoAuth();
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) throw new Error(`Unknown baseUrl key: ${k}`);
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(() => controller.abort(new ApiError("Request timeout", { status: 0 })), this.timeoutMs);
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?.name === "AbortError") return false;
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 === "GET" || this.method === "HEAD" ? void 0 : args;
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: (this.method === "GET" ? args?.query : void 0) ?? options?.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.iso.datetime(),
301
- updatedAt: z.iso.datetime()
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.iso.datetime().optional(),
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
- //#region lib/index.ts
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