@beyonk/http 12.1.1 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,464 @@
1
+ // lib/index.js
2
+ var HttpError = class extends Error {
3
+ /**
4
+ * Create a new HTTP error
5
+ * @param {string} message - Error message
6
+ * @param {any} body - Error response body
7
+ */
8
+ constructor(message, body) {
9
+ super(message);
10
+ this.body = body;
11
+ }
12
+ };
13
+ var AccessDeniedError = class extends HttpError {
14
+ };
15
+ var PaymentRequiredError = class extends HttpError {
16
+ };
17
+ var ForbiddenError = class extends HttpError {
18
+ };
19
+ var NotFoundError = class extends HttpError {
20
+ };
21
+ var NotAcceptableError = class extends HttpError {
22
+ };
23
+ var ConflictError = class extends HttpError {
24
+ };
25
+ var GoneError = class extends HttpError {
26
+ };
27
+ var PreconditionFailedError = class extends HttpError {
28
+ };
29
+ var ExpectationFailedError = class extends HttpError {
30
+ };
31
+ var BadDataError = class extends HttpError {
32
+ };
33
+ var TooManyRequestsError = class extends HttpError {
34
+ };
35
+ var errorMapping = {
36
+ 401: AccessDeniedError,
37
+ 402: PaymentRequiredError,
38
+ 403: ForbiddenError,
39
+ 404: NotFoundError,
40
+ 406: NotAcceptableError,
41
+ 409: ConflictError,
42
+ 410: GoneError,
43
+ 412: PreconditionFailedError,
44
+ 417: ExpectationFailedError,
45
+ 422: BadDataError,
46
+ 429: TooManyRequestsError
47
+ };
48
+ function simpleClone(obj) {
49
+ return JSON.parse(JSON.stringify(obj));
50
+ }
51
+ function getErrorByCode(code) {
52
+ return errorMapping[code] || HttpError;
53
+ }
54
+ var DEFAULT_CONFIG = {
55
+ endpoint: null,
56
+ method: "GET",
57
+ payload: null,
58
+ query: null,
59
+ headers: {},
60
+ overrides: {}
61
+ };
62
+ var Api = class {
63
+ config = simpleClone(DEFAULT_CONFIG);
64
+ /** @type {Record<string, ErrorHandler>} */
65
+ handlers = {};
66
+ /** @type {FetchClient|null} */
67
+ client = null;
68
+ /** @type {ApiContext|null} */
69
+ ctx = null;
70
+ /** @type {ErrorHandler|null} */
71
+ defaultHandler = null;
72
+ /**
73
+ * Creates a new API instance
74
+ * @param {ApiOptions} options - API configuration options
75
+ */
76
+ constructor(options) {
77
+ this.options = Object.assign({
78
+ retry: false,
79
+ parseErrors: true,
80
+ handlers: {}
81
+ }, options);
82
+ }
83
+ /**
84
+ * Reset the request configuration to defaults
85
+ */
86
+ resetRequest() {
87
+ this.config = simpleClone(DEFAULT_CONFIG);
88
+ }
89
+ /**
90
+ * Get the HTTP client to use for requests
91
+ * @returns {FetchClient} HTTP client
92
+ * @throws {Error} If no client is available
93
+ */
94
+ getClient() {
95
+ if (this.options.mock) {
96
+ console.warn("@beyonk/http: Using mocked http client");
97
+ return this.options.mock;
98
+ }
99
+ if (this.client) {
100
+ return this.client;
101
+ }
102
+ if (config?.fetch) {
103
+ return config.fetch;
104
+ }
105
+ if ("fetch" in globalThis) {
106
+ return globalThis.fetch;
107
+ }
108
+ throw Error("No client provided and can't find one automatically");
109
+ }
110
+ /**
111
+ * Handle an error
112
+ * @param {HttpError} e - Error instance
113
+ * @param {ApiContext} [ctx] - API context
114
+ * @returns {any} Result of error handler
115
+ */
116
+ handle(e, ctx) {
117
+ const constructorName = Object.getPrototypeOf(e).constructor.name;
118
+ const globalHandlerName = `${constructorName[0].toLowerCase()}${constructorName.slice(1, -5)}`;
119
+ const handler = this.handlers[constructorName] || this.options.handlers && this.options.handlers[globalHandlerName] || this.defaultHandler || ((e2) => {
120
+ console.error(constructorName, e2.message, e2);
121
+ });
122
+ return handler(e, ctx);
123
+ }
124
+ /**
125
+ * Send the HTTP request
126
+ * @template T
127
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
128
+ * @returns {Promise<T>} Response data
129
+ */
130
+ async send(fn) {
131
+ const endpoint = this.config.endpoint?.includes("://") ? this.config.endpoint : `${this.options.baseUrl}/${this.config.endpoint}`;
132
+ const hasPayload = !!this.config.payload;
133
+ const options = Object.assign(
134
+ {
135
+ method: this.config.method,
136
+ cors: true,
137
+ credentials: "include",
138
+ headers: Object.assign(
139
+ { Accept: "application/json" },
140
+ hasPayload ? { "Content-Type": "application/json" } : {},
141
+ this.config.headers
142
+ )
143
+ },
144
+ hasPayload ? { body: JSON.stringify(this.config.payload) } : {},
145
+ this.config.overrides
146
+ );
147
+ const client = this.getClient();
148
+ const ep = this.config.query ? `${endpoint}?${this.config.query}` : `${endpoint}`;
149
+ let result;
150
+ try {
151
+ result = await this.#doQuery(1, client, ep, options);
152
+ } catch (e) {
153
+ console.log(e);
154
+ return this.handle(e, this.ctx);
155
+ } finally {
156
+ this.resetRequest();
157
+ }
158
+ const { json, httpStatus } = result;
159
+ return fn ? fn(json, httpStatus) : json;
160
+ }
161
+ /**
162
+ * Check if the response has content
163
+ * @param {import('./types.js').Response} response - HTTP response
164
+ * @returns {boolean} Whether the response has content
165
+ */
166
+ #hasContent(response) {
167
+ const contentLength = parseInt(response.headers.get("content-length") ?? "", 10);
168
+ const isNoContentResponse = response.status === 204;
169
+ if (isNoContentResponse) {
170
+ return false;
171
+ }
172
+ const headerExists = !isNaN(contentLength);
173
+ return !headerExists || contentLength > 0;
174
+ }
175
+ /**
176
+ * Perform the HTTP request with retry logic
177
+ * @param {number} attempt - Current attempt number
178
+ * @param {FetchClient} client - HTTP client
179
+ * @param {string} endpoint - API endpoint URL
180
+ * @param {Record<string, any>} options - Request options
181
+ * @returns {Promise<QueryResult>} Response data
182
+ * @throws {HttpError} If the request fails
183
+ */
184
+ async #doQuery(attempt, client, endpoint, options) {
185
+ const retry = this.options.retry || { attempts: 1 };
186
+ try {
187
+ const r = await client(endpoint, options);
188
+ if (r.status >= 200 && r.status < 400) {
189
+ let json;
190
+ if (this.#hasContent(r)) {
191
+ try {
192
+ json = await r.json();
193
+ } catch (e) {
194
+ console.error("Unable to parse response json", e.message);
195
+ }
196
+ }
197
+ return { httpStatus: r.status, json };
198
+ }
199
+ let content = "";
200
+ try {
201
+ content = this.options.parseErrors ? await r.json() : await r.text();
202
+ } catch (e) {
203
+ console.log("Failed to parse error body when asked.");
204
+ }
205
+ const ClientError = getErrorByCode(r.status);
206
+ throw new ClientError(r.statusText, content);
207
+ } catch (e) {
208
+ if (retry.attempts && retry.errors && attempt < retry.attempts && retry.errors.includes(e.code)) {
209
+ console.warn(`Got ${e.code} when calling ${endpoint}. Retrying request (${attempt}/${retry.attempts})`);
210
+ return this.#doQuery(++attempt, client, endpoint, options);
211
+ }
212
+ throw e;
213
+ }
214
+ }
215
+ /**
216
+ * Set the context for the request
217
+ * @param {ApiContext} ctx - Request context
218
+ * @returns {this} Current instance
219
+ */
220
+ context(ctx) {
221
+ if (ctx.fetch) {
222
+ this.client = ctx.fetch;
223
+ }
224
+ this.ctx = ctx;
225
+ return this;
226
+ }
227
+ /**
228
+ * Set request overrides
229
+ * @param {Record<string, any>} override - Request overrides
230
+ * @returns {this} Current instance
231
+ */
232
+ override(override) {
233
+ this.config.overrides = override;
234
+ return this;
235
+ }
236
+ /**
237
+ * Set request headers
238
+ * @param {Record<string, string>} headers - Request headers
239
+ * @returns {this} Current instance
240
+ */
241
+ headers(headers) {
242
+ this.config.headers = headers;
243
+ return this;
244
+ }
245
+ /**
246
+ * Perform a GET request
247
+ * @template T
248
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
249
+ * @returns {Promise<T>} Response data
250
+ */
251
+ async get(fn) {
252
+ this.config.method = "GET";
253
+ return this.send(fn);
254
+ }
255
+ /**
256
+ * Perform a POST request
257
+ * @template T
258
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
259
+ * @returns {Promise<T>} Response data
260
+ */
261
+ async post(fn) {
262
+ this.config.method = "POST";
263
+ return this.send(fn);
264
+ }
265
+ /**
266
+ * Perform a PATCH request
267
+ * @template T
268
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
269
+ * @returns {Promise<T>} Response data
270
+ */
271
+ async patch(fn) {
272
+ this.config.method = "PATCH";
273
+ return this.send(fn);
274
+ }
275
+ /**
276
+ * Perform a PUT request
277
+ * @template T
278
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
279
+ * @returns {Promise<T>} Response data
280
+ */
281
+ async put(fn) {
282
+ this.config.method = "PUT";
283
+ return this.send(fn);
284
+ }
285
+ /**
286
+ * Perform a DELETE request
287
+ * @template T
288
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
289
+ * @returns {Promise<T>} Response data
290
+ */
291
+ async del(fn) {
292
+ this.config.method = "DELETE";
293
+ return this.send(fn);
294
+ }
295
+ /**
296
+ * Set the API endpoint
297
+ * @param {string} endpoint - API endpoint
298
+ * @returns {this} Current instance
299
+ */
300
+ endpoint(endpoint) {
301
+ this.config.endpoint = endpoint;
302
+ return this;
303
+ }
304
+ /**
305
+ * Set query parameters
306
+ * @param {Record<string, any>} query - Query parameters
307
+ * @returns {this} Current instance
308
+ */
309
+ query(query) {
310
+ const q = Object.entries(query).reduce(
311
+ (curr, [k, v]) => {
312
+ if (typeof v === "undefined") {
313
+ return curr;
314
+ }
315
+ if (Array.isArray(v)) {
316
+ curr.push(...v.map((n) => `${k}=${encodeURIComponent(n)}`));
317
+ } else {
318
+ curr.push(`${k}=${encodeURIComponent(v)}`);
319
+ }
320
+ return curr;
321
+ },
322
+ /** @type {string[]} */
323
+ []
324
+ );
325
+ this.config.query = q.join("&");
326
+ return this;
327
+ }
328
+ /**
329
+ * Set request payload
330
+ * @param {any} payload - Request payload
331
+ * @returns {this} Current instance
332
+ */
333
+ payload(payload) {
334
+ this.config.payload = payload;
335
+ return this;
336
+ }
337
+ /**
338
+ * Register a default error handler
339
+ * @param {ErrorHandler} fn - Error handler function
340
+ * @returns {this} Current instance
341
+ */
342
+ default(fn) {
343
+ this.defaultHandler = fn;
344
+ return this;
345
+ }
346
+ /**
347
+ * Register a handler for AccessDenied (401) errors
348
+ * @param {ErrorHandler} fn - Error handler function
349
+ * @returns {this} Current instance
350
+ */
351
+ accessDenied(fn) {
352
+ this.handlers[AccessDeniedError.name] = fn;
353
+ return this;
354
+ }
355
+ /**
356
+ * Register a handler for PaymentRequired (402) errors
357
+ * @param {ErrorHandler} fn - Error handler function
358
+ * @returns {this} Current instance
359
+ */
360
+ paymentRequired(fn) {
361
+ this.handlers[PaymentRequiredError.name] = fn;
362
+ return this;
363
+ }
364
+ /**
365
+ * Register a handler for Forbidden (403) errors
366
+ * @param {ErrorHandler} fn - Error handler function
367
+ * @returns {this} Current instance
368
+ */
369
+ forbidden(fn) {
370
+ this.handlers[ForbiddenError.name] = fn;
371
+ return this;
372
+ }
373
+ /**
374
+ * Register a handler for NotFound (404) errors
375
+ * @param {ErrorHandler} fn - Error handler function
376
+ * @returns {this} Current instance
377
+ */
378
+ notFound(fn) {
379
+ this.handlers[NotFoundError.name] = fn;
380
+ return this;
381
+ }
382
+ /**
383
+ * Register a handler for NotAcceptable (406) errors
384
+ * @param {ErrorHandler} fn - Error handler function
385
+ * @returns {this} Current instance
386
+ */
387
+ notAcceptable(fn) {
388
+ this.handlers[NotAcceptableError.name] = fn;
389
+ return this;
390
+ }
391
+ /**
392
+ * Register a handler for Conflict (409) errors
393
+ * @param {ErrorHandler} fn - Error handler function
394
+ * @returns {this} Current instance
395
+ */
396
+ conflict(fn) {
397
+ this.handlers[ConflictError.name] = fn;
398
+ return this;
399
+ }
400
+ /**
401
+ * Register a handler for Gone (410) errors
402
+ * @param {ErrorHandler} fn - Error handler function
403
+ * @returns {this} Current instance
404
+ */
405
+ gone(fn) {
406
+ this.handlers[GoneError.name] = fn;
407
+ return this;
408
+ }
409
+ /**
410
+ * Register a handler for PreconditionFailed (412) errors
411
+ * @param {ErrorHandler} fn - Error handler function
412
+ * @returns {this} Current instance
413
+ */
414
+ preconditionFailed(fn) {
415
+ this.handlers[PreconditionFailedError.name] = fn;
416
+ return this;
417
+ }
418
+ /**
419
+ * Register a handler for ExpectationFailed (417) errors
420
+ * @param {ErrorHandler} fn - Error handler function
421
+ * @returns {this} Current instance
422
+ */
423
+ expectationFailed(fn) {
424
+ this.handlers[ExpectationFailedError.name] = fn;
425
+ return this;
426
+ }
427
+ /**
428
+ * Register a handler for BadData (422) errors
429
+ * @param {ErrorHandler} fn - Error handler function
430
+ * @returns {this} Current instance
431
+ */
432
+ badData(fn) {
433
+ this.handlers[BadDataError.name] = fn;
434
+ return this;
435
+ }
436
+ /**
437
+ * Register a handler for TooManyRequests (429) errors
438
+ * @param {ErrorHandler} fn - Error handler function
439
+ * @returns {this} Current instance
440
+ */
441
+ tooManyRequests(fn) {
442
+ this.handlers[TooManyRequestsError.name] = fn;
443
+ return this;
444
+ }
445
+ };
446
+ var config;
447
+ function configure(options) {
448
+ config = options;
449
+ }
450
+ function create() {
451
+ if (!config) {
452
+ throw new Error("Api client must be configured");
453
+ }
454
+ return new Api(config);
455
+ }
456
+ var index_default = {
457
+ create,
458
+ configure
459
+ };
460
+ export {
461
+ Api,
462
+ HttpError,
463
+ index_default as default
464
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beyonk/http",
3
- "version": "12.1.1",
3
+ "version": "12.2.0",
4
4
  "description": "An isomorphic http client for Svelte apps",
5
5
  "type": "module",
6
6
  "repository": {