@api-wrappers/api-core 1.0.0 → 1.0.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +23 -9
- package/dist/index.cjs +90 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -49
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +59 -49
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +86 -24
- package/dist/index.mjs.map +1 -1
- package/docs/guides/error-handling.md +10 -10
- package/docs/guides/rest-requests.md +4 -0
- package/docs/reference/configuration.md +15 -4
- package/docs/reference/exports.md +8 -0
- package/package.json +22 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.2 - 2026-05-14
|
|
4
|
+
|
|
5
|
+
- Broadened the TypeScript peer dependency range to support TypeScript 6.
|
|
6
|
+
- Added clearer README guidance for developers building wrapper libraries.
|
|
7
|
+
|
|
8
|
+
## 1.0.1 - 2026-05-14
|
|
9
|
+
|
|
10
|
+
- Fixed cache keys so array and nullish query params match the actual transport URL serialization.
|
|
11
|
+
- Fixed `afterResponse` status handling so plugins can replace the final response.
|
|
12
|
+
- Fixed GraphQL requests so callers cannot accidentally override the JSON content type.
|
|
13
|
+
- Added structured JSON content-type support for `application/*+json` responses and request bodies.
|
|
14
|
+
- Added retry attempt normalization and accurate `retryCount` values when retry plugins override `maxAttempts`.
|
|
15
|
+
- Added native `HeadersInit` support for default, per-request, and GraphQL headers.
|
|
16
|
+
- Added error guard helpers for ergonomic TypeScript error handling.
|
|
17
|
+
- Added a one-command `verify` script, package metadata cleanup, package export for `package.json`, and an MIT license file.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TDanks2000
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @api-wrappers/api-core
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
Shared TypeScript HTTP runtime for API wrapper libraries.
|
|
4
6
|
|
|
5
7
|
`@api-wrappers/api-core` gives wrapper packages a small, predictable foundation
|
|
@@ -20,6 +22,8 @@ their internal HTTP layer consistent and testable.
|
|
|
20
22
|
- Deterministic plugin lifecycle with `setup`, `beforeRequest`,
|
|
21
23
|
`afterResponse`, `onError`, and `dispose`.
|
|
22
24
|
- Built-in auth, cache, logger, rate-limit, retry, and timeout plugins.
|
|
25
|
+
- Native `HeadersInit` support for default, per-request, and GraphQL headers.
|
|
26
|
+
- Type guards for ergonomic `unknown` error handling in TypeScript.
|
|
23
27
|
- Fetch transport with JSON bodies, raw string bodies, abort signals, and
|
|
24
28
|
timeout handling.
|
|
25
29
|
- Response parsing for JSON, text, and binary payloads.
|
|
@@ -360,22 +364,22 @@ contract.
|
|
|
360
364
|
|
|
361
365
|
```ts
|
|
362
366
|
import {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
+
isApiError,
|
|
368
|
+
isGraphQLRequestError,
|
|
369
|
+
isRateLimitError,
|
|
370
|
+
isTimeoutError,
|
|
367
371
|
} from "@api-wrappers/api-core";
|
|
368
372
|
|
|
369
373
|
try {
|
|
370
374
|
await client.get("/resource");
|
|
371
375
|
} catch (error) {
|
|
372
|
-
if (error
|
|
376
|
+
if (isRateLimitError(error)) {
|
|
373
377
|
console.log(error.retryAfterMs);
|
|
374
|
-
} else if (error
|
|
378
|
+
} else if (isTimeoutError(error)) {
|
|
375
379
|
console.log("timed out");
|
|
376
|
-
} else if (error
|
|
380
|
+
} else if (isGraphQLRequestError(error)) {
|
|
377
381
|
console.log(error.graphqlErrors);
|
|
378
|
-
} else if (error
|
|
382
|
+
} else if (isApiError(error)) {
|
|
379
383
|
console.log(error.status, error.responseBody);
|
|
380
384
|
}
|
|
381
385
|
}
|
|
@@ -424,15 +428,23 @@ import {
|
|
|
424
428
|
createTimeoutPlugin,
|
|
425
429
|
gql,
|
|
426
430
|
GraphQLRequestError,
|
|
431
|
+
isApiCoreError,
|
|
432
|
+
isApiError,
|
|
433
|
+
isGraphQLRequestError,
|
|
434
|
+
isRateLimitError,
|
|
435
|
+
isTimeoutError,
|
|
427
436
|
MemoryStore,
|
|
428
437
|
RateLimitError,
|
|
429
438
|
TimeoutError,
|
|
430
439
|
} from "@api-wrappers/api-core";
|
|
431
440
|
|
|
432
441
|
import type {
|
|
442
|
+
ApiCoreError,
|
|
433
443
|
ApiPlugin,
|
|
434
444
|
ApiResponse,
|
|
435
445
|
ClientConfig,
|
|
446
|
+
FetchLike,
|
|
447
|
+
HeaderInput,
|
|
436
448
|
QueryParams,
|
|
437
449
|
RequestContext,
|
|
438
450
|
RequestOptions,
|
|
@@ -465,10 +477,12 @@ The package publishes:
|
|
|
465
477
|
|
|
466
478
|
```bash
|
|
467
479
|
bun install
|
|
480
|
+
bun run verify
|
|
468
481
|
bun run check
|
|
482
|
+
bun run typecheck
|
|
469
483
|
bun test
|
|
470
484
|
bun run build
|
|
471
|
-
|
|
485
|
+
bun run pack:dry-run
|
|
472
486
|
```
|
|
473
487
|
|
|
474
488
|
`dist` is generated by `tsdown`. The published package includes `dist`, `docs`,
|
package/dist/index.cjs
CHANGED
|
@@ -209,6 +209,12 @@ function buildUrl(base, query) {
|
|
|
209
209
|
return `${base}${base.includes("?") ? base.endsWith("?") || base.endsWith("&") ? "" : "&" : "?"}${qs}`;
|
|
210
210
|
}
|
|
211
211
|
//#endregion
|
|
212
|
+
//#region src/utils/isJsonContentType.ts
|
|
213
|
+
function isJsonContentType(contentType) {
|
|
214
|
+
const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
215
|
+
return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
212
218
|
//#region src/utils/isPlainObject.ts
|
|
213
219
|
function isPlainObject(value) {
|
|
214
220
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -226,9 +232,9 @@ const defaultFetch = (input, init) => {
|
|
|
226
232
|
*
|
|
227
233
|
* ```ts
|
|
228
234
|
* import nodeFetch from "node-fetch";
|
|
229
|
-
* createClient({ fetch: nodeFetch as
|
|
235
|
+
* createClient({ fetch: nodeFetch as FetchLike });
|
|
230
236
|
* // — or set it directly on the transport:
|
|
231
|
-
* const transport = createFetchTransport(nodeFetch as
|
|
237
|
+
* const transport = createFetchTransport(nodeFetch as FetchLike);
|
|
232
238
|
* ```
|
|
233
239
|
*/
|
|
234
240
|
function createFetchTransport(fetchFn = defaultFetch) {
|
|
@@ -280,8 +286,7 @@ function createFetchTransport(fetchFn = defaultFetch) {
|
|
|
280
286
|
const fetchTransport = createFetchTransport();
|
|
281
287
|
function serializeRequestBody(body, headers) {
|
|
282
288
|
if (isBodyInit(body)) return body;
|
|
283
|
-
|
|
284
|
-
if (isPlainObject(body) || Array.isArray(body) || contentType.includes("json")) return JSON.stringify(body);
|
|
289
|
+
if (isPlainObject(body) || Array.isArray(body) || isJsonContentType(headers["content-type"])) return JSON.stringify(body);
|
|
285
290
|
return String(body);
|
|
286
291
|
}
|
|
287
292
|
function isBodyInit(body) {
|
|
@@ -304,10 +309,29 @@ function mergeHeaders(...sources) {
|
|
|
304
309
|
const result = {};
|
|
305
310
|
for (const source of sources) {
|
|
306
311
|
if (!source) continue;
|
|
307
|
-
|
|
312
|
+
if (isHeaders(source)) {
|
|
313
|
+
source.forEach((value, key) => {
|
|
314
|
+
result[key.toLowerCase()] = value;
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (Array.isArray(source)) {
|
|
319
|
+
for (const [key, value] of source) result[key.toLowerCase()] = value;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
for (const [key, value] of Object.entries(source)) result[key.toLowerCase()] = String(value);
|
|
308
323
|
}
|
|
309
324
|
return result;
|
|
310
325
|
}
|
|
326
|
+
function isHeaders(source) {
|
|
327
|
+
return typeof Headers !== "undefined" && source instanceof Headers;
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/utils/normalizeRetryMaxAttempts.ts
|
|
331
|
+
function normalizeRetryMaxAttempts(value) {
|
|
332
|
+
if (value === void 0 || !Number.isFinite(value)) return 1;
|
|
333
|
+
return Math.max(1, Math.floor(value));
|
|
334
|
+
}
|
|
311
335
|
//#endregion
|
|
312
336
|
//#region src/utils/resolveUrl.ts
|
|
313
337
|
/**
|
|
@@ -416,7 +440,7 @@ var BaseHttpClient = class {
|
|
|
416
440
|
await this.init();
|
|
417
441
|
const transport = this.config.transport ?? (this.config.fetch ? createFetchTransport(this.config.fetch) : fetchTransport);
|
|
418
442
|
const retryCfg = this.config.retry;
|
|
419
|
-
let maxAttempts = retryCfg?.maxAttempts
|
|
443
|
+
let maxAttempts = normalizeRetryMaxAttempts(retryCfg?.maxAttempts);
|
|
420
444
|
let baseDelay = retryCfg?.delayMs ?? 500;
|
|
421
445
|
let jitter = retryCfg?.jitter ?? true;
|
|
422
446
|
let retriableCodes = retryCfg?.retriableStatusCodes ?? DEFAULT_RETRIABLE_STATUS_CODES;
|
|
@@ -443,10 +467,15 @@ var BaseHttpClient = class {
|
|
|
443
467
|
await this.pluginManager.onError(err, getPluginErrorContext(err) ?? baseCtx);
|
|
444
468
|
throw err;
|
|
445
469
|
}
|
|
446
|
-
if (ctx.meta["retry.maxAttempts"] !== void 0) maxAttempts = ctx.meta["retry.maxAttempts"];
|
|
470
|
+
if (ctx.meta["retry.maxAttempts"] !== void 0) maxAttempts = normalizeRetryMaxAttempts(ctx.meta["retry.maxAttempts"]);
|
|
447
471
|
if (ctx.meta["retry.delayMs"] !== void 0) baseDelay = ctx.meta["retry.delayMs"];
|
|
448
472
|
if (ctx.meta["retry.jitter"] !== void 0) jitter = ctx.meta["retry.jitter"];
|
|
449
473
|
if (ctx.meta["retry.retriableStatusCodes"] !== void 0) retriableCodes = ctx.meta["retry.retriableStatusCodes"];
|
|
474
|
+
const retryCount = maxAttempts - 1 - attempt;
|
|
475
|
+
if (ctx.retryCount !== retryCount) ctx = {
|
|
476
|
+
...ctx,
|
|
477
|
+
retryCount
|
|
478
|
+
};
|
|
450
479
|
let rawResponse;
|
|
451
480
|
if (ctx.syntheticResponse) rawResponse = ctx.syntheticResponse;
|
|
452
481
|
else try {
|
|
@@ -473,16 +502,17 @@ var BaseHttpClient = class {
|
|
|
473
502
|
await this.pluginManager.onError(err, ctx);
|
|
474
503
|
throw err;
|
|
475
504
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
505
|
+
const finalResponse = resCtx.response;
|
|
506
|
+
if (!finalResponse.ok) {
|
|
507
|
+
if (retriableCodes.includes(finalResponse.status) && attempt < maxAttempts - 1) {
|
|
508
|
+
if (finalResponse.status === 429) {
|
|
509
|
+
const wait = readRetryAfterMs(finalResponse);
|
|
480
510
|
await this.waitForRetry(attempt, wait ?? baseDelay, false);
|
|
481
511
|
} else await this.waitForRetry(attempt, baseDelay, jitter);
|
|
482
|
-
lastError = normalizeHttpError(
|
|
512
|
+
lastError = normalizeHttpError(finalResponse, resCtx.parsedBody);
|
|
483
513
|
continue;
|
|
484
514
|
}
|
|
485
|
-
const err = normalizeHttpError(
|
|
515
|
+
const err = normalizeHttpError(finalResponse, resCtx.parsedBody);
|
|
486
516
|
await this.pluginManager.onError(err, ctx);
|
|
487
517
|
throw err;
|
|
488
518
|
}
|
|
@@ -586,7 +616,7 @@ var BaseHttpClient = class {
|
|
|
586
616
|
...variables !== void 0 && { variables },
|
|
587
617
|
...operationName !== void 0 && { operationName }
|
|
588
618
|
},
|
|
589
|
-
headers,
|
|
619
|
+
headers: mergeHeaders(headers, { "content-type": "application/json" }),
|
|
590
620
|
signal,
|
|
591
621
|
timeoutMs,
|
|
592
622
|
cacheKey,
|
|
@@ -610,7 +640,7 @@ async function parseBody(response, responseType = "auto") {
|
|
|
610
640
|
if (responseType === "text") return text;
|
|
611
641
|
if (!text) return void 0;
|
|
612
642
|
if (responseType === "json") return JSON.parse(text);
|
|
613
|
-
if ((response.headers.get("content-type")
|
|
643
|
+
if (isJsonContentType(response.headers.get("content-type"))) return JSON.parse(text);
|
|
614
644
|
return text;
|
|
615
645
|
}
|
|
616
646
|
function normalizeHttpError(response, body) {
|
|
@@ -646,6 +676,23 @@ function createClient(config) {
|
|
|
646
676
|
return new BaseHttpClient(config);
|
|
647
677
|
}
|
|
648
678
|
//#endregion
|
|
679
|
+
//#region src/errors/guards.ts
|
|
680
|
+
function isApiError(error) {
|
|
681
|
+
return error instanceof ApiError;
|
|
682
|
+
}
|
|
683
|
+
function isRateLimitError(error) {
|
|
684
|
+
return error instanceof RateLimitError;
|
|
685
|
+
}
|
|
686
|
+
function isTimeoutError(error) {
|
|
687
|
+
return error instanceof TimeoutError;
|
|
688
|
+
}
|
|
689
|
+
function isGraphQLRequestError(error) {
|
|
690
|
+
return error instanceof GraphQLRequestError;
|
|
691
|
+
}
|
|
692
|
+
function isApiCoreError(error) {
|
|
693
|
+
return isApiError(error) || isTimeoutError(error);
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
649
696
|
//#region src/plugins/auth/authPlugin.ts
|
|
650
697
|
/**
|
|
651
698
|
* Adds an auth token header before each request. The token can be static or
|
|
@@ -723,7 +770,7 @@ function createCachePlugin(options = {}) {
|
|
|
723
770
|
const key = ctx.cacheKey ?? generateKey(ctx);
|
|
724
771
|
const cached = await store.get(key);
|
|
725
772
|
if (cached !== void 0) {
|
|
726
|
-
const syntheticResponse = new Response(
|
|
773
|
+
const syntheticResponse = new Response(serializeCachedBody(cached), {
|
|
727
774
|
status: 200,
|
|
728
775
|
headers: { "content-type": "application/json" }
|
|
729
776
|
});
|
|
@@ -784,8 +831,14 @@ function createCachePlugin(options = {}) {
|
|
|
784
831
|
};
|
|
785
832
|
}
|
|
786
833
|
function defaultCacheKey(ctx) {
|
|
787
|
-
|
|
788
|
-
|
|
834
|
+
return `${ctx.method}:${buildUrl(ctx.url, ctx.query)}`;
|
|
835
|
+
}
|
|
836
|
+
function serializeCachedBody(value) {
|
|
837
|
+
try {
|
|
838
|
+
return JSON.stringify(value) ?? null;
|
|
839
|
+
} catch {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
789
842
|
}
|
|
790
843
|
//#endregion
|
|
791
844
|
//#region src/plugins/logger/loggerPlugin.ts
|
|
@@ -933,11 +986,20 @@ function createRetryPlugin(options = {}) {
|
|
|
933
986
|
name: "retry",
|
|
934
987
|
priority: 5,
|
|
935
988
|
beforeRequest(ctx) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (options.
|
|
939
|
-
|
|
940
|
-
|
|
989
|
+
const meta = { ...ctx.meta };
|
|
990
|
+
let retryCount = ctx.retryCount;
|
|
991
|
+
if (options.maxAttempts !== void 0) {
|
|
992
|
+
meta["retry.maxAttempts"] = options.maxAttempts;
|
|
993
|
+
retryCount = normalizeRetryMaxAttempts(options.maxAttempts) - 1 - ctx.attempt;
|
|
994
|
+
}
|
|
995
|
+
if (options.delayMs !== void 0) meta["retry.delayMs"] = options.delayMs;
|
|
996
|
+
if (options.jitter !== void 0) meta["retry.jitter"] = options.jitter;
|
|
997
|
+
if (options.retriableStatusCodes !== void 0) meta["retry.retriableStatusCodes"] = options.retriableStatusCodes;
|
|
998
|
+
return {
|
|
999
|
+
...ctx,
|
|
1000
|
+
meta,
|
|
1001
|
+
retryCount
|
|
1002
|
+
};
|
|
941
1003
|
}
|
|
942
1004
|
};
|
|
943
1005
|
}
|
|
@@ -1055,7 +1117,12 @@ exports.createRetryPlugin = createRetryPlugin;
|
|
|
1055
1117
|
exports.createTimeoutPlugin = createTimeoutPlugin;
|
|
1056
1118
|
exports.fetchTransport = fetchTransport;
|
|
1057
1119
|
exports.gql = gql;
|
|
1120
|
+
exports.isApiCoreError = isApiCoreError;
|
|
1121
|
+
exports.isApiError = isApiError;
|
|
1122
|
+
exports.isGraphQLRequestError = isGraphQLRequestError;
|
|
1058
1123
|
exports.isPlainObject = isPlainObject;
|
|
1124
|
+
exports.isRateLimitError = isRateLimitError;
|
|
1125
|
+
exports.isTimeoutError = isTimeoutError;
|
|
1059
1126
|
exports.mergeHeaders = mergeHeaders;
|
|
1060
1127
|
exports.resolveUrl = resolveUrl;
|
|
1061
1128
|
exports.sleep = sleep;
|