@fgrzl/fetch 1.1.0 → 1.3.0-alpha.3

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.d.ts CHANGED
@@ -59,6 +59,20 @@ interface FetchClientOptions {
59
59
  * - 'omit': Never send cookies
60
60
  */
61
61
  credentials?: RequestCredentials;
62
+ /**
63
+ * Base URL for relative requests.
64
+ *
65
+ * When set, all relative URLs (not starting with http:// or https://) will be
66
+ * prefixed with this base URL. Absolute URLs are used as-is.
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const client = new FetchClient({ baseUrl: 'https://api.example.com' });
71
+ * await client.get('/users'); // → GET https://api.example.com/users
72
+ * await client.get('https://other-api.com/data'); // → GET https://other-api.com/data
73
+ * ```
74
+ */
75
+ baseUrl?: string;
62
76
  }
63
77
 
64
78
  /**
@@ -115,12 +129,45 @@ type FetchMiddleware = (request: RequestInit & {
115
129
  declare class FetchClient {
116
130
  private middlewares;
117
131
  private credentials;
132
+ private baseUrl;
118
133
  constructor(config?: FetchClientOptions);
119
134
  use(middleware: FetchMiddleware): this;
135
+ /**
136
+ * Set or update the base URL for this client instance.
137
+ *
138
+ * When a base URL is set, relative URLs will be resolved against it.
139
+ * Absolute URLs will continue to work unchanged.
140
+ *
141
+ * @param baseUrl - The base URL to set, or undefined to clear it
142
+ * @returns The client instance for method chaining
143
+ *
144
+ * @example Set base URL:
145
+ * ```typescript
146
+ * const client = new FetchClient();
147
+ * client.setBaseUrl('https://api.example.com');
148
+ *
149
+ * // Now relative URLs work
150
+ * await client.get('/users'); // → GET https://api.example.com/users
151
+ * ```
152
+ *
153
+ * @example Chain with middleware:
154
+ * ```typescript
155
+ * const client = useProductionStack(new FetchClient())
156
+ * .setBaseUrl(process.env.API_BASE_URL);
157
+ * ```
158
+ */
159
+ setBaseUrl(baseUrl?: string): this;
120
160
  request<T = unknown>(url: string, init?: RequestInit): Promise<FetchResponse<T>>;
121
161
  private coreFetch;
122
162
  private parseResponse;
123
163
  private buildUrlWithParams;
164
+ /**
165
+ * Resolves a URL with the base URL if it's relative and base URL is configured
166
+ * @param url - The URL to resolve
167
+ * @returns The resolved URL
168
+ * @private
169
+ */
170
+ private resolveUrl;
124
171
  /**
125
172
  * HEAD request with query parameter support.
126
173
  *
@@ -314,6 +361,93 @@ declare class NetworkError extends FetchError {
314
361
  constructor(message: string, url: string, cause?: Error);
315
362
  }
316
363
 
364
+ /**
365
+ * @fileoverview Query parameter utilities for FetchClient
366
+ *
367
+ * Provides utilities for building URL query strings from JavaScript objects
368
+ * with proper handling of arrays, undefined values, and URL encoding.
369
+ */
370
+ /**
371
+ * Query parameter value types that can be converted to URL parameters.
372
+ * Arrays are handled specially with multiple parameter entries.
373
+ */
374
+ type QueryValue = string | number | boolean | null | undefined | QueryValue[];
375
+ /**
376
+ * Object representing query parameters for URL construction.
377
+ */
378
+ type QueryParams = Record<string, QueryValue>;
379
+ /**
380
+ * Builds a URL query string from a JavaScript object.
381
+ *
382
+ * Features:
383
+ * - ✅ Proper URL encoding via native URLSearchParams
384
+ * - ✅ Array handling with multiple parameter entries
385
+ * - ✅ Undefined value filtering (excluded from output)
386
+ * - ✅ Type coercion to strings for all values
387
+ *
388
+ * @param query - Object containing query parameters
389
+ * @returns URL-encoded query string (without leading '?')
390
+ *
391
+ * @example Basic usage:
392
+ * ```typescript
393
+ * buildQueryParams({ name: 'John', age: 30 })
394
+ * // → "name=John&age=30"
395
+ * ```
396
+ *
397
+ * @example Array handling:
398
+ * ```typescript
399
+ * buildQueryParams({ tags: ['typescript', 'javascript'], active: true })
400
+ * // → "tags=typescript&tags=javascript&active=true"
401
+ * ```
402
+ *
403
+ * @example Undefined filtering:
404
+ * ```typescript
405
+ * buildQueryParams({ name: 'John', email: undefined, age: null })
406
+ * // → "name=John&age=null" (undefined excluded, null converted to string)
407
+ * ```
408
+ *
409
+ * @example Real-world API usage:
410
+ * ```typescript
411
+ * const filters = {
412
+ * status: ['active', 'pending'],
413
+ * limit: 50,
414
+ * offset: 0,
415
+ * search: searchTerm || undefined // Conditionally included
416
+ * };
417
+ *
418
+ * const queryString = buildQueryParams(filters);
419
+ * // → "status=active&status=pending&limit=50&offset=0"
420
+ * // (search excluded if searchTerm is undefined)
421
+ * ```
422
+ */
423
+ declare function buildQueryParams(query: QueryParams): string;
424
+ /**
425
+ * Appends query parameters to a URL, handling existing query strings properly.
426
+ *
427
+ * @param baseUrl - The base URL to append parameters to
428
+ * @param query - Object containing query parameters
429
+ * @returns Complete URL with query parameters appended
430
+ *
431
+ * @example Basic URL building:
432
+ * ```typescript
433
+ * appendQueryParams('/api/users', { limit: 10, active: true })
434
+ * // → "/api/users?limit=10&active=true"
435
+ * ```
436
+ *
437
+ * @example Existing query parameters:
438
+ * ```typescript
439
+ * appendQueryParams('/api/users?sort=name', { limit: 10 })
440
+ * // → "/api/users?sort=name&limit=10"
441
+ * ```
442
+ *
443
+ * @example Empty query object:
444
+ * ```typescript
445
+ * appendQueryParams('/api/users', {})
446
+ * // → "/api/users" (no change)
447
+ * ```
448
+ */
449
+ declare function appendQueryParams(baseUrl: string, query: QueryParams): string;
450
+
317
451
  /**
318
452
  * @fileoverview Authentication middleware types and configuration.
319
453
  */
@@ -1410,6 +1544,18 @@ declare function useBasicStack(client: FetchClient, config: {
1410
1544
  * const activeUsers = await api.get('/api/users', { status: 'active', limit: 10 });
1411
1545
  * ```
1412
1546
  *
1547
+ * @example Set base URL for API-specific clients:
1548
+ * ```typescript
1549
+ * import api from '@fgrzl/fetch';
1550
+ *
1551
+ * // Configure base URL dynamically
1552
+ * api.setBaseUrl('https://api.example.com');
1553
+ *
1554
+ * // Now all relative URLs are prefixed automatically
1555
+ * const users = await api.get('/users'); // → GET https://api.example.com/users
1556
+ * const posts = await api.get('/posts'); // → GET https://api.example.com/posts
1557
+ * ```
1558
+ *
1413
1559
  * @example Configure authentication:
1414
1560
  * ```typescript
1415
1561
  * import api from '@fgrzl/fetch';
@@ -1430,7 +1576,22 @@ declare function useBasicStack(client: FetchClient, config: {
1430
1576
  * tokenProvider: () => getJWTToken()
1431
1577
  * });
1432
1578
  * ```
1579
+ *
1580
+ * @example Production-ready API client with base URL:
1581
+ * ```typescript
1582
+ * import { FetchClient, useProductionStack } from '@fgrzl/fetch';
1583
+ *
1584
+ * // One-liner production setup with base URL
1585
+ * const apiClient = useProductionStack(new FetchClient(), {
1586
+ * auth: { tokenProvider: () => getAuthToken() },
1587
+ * retry: { maxRetries: 3 },
1588
+ * logging: { level: 'info' }
1589
+ * }).setBaseUrl(process.env.API_BASE_URL || 'https://api.example.com');
1590
+ *
1591
+ * // Ready to use with full production features
1592
+ * const users = await apiClient.get('/users');
1593
+ * ```
1433
1594
  */
1434
1595
  declare const api: FetchClient;
1435
1596
 
1436
- export { type AuthTokenProvider, type AuthenticationOptions, type AuthorizationOptions, type CacheEntry, type CacheKeyGenerator, type CacheOptions, type CacheStorage, FetchClient, type FetchClientOptions, FetchError, type FetchResponse, HttpError, type FetchMiddleware as InterceptMiddleware, type LogLevel, type Logger, type LoggingOptions, NetworkError, type RateLimitAlgorithm, type RateLimitOptions, type RetryOptions, type UnauthorizedHandler, createAuthenticationMiddleware, createAuthorizationMiddleware, createCacheMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRetryMiddleware, api as default, useAuthentication, useAuthorization, useBasicStack, useCSRF, useCache, useDevelopmentStack, useLogging, useProductionStack, useRateLimit, useRetry };
1597
+ export { type AuthTokenProvider, type AuthenticationOptions, type AuthorizationOptions, type CacheEntry, type CacheKeyGenerator, type CacheOptions, type CacheStorage, FetchClient, type FetchClientOptions, FetchError, type FetchResponse, HttpError, type FetchMiddleware as InterceptMiddleware, type LogLevel, type Logger, type LoggingOptions, NetworkError, type QueryParams, type QueryValue, type RateLimitAlgorithm, type RateLimitOptions, type RetryOptions, type UnauthorizedHandler, appendQueryParams, buildQueryParams, createAuthenticationMiddleware, createAuthorizationMiddleware, createCacheMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRetryMiddleware, api as default, useAuthentication, useAuthorization, useBasicStack, useCSRF, useCache, useDevelopmentStack, useLogging, useProductionStack, useRateLimit, useRetry };
package/dist/index.js CHANGED
@@ -3,16 +3,46 @@ var FetchClient = class {
3
3
  constructor(config = {}) {
4
4
  this.middlewares = [];
5
5
  this.credentials = config.credentials ?? "same-origin";
6
+ this.baseUrl = config.baseUrl;
6
7
  }
7
8
  use(middleware) {
8
9
  this.middlewares.push(middleware);
9
10
  return this;
10
11
  }
12
+ /**
13
+ * Set or update the base URL for this client instance.
14
+ *
15
+ * When a base URL is set, relative URLs will be resolved against it.
16
+ * Absolute URLs will continue to work unchanged.
17
+ *
18
+ * @param baseUrl - The base URL to set, or undefined to clear it
19
+ * @returns The client instance for method chaining
20
+ *
21
+ * @example Set base URL:
22
+ * ```typescript
23
+ * const client = new FetchClient();
24
+ * client.setBaseUrl('https://api.example.com');
25
+ *
26
+ * // Now relative URLs work
27
+ * await client.get('/users'); // → GET https://api.example.com/users
28
+ * ```
29
+ *
30
+ * @example Chain with middleware:
31
+ * ```typescript
32
+ * const client = useProductionStack(new FetchClient())
33
+ * .setBaseUrl(process.env.API_BASE_URL);
34
+ * ```
35
+ */
36
+ setBaseUrl(baseUrl) {
37
+ this.baseUrl = baseUrl;
38
+ return this;
39
+ }
11
40
  async request(url, init = {}) {
41
+ const resolvedUrl = this.resolveUrl(url);
12
42
  let index = 0;
13
43
  const execute = async (request) => {
14
- const currentRequest = request || { ...init, url };
15
- const currentUrl = currentRequest.url || url;
44
+ const currentRequest = request || { ...init, url: resolvedUrl };
45
+ const currentUrl = currentRequest.url || resolvedUrl;
16
46
  if (index >= this.middlewares.length) {
17
47
  const { url: _, ...requestInit } = currentRequest;
18
48
  return this.coreFetch(requestInit, currentUrl);
@@ -96,20 +126,48 @@ var FetchClient = class {
96
126
  if (!params) {
97
127
  return url;
98
128
  }
99
- const urlObj = new URL(
100
- url,
101
- url.startsWith("http") ? void 0 : "http://localhost"
102
- );
129
+ const resolvedUrl = this.resolveUrl(url);
130
+ if (!resolvedUrl.startsWith("http://") && !resolvedUrl.startsWith("https://") && !resolvedUrl.startsWith("//")) {
131
+ const searchParams = new URLSearchParams();
132
+ Object.entries(params).forEach(([key, value]) => {
133
+ if (value !== void 0 && value !== null) {
134
+ searchParams.set(key, String(value));
135
+ }
136
+ });
137
+ const queryString = searchParams.toString();
138
+ return queryString ? `${resolvedUrl}?${queryString}` : resolvedUrl;
139
+ }
140
+ const urlObj = new URL(resolvedUrl);
103
141
  Object.entries(params).forEach(([key, value]) => {
104
142
  if (value !== void 0 && value !== null) {
105
143
  urlObj.searchParams.set(key, String(value));
106
144
  }
107
145
  });
108
- if (!url.startsWith("http")) {
109
- return urlObj.pathname + urlObj.search;
110
- }
111
146
  return urlObj.toString();
112
147
  }
148
+ /**
149
+ * Resolves a URL with the base URL if it's relative and base URL is configured
150
+ * @param url - The URL to resolve
151
+ * @returns The resolved URL
152
+ * @private
153
+ */
154
+ resolveUrl(url) {
155
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
156
+ return url;
157
+ }
158
+ if (!this.baseUrl) {
159
+ return url;
160
+ }
161
+ try {
162
+ const baseUrl = new URL(this.baseUrl);
163
+ const resolvedUrl = new URL(url, baseUrl);
164
+ return resolvedUrl.toString();
165
+ } catch {
166
+ throw new Error(
167
+ `Invalid URL: Unable to resolve "${url}" with baseUrl "${this.baseUrl}"`
168
+ );
169
+ }
170
+ }
113
171
  // 🎯 PIT OF SUCCESS: Convenience methods with smart defaults
114
172
  /**
115
173
  * HEAD request with query parameter support.
@@ -1022,6 +1080,40 @@ var NetworkError = class extends FetchError {
1022
1080
  }
1023
1081
  };
1024
1082
 
1083
+ // src/client/query.ts
1084
+ function buildQueryParams(query) {
1085
+ const params = new URLSearchParams();
1086
+ for (const [key, value] of Object.entries(query)) {
1087
+ if (value !== void 0) {
1088
+ if (Array.isArray(value)) {
1089
+ value.forEach((item) => {
1090
+ if (item !== void 0) {
1091
+ params.append(key, String(item));
1092
+ }
1093
+ });
1094
+ } else {
1095
+ params.set(key, String(value));
1096
+ }
1097
+ }
1098
+ }
1099
+ return params.toString();
1100
+ }
1101
+ function appendQueryParams(baseUrl, query) {
1102
+ const queryString = buildQueryParams(query);
1103
+ if (!queryString) {
1104
+ return baseUrl;
1105
+ }
1106
+ const fragmentIndex = baseUrl.indexOf("#");
1107
+ if (fragmentIndex !== -1) {
1108
+ const urlPart = baseUrl.substring(0, fragmentIndex);
1109
+ const fragmentPart = baseUrl.substring(fragmentIndex);
1110
+ const separator2 = urlPart.includes("?") ? "&" : "?";
1111
+ return `${urlPart}${separator2}${queryString}${fragmentPart}`;
1112
+ }
1113
+ const separator = baseUrl.includes("?") ? "&" : "?";
1114
+ return `${baseUrl}${separator}${queryString}`;
1115
+ }
1116
+
1025
1117
  // src/index.ts
1026
1118
  var api = useProductionStack(
1027
1119
  new FetchClient({
@@ -1056,6 +1148,8 @@ export {
1056
1148
  FetchError,
1057
1149
  HttpError,
1058
1150
  NetworkError,
1151
+ appendQueryParams,
1152
+ buildQueryParams,
1059
1153
  createAuthenticationMiddleware,
1060
1154
  createAuthorizationMiddleware,
1061
1155
  createCacheMiddleware,