@beyonk/http 12.0.1 → 12.1.2

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,460 @@
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
+ } else if (typeof window !== "undefined") {
102
+ return window.fetch.bind(window);
103
+ }
104
+ throw Error("No client provided and can't find one automatically");
105
+ }
106
+ /**
107
+ * Handle an error
108
+ * @param {HttpError} e - Error instance
109
+ * @param {ApiContext} [ctx] - API context
110
+ * @returns {any} Result of error handler
111
+ */
112
+ handle(e, ctx) {
113
+ const constructorName = Object.getPrototypeOf(e).constructor.name;
114
+ const globalHandlerName = `${constructorName[0].toLowerCase()}${constructorName.slice(1, -5)}`;
115
+ const handler = this.handlers[constructorName] || this.options.handlers && this.options.handlers[globalHandlerName] || this.defaultHandler || ((e2) => {
116
+ console.error(constructorName, e2.message, e2);
117
+ });
118
+ return handler(e, ctx);
119
+ }
120
+ /**
121
+ * Send the HTTP request
122
+ * @template T
123
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
124
+ * @returns {Promise<T>} Response data
125
+ */
126
+ async send(fn) {
127
+ const endpoint = this.config.endpoint?.includes("://") ? this.config.endpoint : `${this.options.baseUrl}/${this.config.endpoint}`;
128
+ const hasPayload = !!this.config.payload;
129
+ const options = Object.assign(
130
+ {
131
+ method: this.config.method,
132
+ cors: true,
133
+ credentials: "include",
134
+ headers: Object.assign(
135
+ { Accept: "application/json" },
136
+ hasPayload ? { "Content-Type": "application/json" } : {},
137
+ this.config.headers
138
+ )
139
+ },
140
+ hasPayload ? { body: JSON.stringify(this.config.payload) } : {},
141
+ this.config.overrides
142
+ );
143
+ const client = this.getClient();
144
+ const ep = this.config.query ? `${endpoint}?${this.config.query}` : `${endpoint}`;
145
+ let result;
146
+ try {
147
+ result = await this.#doQuery(1, client, ep, options);
148
+ } catch (e) {
149
+ console.log(e);
150
+ return this.handle(e, this.ctx);
151
+ } finally {
152
+ this.resetRequest();
153
+ }
154
+ const { json, httpStatus } = result;
155
+ return fn ? fn(json, httpStatus) : json;
156
+ }
157
+ /**
158
+ * Check if the response has content
159
+ * @param {import('./types.js').Response} response - HTTP response
160
+ * @returns {boolean} Whether the response has content
161
+ */
162
+ #hasContent(response) {
163
+ const contentLength = parseInt(response.headers.get("content-length") ?? "", 10);
164
+ const isNoContentResponse = response.status === 204;
165
+ if (isNoContentResponse) {
166
+ return false;
167
+ }
168
+ const headerExists = !isNaN(contentLength);
169
+ return !headerExists || contentLength > 0;
170
+ }
171
+ /**
172
+ * Perform the HTTP request with retry logic
173
+ * @param {number} attempt - Current attempt number
174
+ * @param {FetchClient} client - HTTP client
175
+ * @param {string} endpoint - API endpoint URL
176
+ * @param {Record<string, any>} options - Request options
177
+ * @returns {Promise<QueryResult>} Response data
178
+ * @throws {HttpError} If the request fails
179
+ */
180
+ async #doQuery(attempt, client, endpoint, options) {
181
+ const retry = this.options.retry || { attempts: 1 };
182
+ try {
183
+ const r = await client(endpoint, options);
184
+ if (r.status >= 200 && r.status < 400) {
185
+ let json;
186
+ if (this.#hasContent(r)) {
187
+ try {
188
+ json = await r.json();
189
+ } catch (e) {
190
+ console.error("Unable to parse response json", e.message);
191
+ }
192
+ }
193
+ return { httpStatus: r.status, json };
194
+ }
195
+ let content = "";
196
+ try {
197
+ content = this.options.parseErrors ? await r.json() : await r.text();
198
+ } catch (e) {
199
+ console.log("Failed to parse error body when asked.");
200
+ }
201
+ const ClientError = getErrorByCode(r.status);
202
+ throw new ClientError(r.statusText, content);
203
+ } catch (e) {
204
+ if (retry.attempts && retry.errors && attempt < retry.attempts && retry.errors.includes(e.code)) {
205
+ console.warn(`Got ${e.code} when calling ${endpoint}. Retrying request (${attempt}/${retry.attempts})`);
206
+ return this.#doQuery(++attempt, client, endpoint, options);
207
+ }
208
+ throw e;
209
+ }
210
+ }
211
+ /**
212
+ * Set the context for the request
213
+ * @param {ApiContext} ctx - Request context
214
+ * @returns {this} Current instance
215
+ */
216
+ context(ctx) {
217
+ if (ctx.fetch) {
218
+ this.client = ctx.fetch;
219
+ }
220
+ this.ctx = ctx;
221
+ return this;
222
+ }
223
+ /**
224
+ * Set request overrides
225
+ * @param {Record<string, any>} override - Request overrides
226
+ * @returns {this} Current instance
227
+ */
228
+ override(override) {
229
+ this.config.overrides = override;
230
+ return this;
231
+ }
232
+ /**
233
+ * Set request headers
234
+ * @param {Record<string, string>} headers - Request headers
235
+ * @returns {this} Current instance
236
+ */
237
+ headers(headers) {
238
+ this.config.headers = headers;
239
+ return this;
240
+ }
241
+ /**
242
+ * Perform a GET request
243
+ * @template T
244
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
245
+ * @returns {Promise<T>} Response data
246
+ */
247
+ async get(fn) {
248
+ this.config.method = "GET";
249
+ return this.send(fn);
250
+ }
251
+ /**
252
+ * Perform a POST request
253
+ * @template T
254
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
255
+ * @returns {Promise<T>} Response data
256
+ */
257
+ async post(fn) {
258
+ this.config.method = "POST";
259
+ return this.send(fn);
260
+ }
261
+ /**
262
+ * Perform a PATCH request
263
+ * @template T
264
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
265
+ * @returns {Promise<T>} Response data
266
+ */
267
+ async patch(fn) {
268
+ this.config.method = "PATCH";
269
+ return this.send(fn);
270
+ }
271
+ /**
272
+ * Perform a PUT request
273
+ * @template T
274
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
275
+ * @returns {Promise<T>} Response data
276
+ */
277
+ async put(fn) {
278
+ this.config.method = "PUT";
279
+ return this.send(fn);
280
+ }
281
+ /**
282
+ * Perform a DELETE request
283
+ * @template T
284
+ * @param {ResponseTransformer<T>} [fn] - Function to transform the response
285
+ * @returns {Promise<T>} Response data
286
+ */
287
+ async del(fn) {
288
+ this.config.method = "DELETE";
289
+ return this.send(fn);
290
+ }
291
+ /**
292
+ * Set the API endpoint
293
+ * @param {string} endpoint - API endpoint
294
+ * @returns {this} Current instance
295
+ */
296
+ endpoint(endpoint) {
297
+ this.config.endpoint = endpoint;
298
+ return this;
299
+ }
300
+ /**
301
+ * Set query parameters
302
+ * @param {Record<string, any>} query - Query parameters
303
+ * @returns {this} Current instance
304
+ */
305
+ query(query) {
306
+ const q = Object.entries(query).reduce(
307
+ (curr, [k, v]) => {
308
+ if (typeof v === "undefined") {
309
+ return curr;
310
+ }
311
+ if (Array.isArray(v)) {
312
+ curr.push(...v.map((n) => `${k}=${encodeURIComponent(n)}`));
313
+ } else {
314
+ curr.push(`${k}=${encodeURIComponent(v)}`);
315
+ }
316
+ return curr;
317
+ },
318
+ /** @type {string[]} */
319
+ []
320
+ );
321
+ this.config.query = q.join("&");
322
+ return this;
323
+ }
324
+ /**
325
+ * Set request payload
326
+ * @param {any} payload - Request payload
327
+ * @returns {this} Current instance
328
+ */
329
+ payload(payload) {
330
+ this.config.payload = payload;
331
+ return this;
332
+ }
333
+ /**
334
+ * Register a default error handler
335
+ * @param {ErrorHandler} fn - Error handler function
336
+ * @returns {this} Current instance
337
+ */
338
+ default(fn) {
339
+ this.defaultHandler = fn;
340
+ return this;
341
+ }
342
+ /**
343
+ * Register a handler for AccessDenied (401) errors
344
+ * @param {ErrorHandler} fn - Error handler function
345
+ * @returns {this} Current instance
346
+ */
347
+ accessDenied(fn) {
348
+ this.handlers[AccessDeniedError.name] = fn;
349
+ return this;
350
+ }
351
+ /**
352
+ * Register a handler for PaymentRequired (402) errors
353
+ * @param {ErrorHandler} fn - Error handler function
354
+ * @returns {this} Current instance
355
+ */
356
+ paymentRequired(fn) {
357
+ this.handlers[PaymentRequiredError.name] = fn;
358
+ return this;
359
+ }
360
+ /**
361
+ * Register a handler for Forbidden (403) errors
362
+ * @param {ErrorHandler} fn - Error handler function
363
+ * @returns {this} Current instance
364
+ */
365
+ forbidden(fn) {
366
+ this.handlers[ForbiddenError.name] = fn;
367
+ return this;
368
+ }
369
+ /**
370
+ * Register a handler for NotFound (404) errors
371
+ * @param {ErrorHandler} fn - Error handler function
372
+ * @returns {this} Current instance
373
+ */
374
+ notFound(fn) {
375
+ this.handlers[NotFoundError.name] = fn;
376
+ return this;
377
+ }
378
+ /**
379
+ * Register a handler for NotAcceptable (406) errors
380
+ * @param {ErrorHandler} fn - Error handler function
381
+ * @returns {this} Current instance
382
+ */
383
+ notAcceptable(fn) {
384
+ this.handlers[NotAcceptableError.name] = fn;
385
+ return this;
386
+ }
387
+ /**
388
+ * Register a handler for Conflict (409) errors
389
+ * @param {ErrorHandler} fn - Error handler function
390
+ * @returns {this} Current instance
391
+ */
392
+ conflict(fn) {
393
+ this.handlers[ConflictError.name] = fn;
394
+ return this;
395
+ }
396
+ /**
397
+ * Register a handler for Gone (410) errors
398
+ * @param {ErrorHandler} fn - Error handler function
399
+ * @returns {this} Current instance
400
+ */
401
+ gone(fn) {
402
+ this.handlers[GoneError.name] = fn;
403
+ return this;
404
+ }
405
+ /**
406
+ * Register a handler for PreconditionFailed (412) errors
407
+ * @param {ErrorHandler} fn - Error handler function
408
+ * @returns {this} Current instance
409
+ */
410
+ preconditionFailed(fn) {
411
+ this.handlers[PreconditionFailedError.name] = fn;
412
+ return this;
413
+ }
414
+ /**
415
+ * Register a handler for ExpectationFailed (417) errors
416
+ * @param {ErrorHandler} fn - Error handler function
417
+ * @returns {this} Current instance
418
+ */
419
+ expectationFailed(fn) {
420
+ this.handlers[ExpectationFailedError.name] = fn;
421
+ return this;
422
+ }
423
+ /**
424
+ * Register a handler for BadData (422) errors
425
+ * @param {ErrorHandler} fn - Error handler function
426
+ * @returns {this} Current instance
427
+ */
428
+ badData(fn) {
429
+ this.handlers[BadDataError.name] = fn;
430
+ return this;
431
+ }
432
+ /**
433
+ * Register a handler for TooManyRequests (429) errors
434
+ * @param {ErrorHandler} fn - Error handler function
435
+ * @returns {this} Current instance
436
+ */
437
+ tooManyRequests(fn) {
438
+ this.handlers[TooManyRequestsError.name] = fn;
439
+ return this;
440
+ }
441
+ };
442
+ var config;
443
+ function configure(options) {
444
+ config = options;
445
+ }
446
+ function create() {
447
+ if (!config) {
448
+ throw new Error("Api client must be configured");
449
+ }
450
+ return new Api(config);
451
+ }
452
+ var index_default = {
453
+ create,
454
+ configure
455
+ };
456
+ export {
457
+ Api,
458
+ HttpError,
459
+ index_default as default
460
+ };
package/package.json CHANGED
@@ -1,24 +1,30 @@
1
1
  {
2
2
  "name": "@beyonk/http",
3
- "version": "12.0.1",
3
+ "version": "12.1.2",
4
4
  "description": "An isomorphic http client for Svelte apps",
5
- "main": "lib/entrypoint.js",
6
- "module": "lib/entrypoint.js",
7
5
  "type": "module",
8
- "directories": {
9
- "lib": "lib"
10
- },
11
6
  "repository": {
12
7
  "type": "git",
13
8
  "url": "https://github.com/beyonk-adventures/http.git"
14
9
  },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.cjs"
18
+ }
19
+ },
15
20
  "devDependencies": {
16
- "@beyonk/eslint-config": "^4.2.0",
17
- "@beyonk/esm": "^4.0.1",
21
+ "@beyonk/eslint-config": "^9.0.3",
18
22
  "@hapi/code": "^5.3.1",
19
- "eslint": "^7.2.0",
23
+ "eslint": "^9.29.0",
20
24
  "mocha": "^9.0.2",
21
- "sinon": "^7.5.0"
25
+ "sinon": "^7.5.0",
26
+ "tsup": "^8.4.0",
27
+ "typescript": "^5.8.3"
22
28
  },
23
29
  "eslintConfig": {
24
30
  "extends": "@beyonk",
@@ -45,9 +51,9 @@
45
51
  "volta": {
46
52
  "node": "18.15.0"
47
53
  },
48
- "packageManager": "pnpm@8.0.0",
49
54
  "scripts": {
50
55
  "test": "mocha './!(node_modules)/**/**.+(spec).js'",
51
- "lint": "eslint lib --ext .js"
56
+ "lint": "eslint lib --ext .js",
57
+ "build": "tsup"
52
58
  }
53
59
  }
@@ -1,60 +0,0 @@
1
- name: publish
2
-
3
- on:
4
- push:
5
- branches:
6
- - '*'
7
- tags:
8
- - 'v*'
9
-
10
- jobs:
11
- build:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v3
15
- with:
16
- ref: master
17
-
18
- - uses: volta-cli/action@v4
19
-
20
- - name: Cache pnpm modules
21
- uses: actions/cache@v2
22
- with:
23
- path: ~/.pnpm-store
24
- key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
25
- restore-keys: |
26
- ${{ runner.os }}-
27
-
28
- - uses: pnpm/action-setup@v2.2.4
29
- with:
30
- run_install: true
31
-
32
- - run: pnpm lint
33
-
34
- publish-npm:
35
- if: startsWith(github.ref, 'refs/tags/v')
36
- needs: build
37
- runs-on: ubuntu-latest
38
- steps:
39
- - uses: actions/checkout@v3
40
- with:
41
- ref: master
42
-
43
- - uses: volta-cli/action@v4
44
-
45
- - name: Authorize NPM
46
- run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
47
-
48
- - name: Cache pnpm modules
49
- uses: actions/cache@v2
50
- with:
51
- path: ~/.pnpm-store
52
- key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
53
- restore-keys: |
54
- ${{ runner.os }}-
55
-
56
- - uses: pnpm/action-setup@v2.2.4
57
- with:
58
- run_install: true
59
-
60
- - run: pnpm publish
@@ -1,12 +0,0 @@
1
- function compose (...fns) {
2
- return function () {
3
- var result = fns[0].apply(this, arguments)
4
- var len = fns.length
5
- for (var i = 1; i < len; i++) {
6
- result = fns[i].call(this, result)
7
- }
8
- return result
9
- }
10
- }
11
-
12
- export { compose }