aspi 1.1.0-beta.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.cjs ADDED
@@ -0,0 +1,1237 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Aspi: () => Aspi,
24
+ AspiError: () => AspiError,
25
+ CustomError: () => CustomError,
26
+ Request: () => Request,
27
+ Result: () => result_exports,
28
+ getHttpErrorStatus: () => getHttpErrorStatus,
29
+ httpErrors: () => httpErrors
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/http.ts
34
+ var httpErrors = {
35
+ BAD_REQUEST: 400,
36
+ UNAUTHORIZED: 401,
37
+ FORBIDDEN: 403,
38
+ NOT_FOUND: 404,
39
+ METHOD_NOT_ALLOWED: 405,
40
+ CONFLICT: 409,
41
+ PAYLOAD_TOO_LARGE: 413,
42
+ UNSUPPORTED_MEDIA_TYPE: 415,
43
+ UNPROCESSABLE_ENTITY: 422,
44
+ TOO_MANY_REQUESTS: 429,
45
+ INTERNAL_SERVER_ERROR: 500,
46
+ NOT_IMPLEMENTED: 501,
47
+ BAD_GATEWAY: 502,
48
+ SERVICE_UNAVAILABLE: 503,
49
+ GATEWAY_TIMEOUT: 504
50
+ };
51
+ var getHttpErrorStatus = (status) => {
52
+ return Object.keys(httpErrors).find(
53
+ (key) => httpErrors[key] === status
54
+ );
55
+ };
56
+
57
+ // src/error.ts
58
+ var AspiError = class extends Error {
59
+ tag = "aspiError";
60
+ request;
61
+ response;
62
+ /**
63
+ * Creates an instance of AspiError
64
+ * @param {string} message - The error message
65
+ * @param {AspiRe} request - The request configuration
66
+ * @param {AspiResponse} response - The error response
67
+ */
68
+ constructor(message, request, response) {
69
+ super(message);
70
+ this.request = request;
71
+ this.response = response;
72
+ }
73
+ /**
74
+ * Conditionally executes callback if status matches
75
+ * @template T
76
+ * @param {HttpErrorStatus} status - The status to match against
77
+ * @param {(args: { request: AspiRequest; response: AspiResponse }) => T} cb - Callback to execute on match
78
+ * @returns {T | undefined} Result of callback if status matches, undefined otherwise
79
+ */
80
+ ifMatch(status, cb) {
81
+ if (this.response.statusText === status) {
82
+ return cb({
83
+ request: this.request,
84
+ response: this.response
85
+ });
86
+ }
87
+ }
88
+ };
89
+ var CustomError = class extends Error {
90
+ tag;
91
+ data;
92
+ /**
93
+ * Creates an instance of CustomError
94
+ * @param {Tag} tag - Tag identifying the error type
95
+ * @param {A} data - Additional error data
96
+ */
97
+ constructor(tag, data) {
98
+ super(tag);
99
+ this.tag = tag;
100
+ this.data = data;
101
+ }
102
+ };
103
+
104
+ // src/result.ts
105
+ var result_exports = {};
106
+ __export(result_exports, {
107
+ catchAllErrors: () => catchAllErrors,
108
+ catchError: () => catchError,
109
+ catchErrors: () => catchErrors,
110
+ err: () => err,
111
+ getErrorOrNull: () => getErrorOrNull,
112
+ getOrElse: () => getOrElse,
113
+ getOrNull: () => getOrNull,
114
+ getOrThrow: () => getOrThrow,
115
+ getOrThrowWith: () => getOrThrowWith,
116
+ isErr: () => isErr,
117
+ isOk: () => isOk,
118
+ map: () => map,
119
+ mapErr: () => mapErr,
120
+ match: () => match,
121
+ ok: () => ok,
122
+ pipe: () => pipe
123
+ });
124
+ var ok = (value) => ({ __tag: "ok", value });
125
+ var err = (error) => ({ __tag: "err", error });
126
+ var isOk = (result) => result.__tag === "ok";
127
+ var isErr = (result) => !isOk(result);
128
+ function map(selfOrFn, fn) {
129
+ if (fn) {
130
+ const self = selfOrFn;
131
+ return isOk(self) ? ok(fn(self.value)) : self;
132
+ } else {
133
+ const mappingFn = selfOrFn;
134
+ return (self) => isOk(self) ? ok(mappingFn(self.value)) : self;
135
+ }
136
+ }
137
+ function mapErr(selfOrFn, fn) {
138
+ if (fn) {
139
+ const self = selfOrFn;
140
+ return isErr(self) ? err(fn(self.error)) : self;
141
+ } else {
142
+ const mappingFn = selfOrFn;
143
+ return (self) => isErr(self) ? err(mappingFn(self.error)) : self;
144
+ }
145
+ }
146
+ function match(selfOrHandlers, handlers) {
147
+ if (handlers) {
148
+ const self = selfOrHandlers;
149
+ return isOk(self) ? handlers.onOk(self.value) : handlers.onErr(self.error);
150
+ } else {
151
+ const handlersObj = selfOrHandlers;
152
+ return (self) => isOk(self) ? handlersObj.onOk(self.value) : handlersObj.onErr(self.error);
153
+ }
154
+ }
155
+ var getOrNull = (result) => {
156
+ if (isOk(result)) {
157
+ return result.value;
158
+ } else {
159
+ return null;
160
+ }
161
+ };
162
+ var getErrorOrNull = (result) => {
163
+ if (isErr(result)) {
164
+ return result.error;
165
+ } else {
166
+ return null;
167
+ }
168
+ };
169
+ var getOrElse = (result, fallback) => isOk(result) ? result.value : fallback;
170
+ var getOrThrow = (result) => {
171
+ if (isOk(result)) {
172
+ return result.value;
173
+ } else {
174
+ throw result.error;
175
+ }
176
+ };
177
+ function getOrThrowWith(selfOrFn, fn) {
178
+ if (fn) {
179
+ const self = selfOrFn;
180
+ if (isOk(self)) {
181
+ return self.value;
182
+ } else {
183
+ throw fn(self.error);
184
+ }
185
+ } else {
186
+ const throwFn = selfOrFn;
187
+ return (self) => {
188
+ if (isOk(self)) {
189
+ return self.value;
190
+ } else {
191
+ throw throwFn(self.error);
192
+ }
193
+ };
194
+ }
195
+ }
196
+ function catchError(resultOrErrorOrTag, tagOrFn, fnOrUndefined) {
197
+ if (typeof resultOrErrorOrTag === "string") {
198
+ const tag = resultOrErrorOrTag;
199
+ const fn = tagOrFn;
200
+ return (resultOrError) => {
201
+ if ("__tag" in resultOrError) {
202
+ const result = resultOrError;
203
+ if (isErr(result) && result.error.tag === tag) {
204
+ fn(result.error);
205
+ return ok(null);
206
+ }
207
+ return result;
208
+ }
209
+ const error = resultOrError;
210
+ if (error.tag === tag) {
211
+ fn(error);
212
+ }
213
+ };
214
+ } else {
215
+ const resultOrError = resultOrErrorOrTag;
216
+ const tag = tagOrFn;
217
+ const fn = fnOrUndefined;
218
+ if ("__tag" in resultOrError) {
219
+ const result = resultOrError;
220
+ if (isErr(result) && result.error.tag === tag) {
221
+ fn(result.error);
222
+ return ok(null);
223
+ }
224
+ return result;
225
+ }
226
+ const error = resultOrError;
227
+ if (error.tag === tag) {
228
+ fn(error);
229
+ }
230
+ }
231
+ }
232
+ function catchAllErrors(resultOrFn, fnOrUndefined) {
233
+ if ("__tag" in resultOrFn) {
234
+ const result = resultOrFn;
235
+ if (isErr(result)) {
236
+ fnOrUndefined(result.error);
237
+ }
238
+ return result;
239
+ }
240
+ return (result) => {
241
+ if (isErr(result)) {
242
+ fnOrUndefined(result.error);
243
+ }
244
+ return result;
245
+ };
246
+ }
247
+ function catchErrors(resultOrHandlers, handlersOrUndefined) {
248
+ if ("__tag" in resultOrHandlers) {
249
+ const result = resultOrHandlers;
250
+ if (isErr(result)) {
251
+ const handler = handlersOrUndefined[result.error.tag];
252
+ if (handler) {
253
+ handler(result.error);
254
+ }
255
+ }
256
+ return result;
257
+ } else {
258
+ return (result) => {
259
+ if (isErr(result)) {
260
+ const handler = handlersOrUndefined[result.error.tag];
261
+ if (handler) {
262
+ handler(result.error);
263
+ }
264
+ }
265
+ return result;
266
+ };
267
+ }
268
+ }
269
+ var pipe = (result, ab) => {
270
+ function run(a) {
271
+ return ab(a);
272
+ }
273
+ run.pipe = (bc) => pipe(result, (a) => bc(ab(a)));
274
+ run.execute = () => run(result);
275
+ return run;
276
+ };
277
+
278
+ // src/request.ts
279
+ var Request = class {
280
+ #path;
281
+ #localRequestInit;
282
+ #customErrorCbs = {};
283
+ #queryParams;
284
+ #middlewares;
285
+ #schema = null;
286
+ #bodySchema = null;
287
+ #retryConfig;
288
+ #shouldBeResult = false;
289
+ #bodySchemaIssues = [];
290
+ constructor(method, path, {
291
+ requestConfig,
292
+ retryConfig,
293
+ middlewares,
294
+ errorCbs
295
+ }) {
296
+ this.#path = path;
297
+ this.#middlewares = middlewares || [];
298
+ this.#localRequestInit = { ...requestConfig, method };
299
+ this.#retryConfig = retryConfig;
300
+ this.#customErrorCbs = errorCbs || {};
301
+ }
302
+ /**
303
+ * Sets the base URL for the request.
304
+ * @param url The base URL to set
305
+ * @returns The request instance for chaining
306
+ * @example
307
+ * const request = new Request('/users', config);
308
+ * request.setBaseUrl('https://api.example.com');
309
+ */
310
+ setBaseUrl(url) {
311
+ this.#localRequestInit.baseUrl = url;
312
+ return this;
313
+ }
314
+ /**
315
+ * Sets the retry configuration for the request.
316
+ * @param retry The retry configuration object
317
+ * @returns The request instance for chaining
318
+ * @example
319
+ * const request = new Request('/users', config);
320
+ * request.setRetry({
321
+ * retries: 3, // Number of retry attempts
322
+ * retryDelay: 1000, // Delay between retries in ms
323
+ * retryOn: [500, 502, 503], // Status codes to retry on
324
+ * retryWhile: (request, response) => response.status >= 500 // Custom retry condition
325
+ * });
326
+ */
327
+ setRetry(retry) {
328
+ this.#retryConfig = {
329
+ ...this.#retryConfig,
330
+ ...retry
331
+ };
332
+ return this;
333
+ }
334
+ /**
335
+ * Sets multiple headers for the request.
336
+ * @param headers An object containing header key-value pairs
337
+ * @returns The request instance for chaining
338
+ * @example
339
+ * const request = new Request('/users', config);
340
+ * request.setHeaders({
341
+ * 'Content-Type': 'application/json',
342
+ * 'Accept': 'application/json'
343
+ * });
344
+ */
345
+ setHeaders(headers) {
346
+ this.#localRequestInit.headers = headers;
347
+ return this;
348
+ }
349
+ /**
350
+ * Sets a single header for the request.
351
+ * @param key The header key
352
+ * @param value The header value
353
+ * @returns The request instance for chaining
354
+ * @example
355
+ * const request = new Request('/users', config);
356
+ * request.setHeader('Content-Type', 'application/json');
357
+ */
358
+ setHeader(key, value) {
359
+ this.#localRequestInit.headers = {
360
+ ...this.#localRequestInit.headers,
361
+ [key]: value
362
+ };
363
+ return this;
364
+ }
365
+ /**
366
+ * Sets the Authorization header with a bearer token.
367
+ * @param token The bearer token to set
368
+ * @returns The request instance for chaining
369
+ * @example
370
+ * const request = new Request('/users', config);
371
+ * request.setBearer('my-auth-token');
372
+ */
373
+ setBearer(token) {
374
+ return this.setHeader("Authorization", `Bearer ${token}`);
375
+ }
376
+ /**
377
+ * Sets a validation schema for the request body using a StandardSchemaV1 schema.
378
+ * @template TSchema Type parameter extending StandardSchemaV1
379
+ * @param schema The schema to validate the body against
380
+ * @returns The request instance for chaining with updated error type
381
+ * @example
382
+ * const userSchema = z.object({
383
+ * name: z.string(),
384
+ * email: z.string().email()
385
+ * });
386
+ *
387
+ * const request = new Request('/users', config);
388
+ * request
389
+ * .bodySchema(userSchema)
390
+ * .bodyJson({
391
+ * name: 'John',
392
+ * email: 'john@example.com'
393
+ * });
394
+ */
395
+ bodySchema(schema) {
396
+ this.#bodySchema = schema;
397
+ return this;
398
+ }
399
+ /**
400
+ * Sets the request body as JSON by automatically stringifying the provided object.
401
+ * @param body The object to be stringified and sent as the request body
402
+ * @returns The request instance for chaining
403
+ * @example
404
+ * const request = new Request('/users', config);
405
+ * request.bodyJson({
406
+ * name: 'John Doe',
407
+ * email: 'john@example.com',
408
+ * age: 30
409
+ * });
410
+ */
411
+ bodyJson(body) {
412
+ if (this.#bodySchema) {
413
+ const data = this.#bodySchema["~standard"].validate(body);
414
+ if (data instanceof Promise) {
415
+ throw new Error("Schema validation should not return a promise");
416
+ }
417
+ if (data.issues) {
418
+ this.#bodySchemaIssues = data.issues;
419
+ } else {
420
+ this.#localRequestInit.body = JSON.stringify(body);
421
+ }
422
+ }
423
+ return this;
424
+ }
425
+ /**
426
+ * Sets the raw request body (unsafe, use with caution).
427
+ * @param body The body content to send with the request
428
+ * @returns The request instance for chaining
429
+ * @example
430
+ * const request = new Request('/users', config);
431
+ * request.unsafeBody(new FormData());
432
+ *
433
+ * // or with raw text
434
+ * request.unsafeBody('Hello World');
435
+ *
436
+ * // or with URLSearchParams
437
+ * request.unsafeBody(new URLSearchParams({ key: 'value' }));
438
+ */
439
+ unsafeBody(body) {
440
+ this.#localRequestInit.body = body;
441
+ return this;
442
+ }
443
+ /**
444
+ * Handles 404 Not Found errors with a custom callback.
445
+ * @param cb The callback function to handle the error
446
+ * @returns The request instance for chaining
447
+ * @example
448
+ * const request = new Request('/users', config);
449
+ * request.notFound((error) => {
450
+ * console.log('Resource not found:', error);
451
+ * return { message: 'Custom not found message' };
452
+ * });
453
+ */
454
+ notFound(cb) {
455
+ return this.error("notFoundError", "NOT_FOUND", cb);
456
+ }
457
+ /**
458
+ * Handles 429 Too Many Requests errors with a custom callback.
459
+ * @param cb The callback function to handle the error
460
+ * @returns The request instance for chaining
461
+ * @example
462
+ * const request = new Request('/users', config);
463
+ * request.tooManyRequests((error) => {
464
+ * console.log('Rate limited:', error);
465
+ * return { message: 'Please try again later' };
466
+ * });
467
+ */
468
+ tooManyRequests(cb) {
469
+ return this.error("tooManyRequestsError", "TOO_MANY_REQUESTS", cb);
470
+ }
471
+ /**
472
+ * Handles 400 Bad Request errors with a custom callback.
473
+ * @param cb The callback function to handle the error
474
+ * @returns The request instance for chaining
475
+ * @example
476
+ * const request = new Request('/users', config);
477
+ * request.badRequest((error) => {
478
+ * console.log('Bad request error:', error);
479
+ * return { message: 'Invalid request parameters' };
480
+ * });
481
+ */
482
+ badRequest(cb) {
483
+ return this.error("badRequestError", "BAD_REQUEST", cb);
484
+ }
485
+ /**
486
+ * Handles 409 Conflict errors with a custom callback.
487
+ * @param cb The callback function to handle the error
488
+ * @returns The request instance for chaining
489
+ * @example
490
+ * const request = new Request('/users', config);
491
+ * request.conflict((error) => {
492
+ * console.log('Conflict error:', error);
493
+ * return { message: 'Resource conflict detected' };
494
+ * });
495
+ */
496
+ conflict(cb) {
497
+ return this.error("conflictError", "CONFLICT", cb);
498
+ }
499
+ /**
500
+ * Handles 401 Unauthorized errors with a custom callback.
501
+ * @param cb The callback function to handle the error
502
+ * @returns The request instance for chaining
503
+ * @example
504
+ * const request = new Request('/users', config);
505
+ * request.unauthorised((error) => {
506
+ * console.log('Unauthorized access:', error);
507
+ * return { message: 'Please login first' };
508
+ * });
509
+ */
510
+ unauthorised(cb) {
511
+ return this.error("unauthorisedError", "UNAUTHORIZED", cb);
512
+ }
513
+ /**
514
+ * Handles 403 Forbidden errors with a custom callback.
515
+ * @param cb The callback function to handle the error
516
+ * @returns The request instance for chaining
517
+ * @example
518
+ * const request = new Request('/users', config);
519
+ * request.forbidden((error) => {
520
+ * console.log('Access forbidden:', error);
521
+ * return { message: 'You do not have permission' };
522
+ * });
523
+ */
524
+ forbidden(cb) {
525
+ return this.error("forbiddenError", "FORBIDDEN", cb);
526
+ }
527
+ /**
528
+ * Handles 501 Not Implemented errors with a custom callback.
529
+ * @param cb The callback function to handle the error
530
+ * @returns The request instance for chaining
531
+ * @example
532
+ * const request = new Request('/users', config);
533
+ * request.notImplemented((error) => {
534
+ * console.log('Not implemented:', error);
535
+ * return { message: 'This feature is not implemented yet' };
536
+ * });
537
+ */
538
+ notImplemented(cb) {
539
+ return this.error("notImplementedError", "NOT_IMPLEMENTED", cb);
540
+ }
541
+ /**
542
+ * Handles 500 Internal Server Error errors with a custom callback.
543
+ * @param cb The callback function to handle the error
544
+ * @returns The request instance for chaining
545
+ * @example
546
+ * const request = new Request('/users', config);
547
+ * request.internalServerError((error) => {
548
+ * console.log('Server error:', error);
549
+ * return { message: 'An unexpected error occurred' };
550
+ * });
551
+ */
552
+ internalServerError(cb) {
553
+ return this.error("internalServerError", "INTERNAL_SERVER_ERROR", cb);
554
+ }
555
+ /**
556
+ * Sets a custom error handler for a specific HTTP status code.
557
+ * @param tag A string identifier for the error type
558
+ * @param status The HTTP error status to handle
559
+ * @param cb The callback function to handle the error
560
+ * @returns The request instance for chaining
561
+ * @example
562
+ * const request = new Request('/users', config);
563
+ * request
564
+ .withResult()
565
+ .error('customError', 'BAD_REQUEST', (error) => {
566
+ * console.log('Bad request error:', error);
567
+ * return {
568
+ * message: 'Invalid input',
569
+ * details: error.response.responseData
570
+ * };
571
+ * });
572
+ *
573
+ * // Later when making the request:
574
+ * const result = await request.json();
575
+ * if (Result.isErr(result)) {
576
+ * if(result.tag === 'customError') {
577
+ * console.log(result.error.data.message); // 'Invalid input'
578
+ * }
579
+ */
580
+ error(tag, status, cb) {
581
+ this.#customErrorCbs[httpErrors[status]] = {
582
+ cb,
583
+ tag
584
+ };
585
+ return this;
586
+ }
587
+ /**
588
+ * Sets query parameters for the request URL.
589
+ * @param params An object containing query parameter key-value pairs
590
+ * @returns The request instance for chaining
591
+ * @example
592
+ * const request = new Request('/users', config);
593
+ * request.setQueryParams({
594
+ * page: '1',
595
+ * limit: '10',
596
+ * sort: 'desc'
597
+ * });
598
+ */
599
+ setQueryParams(params) {
600
+ this.#queryParams = new URLSearchParams(params);
601
+ return this;
602
+ }
603
+ /**
604
+ * Sets the output schema for validating the response data using Zod.
605
+ * @param schema The Zod schema to validate the response
606
+ * @returns The request instance for chaining
607
+ * @example
608
+ * import { z } from 'zod';
609
+ *
610
+ * const userSchema = z.object({
611
+ * id: z.number(),
612
+ * name: z.string(),
613
+ * email: z.string().email()
614
+ * });
615
+ *
616
+ * const request = new Request('/users', config);
617
+ * const result = await request
618
+ * .withResult()
619
+ * .output(userSchema)
620
+ * .json();
621
+ *
622
+ * if (Result.isOk(result)) {
623
+ * const user = result.value; // Typed and validated user data
624
+ * }
625
+ */
626
+ schema(schema) {
627
+ this.#schema = schema;
628
+ return this;
629
+ }
630
+ /**
631
+ * Executes the request and returns the JSON response.
632
+ * @returns A Promise containing the Result type with either successful data or error information
633
+ * @example
634
+ * const request = new Request('/users', config);
635
+ * const result = await request
636
+ * .setQueryParams({ id: '123' })
637
+ * .withResult()
638
+ * .notFound((error) => ({ message: 'User not found' }))
639
+ * .json<User>();
640
+ *
641
+ * if (Result.isOk(result)) {
642
+ * const user = result.value; // User data
643
+ * } else {
644
+ * console.error(result.error); // Error handling
645
+ * }
646
+ */
647
+ async json() {
648
+ const output = await this.#makeRequest(
649
+ async (response) => response.json().catch(
650
+ (e) => new CustomError("jsonParseError", {
651
+ message: e instanceof Error ? e.message : "Failed to parse JSON"
652
+ })
653
+ ),
654
+ true
655
+ );
656
+ return this.#mapResponse(output);
657
+ }
658
+ /**
659
+ * Executes the request and returns the response as plain text.
660
+ * @returns A Promise containing the Result type with either successful text data or error information
661
+ * @example
662
+ * const request = new Request('/data.txt', config);
663
+ * const result = await request
664
+ * .setQueryParams({ version: '1' })
665
+ * .withResult()
666
+ * .notFound((error) => ({ message: 'Text file not found' }))
667
+ * .text();
668
+ *
669
+ * if (Result.isOk(result)) {
670
+ * const text = result.value; // Plain text content
671
+ * } else {
672
+ * console.error(result.error); // Error handling
673
+ * }
674
+ */
675
+ async text() {
676
+ const output = await this.#makeRequest(
677
+ (response) => response.text()
678
+ );
679
+ return this.#mapResponse(output);
680
+ }
681
+ /**
682
+ * Executes the request and returns the response as a Blob.
683
+ * @returns A Promise containing the Result type with either successful Blob data or error information
684
+ * @example
685
+ * const request = new Request('/image.jpg', config);
686
+ * const result = await request
687
+ * .setQueryParams({ size: 'large' })
688
+ * .withResult()
689
+ * .notFound((error) => ({ message: 'Image not found' }))
690
+ * .blob();
691
+ *
692
+ * if (Result.isOk(result)) {
693
+ * const imageBlob = result.value; // Blob data
694
+ * } else {
695
+ * console.error(result.error); // Error handling
696
+ * }
697
+ */
698
+ async blob() {
699
+ const output = await this.#makeRequest((response) => response.blob());
700
+ return this.#mapResponse(output);
701
+ }
702
+ #url() {
703
+ const baseUrl = this.#localRequestInit.baseUrl?.replace(/\/+$/, "") ?? "";
704
+ const path = this.#path.replace(/^\/+/, "/");
705
+ const queryString = this.#queryParams ? `?${this.#queryParams.toString()}` : "";
706
+ const url = [baseUrl, path, queryString].filter(Boolean).join("");
707
+ return url;
708
+ }
709
+ /**
710
+ * Returns the complete URL for the request including base URL, path, and query parameters.
711
+ * @returns The complete URL string
712
+ * @example
713
+ * const request = new Request('/users', config);
714
+ * request.setBaseUrl('https://api.example.com');
715
+ * request.setQueryParams({ id: '123' });
716
+ * console.log(request.url()); // 'https://api.example.com/users?id=123'
717
+ */
718
+ url() {
719
+ return this.#url();
720
+ }
721
+ /**
722
+ * Configures the request to return a Result type instead of a tuple.
723
+ * @returns The request instance for chaining with Result type return value
724
+ * @example
725
+ * const request = new Request('/users', config);
726
+ * const result = await request
727
+ * .withResult()
728
+ * .json<User>();
729
+ *
730
+ * // Returns Result type instead of tuple
731
+ * if (Result.isOk(result)) {
732
+ * const user = result.value;
733
+ * }
734
+ */
735
+ withResult() {
736
+ this.#shouldBeResult = true;
737
+ return this;
738
+ }
739
+ #mapResponse(value) {
740
+ if (this.#shouldBeResult) {
741
+ return value;
742
+ }
743
+ if (isOk(value)) {
744
+ return [getOrNull(value), null];
745
+ } else {
746
+ return [null, getErrorOrNull(value)];
747
+ }
748
+ }
749
+ async #makeRequest(responseParser, isJson = false) {
750
+ if (this.#bodySchemaIssues.length) {
751
+ return err(new CustomError("parseError", this.#bodySchemaIssues));
752
+ }
753
+ const request = this.#request();
754
+ const { retries, retryDelay, retryOn, retryWhile, onRetry } = this.#sanitisedRetryConfig();
755
+ try {
756
+ const requestInit = request.requestInit;
757
+ const url = this.#url();
758
+ let attempts = 1;
759
+ let response;
760
+ let responseData;
761
+ while (attempts <= retries) {
762
+ try {
763
+ response = await fetch(url, requestInit);
764
+ responseData = await responseParser(response);
765
+ if (responseData instanceof CustomError) {
766
+ return err(responseData);
767
+ }
768
+ const retryWhileCondition = retryWhile ? await retryWhile(
769
+ request,
770
+ this.#makeResponse(response, responseData)
771
+ ) : false;
772
+ if (response.ok || !retryOn.includes(response.status) && !retryWhileCondition) {
773
+ break;
774
+ }
775
+ if (response.status in this.#customErrorCbs && attempts === retries) {
776
+ const result = this.#customErrorCbs[response.status].cb({
777
+ request,
778
+ response: this.#makeResponse(response, responseData)
779
+ });
780
+ return err(
781
+ new CustomError(
782
+ // @ts-ignore
783
+ this.#customErrorCbs[response.status].tag,
784
+ result
785
+ )
786
+ );
787
+ }
788
+ if (attempts < retries) {
789
+ const delay = typeof retryDelay === "function" ? await retryDelay(
790
+ retries - attempts - 1,
791
+ retries,
792
+ request,
793
+ this.#makeResponse(response, responseData)
794
+ ) : retryDelay;
795
+ await new Promise((resolve) => setTimeout(resolve, delay));
796
+ }
797
+ } catch (e) {
798
+ if (attempts === retries) throw e;
799
+ const delay = typeof retryDelay === "function" ? await retryDelay(
800
+ retries - attempts - 1,
801
+ retries,
802
+ request,
803
+ this.#makeResponse(response, responseData)
804
+ ) : retryDelay;
805
+ await new Promise((resolve) => setTimeout(resolve, delay));
806
+ }
807
+ if (onRetry) {
808
+ onRetry(request, this.#makeResponse(response, responseData));
809
+ }
810
+ attempts++;
811
+ }
812
+ if (!response.ok) {
813
+ if (response.status in this.#customErrorCbs) {
814
+ const result = this.#customErrorCbs[response.status].cb({
815
+ request,
816
+ response: this.#makeResponse(response, responseData)
817
+ });
818
+ return err(
819
+ new CustomError(
820
+ // @ts-ignore
821
+ this.#customErrorCbs[response.status].tag,
822
+ result
823
+ )
824
+ );
825
+ }
826
+ return err(
827
+ new AspiError(
828
+ response.statusText,
829
+ this.#request(),
830
+ this.#makeResponse(response, responseData)
831
+ )
832
+ );
833
+ }
834
+ if (isJson && this.#schema) {
835
+ const data = this.#schema["~standard"].validate(responseData);
836
+ if (data instanceof Promise) {
837
+ throw new Error("Schema validation should not return a promise");
838
+ }
839
+ if (data.issues) {
840
+ return err(new CustomError("parseError", data.issues));
841
+ }
842
+ return ok({
843
+ data: data.value,
844
+ request,
845
+ response: this.#makeResponse(response, responseData)
846
+ });
847
+ }
848
+ return ok({
849
+ data: responseData,
850
+ request,
851
+ response: this.#makeResponse(response, responseData)
852
+ });
853
+ } catch (error) {
854
+ if (500 in this.#customErrorCbs) {
855
+ const result = this.#customErrorCbs[500].cb({
856
+ request,
857
+ response: {
858
+ status: 500,
859
+ statusText: "INTERNAL_SERVER_ERROR"
860
+ }
861
+ });
862
+ return err(
863
+ new CustomError(
864
+ // @ts-ignore
865
+ this.#customErrorCbs[500].tag,
866
+ result
867
+ )
868
+ );
869
+ }
870
+ return err(
871
+ new AspiError(
872
+ error instanceof Error ? error.message : "Something went wrong",
873
+ request,
874
+ {
875
+ status: 500,
876
+ statusText: "INTERNAL_SERVER_ERROR"
877
+ }
878
+ )
879
+ );
880
+ }
881
+ }
882
+ #request() {
883
+ let requestInit = this.#localRequestInit;
884
+ for (const middleware of this.#middlewares) {
885
+ requestInit = middleware(this.#localRequestInit);
886
+ }
887
+ return {
888
+ requestInit,
889
+ baseUrl: this.#localRequestInit.baseUrl,
890
+ path: this.#path,
891
+ queryParams: this.#queryParams || null,
892
+ retryConfig: this.#sanitisedRetryConfig()
893
+ };
894
+ }
895
+ #sanitisedRetryConfig() {
896
+ const retries = this.#retryConfig?.retries || 1;
897
+ const retryDelay = this.#retryConfig?.retryDelay || 0;
898
+ const retryOn = this.#retryConfig?.retryOn || [];
899
+ const retryWhile = this.#retryConfig?.retryWhile;
900
+ const onRetry = this.#retryConfig?.onRetry;
901
+ return {
902
+ retries,
903
+ retryDelay,
904
+ retryOn,
905
+ retryWhile,
906
+ onRetry
907
+ };
908
+ }
909
+ #makeResponse(response, responseData) {
910
+ return {
911
+ response,
912
+ status: response.status,
913
+ statusText: getHttpErrorStatus(response.status),
914
+ responseData
915
+ };
916
+ }
917
+ };
918
+
919
+ // src/aspi.ts
920
+ var Aspi = class {
921
+ #globalRequestInit;
922
+ #middlewares = [];
923
+ #retryConfig;
924
+ #customErrorCbs = {};
925
+ constructor(config) {
926
+ const { retryConfig, ...requestConfig } = config;
927
+ this.#globalRequestInit = requestConfig;
928
+ this.#retryConfig = retryConfig;
929
+ }
930
+ /**
931
+ * Sets the base URL for all API requests
932
+ * @param {string} url - The base URL to be set
933
+ * @returns {Aspi} The Aspi instance for chaining
934
+ * @example
935
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
936
+ * api.setBaseUrl('https://api.newdomain.com');
937
+ */
938
+ setBaseUrl(url) {
939
+ this.#globalRequestInit.baseUrl = url;
940
+ return this;
941
+ }
942
+ /**
943
+ * Sets the retry configuration for failed requests
944
+ * @param {AspiRetryConfig<TRequest>} retry - The retry configuration object
945
+ * @returns {Aspi} The Aspi instance for chaining
946
+ * @example
947
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
948
+ * api.setRetry({
949
+ * retries: 3,
950
+ * retryDelay: 1000,
951
+ * retryOn: [500, 502],
952
+ * retryWhile: (req, res) => res.status === 500
953
+ * });
954
+ */
955
+ setRetry(retry) {
956
+ this.#retryConfig = {
957
+ ...this.#retryConfig,
958
+ ...retry
959
+ };
960
+ return this;
961
+ }
962
+ #createRequest(method, path, body) {
963
+ return new Request(method, path, {
964
+ requestConfig: {
965
+ ...this.#globalRequestInit,
966
+ method,
967
+ body
968
+ },
969
+ retryConfig: this.#retryConfig,
970
+ middlewares: this.#middlewares,
971
+ errorCbs: this.#customErrorCbs
972
+ });
973
+ }
974
+ /**
975
+ * Makes a GET request to the specified path
976
+ * @param {string} path - The path to make the request to
977
+ * @returns {Request} A Request instance for chaining
978
+ * @example
979
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
980
+ * const response = await api.get('/users').json();
981
+ */
982
+ get(path) {
983
+ return this.#createRequest("GET", path);
984
+ }
985
+ /**
986
+ * Makes a POST request to the specified path with an optional body
987
+ * @param {string} path - The path to make the request to
988
+ * @param {BodyInit} [body] - The body of the request
989
+ * @returns {Request} A Request instance for chaining
990
+ * @example
991
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
992
+ * const response = await api.post('/users', { name: 'John' }).json();
993
+ */
994
+ post(path, body) {
995
+ return this.#createRequest("POST", path, body);
996
+ }
997
+ /**
998
+ * Makes a PUT request to the specified path with an optional body
999
+ * @param {string} path - The path to make the request to
1000
+ * @param {BodyInit} [body] - The body of the request
1001
+ * @returns {Request} A Request instance for chaining
1002
+ * @example
1003
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1004
+ * const response = await api.put('/users/1', { name: 'John' }).json();
1005
+ */
1006
+ put(path, body) {
1007
+ return this.#createRequest("PUT", path, body);
1008
+ }
1009
+ /**
1010
+ * Makes a PATCH request to the specified path with an optional body
1011
+ * @param {string} path - The path to make the request to
1012
+ * @param {BodyInit} [body] - The body of the request
1013
+ * @returns {Request} A Request instance for chaining
1014
+ * @example
1015
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1016
+ * const response = await api.patch('/users/1', { name: 'John' }).json();
1017
+ */
1018
+ patch(path, body) {
1019
+ return this.#createRequest("PATCH", path, body);
1020
+ }
1021
+ /**
1022
+ * Makes a DELETE request to the specified path
1023
+ * @param {string} path - The path to make the request to
1024
+ * @returns {Request} A Request instance for chaining
1025
+ * @example
1026
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1027
+ * const response = await api.delete('/users/1').json();
1028
+ */
1029
+ delete(path) {
1030
+ return this.#createRequest("DELETE", path);
1031
+ }
1032
+ /**
1033
+ * Makes a HEAD request to the specified path
1034
+ * @param {string} path - The path to make the request to
1035
+ * @returns {Request} A Request instance for chaining
1036
+ * @example
1037
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1038
+ * const response = await api.head('/users').json();
1039
+ */
1040
+ head(path) {
1041
+ return this.#createRequest("HEAD", path);
1042
+ }
1043
+ /**
1044
+ * Makes an OPTIONS request to the specified path
1045
+ * @param {string} path - The path to make the request to
1046
+ * @returns {Request} A Request instance for chaining
1047
+ * @example
1048
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1049
+ * const response = await api.options('/users').json();
1050
+ */
1051
+ options(path) {
1052
+ return this.#createRequest("OPTIONS", path);
1053
+ }
1054
+ /**
1055
+ * Sets multiple headers for all API requests
1056
+ * @param {Record<string, string>} headers - An object containing header key-value pairs
1057
+ * @returns {Aspi} The Aspi instance for chaining
1058
+ * @example
1059
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1060
+ * api.setHeaders({
1061
+ * 'Content-Type': 'application/json',
1062
+ * 'Accept': 'application/json'
1063
+ * });
1064
+ */
1065
+ setHeaders(headers) {
1066
+ this.#globalRequestInit.headers = headers;
1067
+ return this;
1068
+ }
1069
+ /**
1070
+ * Sets a single header for all API requests
1071
+ * @param {string} key - The header key
1072
+ * @param {string} value - The header value
1073
+ * @returns {Aspi} The Aspi instance for chaining
1074
+ * @example
1075
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1076
+ * api.setHeader('Content-Type', 'application/json');
1077
+ */
1078
+ setHeader(key, value) {
1079
+ this.#globalRequestInit.headers = {
1080
+ ...this.#globalRequestInit.headers,
1081
+ [key]: value
1082
+ };
1083
+ return this;
1084
+ }
1085
+ /**
1086
+ * Sets the Authorization header with a Bearer token
1087
+ * @param {string} token - The Bearer token
1088
+ * @returns {Aspi} The Aspi instance for chaining
1089
+ * @example
1090
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1091
+ * api.setBearer('myAuthToken123');
1092
+ */
1093
+ setBearer(token) {
1094
+ return this.setHeader("Authorization", `Bearer ${token}`);
1095
+ }
1096
+ /**
1097
+ * Use a middleware function to transform requests
1098
+ * @param {Middleware<T, U>} fn - The middleware function to apply
1099
+ * @returns {Aspi<U>} A new Aspi instance with the applied middleware
1100
+ * @example
1101
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1102
+ * api.use((req) => {
1103
+ * // Add custom headers
1104
+ * req.headers = {
1105
+ * ...req.headers,
1106
+ * 'x-custom-header': 'custom-value'
1107
+ * };
1108
+ * return req;
1109
+ * });
1110
+ */
1111
+ use(fn) {
1112
+ this.#middlewares = [...this.#middlewares, fn];
1113
+ return this;
1114
+ }
1115
+ /**
1116
+ * Registers a custom error handler for a specific HTTP status
1117
+ * @param {string} tag - The error tag identifier
1118
+ * @param {HttpErrorStatus} status - The HTTP status code to handle
1119
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1120
+ * @returns {Aspi} The Aspi instance for chaining
1121
+ * @example
1122
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1123
+ * api.error('customError', 'BAD_REQUEST', (req, res) => {
1124
+ * console.log('Bad request error occurred');
1125
+ * return { message: 'Invalid input' };
1126
+ * });
1127
+ */
1128
+ error(tag, status, cb) {
1129
+ this.#customErrorCbs[httpErrors[status]] = {
1130
+ cb,
1131
+ tag
1132
+ };
1133
+ return this;
1134
+ }
1135
+ /**
1136
+ * Handles 404 Not Found errors with a custom callback.
1137
+ * @param cb The callback function to handle the error
1138
+ * @returns The request instance for chaining
1139
+ * @example
1140
+ * const request = new Request('/users', config);
1141
+ * request.notFound((error) => {
1142
+ * console.log('Not found:', error);
1143
+ * return { message: 'Resource not found' };
1144
+ * });
1145
+ */
1146
+ notFound(cb) {
1147
+ return this.error("notFoundError", "NOT_FOUND", cb);
1148
+ }
1149
+ /**
1150
+ * Handles 429 Too Many Requests errors with a custom callback.
1151
+ * @param cb The callback function to handle the error
1152
+ * @returns The request instance for chaining
1153
+ * @example
1154
+ * const request = new Request('/users', config);
1155
+ * request.tooManyRequests((error) => {
1156
+ * console.log('Rate limited:', error);
1157
+ * return { message: 'Please try again later' };
1158
+ * });
1159
+ */
1160
+ tooManyRequests(cb) {
1161
+ return this.error("tooManyRequestsError", "TOO_MANY_REQUESTS", cb);
1162
+ }
1163
+ /**
1164
+ * Handles 409 Conflict errors with a custom callback.
1165
+ * @param cb The callback function to handle the error
1166
+ * @returns The request instance for chaining
1167
+ * @example
1168
+ * const request = new Request('/users', config);
1169
+ * request.conflict((error) => {
1170
+ * console.log('Conflict error:', error);
1171
+ * return { message: 'Resource conflict detected' };
1172
+ * });
1173
+ */
1174
+ conflict(cb) {
1175
+ return this.error("conflictError", "CONFLICT", cb);
1176
+ }
1177
+ /**
1178
+ * Registers a handler for 400 Bad Request errors
1179
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1180
+ * @returns {Aspi} The Aspi instance for chaining
1181
+ * @example
1182
+ * api.badRequest((req, res) => ({ message: 'Invalid request parameters' }));
1183
+ */
1184
+ badRequest(cb) {
1185
+ return this.error("badRequestError", "BAD_REQUEST", cb);
1186
+ }
1187
+ /**
1188
+ * Registers a handler for 401 Unauthorized errors
1189
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1190
+ * @returns {Aspi} The Aspi instance for chaining
1191
+ * @example
1192
+ * api.unauthorized((req, res) => ({ message: 'Authentication required' }));
1193
+ */
1194
+ unauthorized(cb) {
1195
+ return this.error("unauthorizedError", "UNAUTHORIZED", cb);
1196
+ }
1197
+ /**
1198
+ * Registers a handler for 403 Forbidden errors
1199
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1200
+ * @returns {Aspi} The Aspi instance for chaining
1201
+ * @example
1202
+ * api.forbidden((req, res) => ({ message: 'Access denied' }));
1203
+ */
1204
+ forbidden(cb) {
1205
+ return this.error("forbiddenError", "FORBIDDEN", cb);
1206
+ }
1207
+ /**
1208
+ * Registers a handler for 501 Not Implemented errors
1209
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1210
+ * @returns {Aspi} The Aspi instance for chaining
1211
+ * @example
1212
+ * api.notImplemented((req, res) => ({ message: 'Feature not implemented' }));
1213
+ */
1214
+ notImplemented(cb) {
1215
+ return this.error("notImplementedError", "NOT_IMPLEMENTED", cb);
1216
+ }
1217
+ /**
1218
+ * Registers a handler for 500 Internal Server errors
1219
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1220
+ * @returns {Aspi} The Aspi instance for chaining
1221
+ * @example
1222
+ * api.internalServerError((req, res) => ({ message: 'Server error occurred' }));
1223
+ */
1224
+ internalServerError(cb) {
1225
+ return this.error("internalServerErrorError", "INTERNAL_SERVER_ERROR", cb);
1226
+ }
1227
+ };
1228
+ // Annotate the CommonJS export names for ESM import in node:
1229
+ 0 && (module.exports = {
1230
+ Aspi,
1231
+ AspiError,
1232
+ CustomError,
1233
+ Request,
1234
+ Result,
1235
+ getHttpErrorStatus,
1236
+ httpErrors
1237
+ });