@vahidkaargar/customized-api-client 0.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/README.md +639 -0
- package/dist/index.cjs +1171 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +317 -0
- package/dist/index.d.ts +317 -0
- package/dist/index.js +1087 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ApiClientError: () => ApiClientError,
|
|
34
|
+
DEFAULT_PAGE_SIZE_CAP: () => DEFAULT_PAGE_SIZE_CAP,
|
|
35
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
36
|
+
IDEMPOTENCY_MAX_LENGTH: () => IDEMPOTENCY_MAX_LENGTH,
|
|
37
|
+
PACKAGE_VERSION: () => PACKAGE_VERSION,
|
|
38
|
+
applyJsonApiHeaders: () => applyJsonApiHeaders,
|
|
39
|
+
applyTransformKeys: () => applyTransformKeys,
|
|
40
|
+
assertValidIdempotencyKey: () => assertValidIdempotencyKey,
|
|
41
|
+
buildCursorPageParams: () => buildCursorPageParams,
|
|
42
|
+
buildJsonApiQuery: () => buildJsonApiQuery,
|
|
43
|
+
buildOffsetPageParams: () => buildOffsetPageParams,
|
|
44
|
+
createApiClient: () => createApiClient,
|
|
45
|
+
createHealthCheck: () => createHealthCheck,
|
|
46
|
+
defaultIdempotencyKey: () => defaultIdempotencyKey,
|
|
47
|
+
dispatchWithRetry: () => dispatchWithRetry,
|
|
48
|
+
etagFromResponseHeaders: () => etagFromResponseHeaders,
|
|
49
|
+
flattenAxiosHeaders: () => flattenAxiosHeaders,
|
|
50
|
+
formatIfMatch: () => formatIfMatch,
|
|
51
|
+
getHeader: () => getHeader,
|
|
52
|
+
getNextPageUrl: () => getNextPageUrl,
|
|
53
|
+
groupValidationErrorsByPointer: () => groupValidationErrorsByPointer,
|
|
54
|
+
indexIncluded: () => indexIncluded,
|
|
55
|
+
isApiClientError: () => isApiClientError,
|
|
56
|
+
isAuthenticationError: () => isAuthenticationError,
|
|
57
|
+
isConflictError: () => isConflictError,
|
|
58
|
+
isForbiddenError: () => isForbiddenError,
|
|
59
|
+
isMutationMethod: () => isMutationMethod,
|
|
60
|
+
isPayloadTooLargeError: () => isPayloadTooLargeError,
|
|
61
|
+
isPreconditionFailedError: () => isPreconditionFailedError,
|
|
62
|
+
isPreconditionRequiredError: () => isPreconditionRequiredError,
|
|
63
|
+
isRetryablePerPolicy: () => isRetryablePerPolicy,
|
|
64
|
+
isValidationError: () => isValidationError,
|
|
65
|
+
normalizeAxiosResponse: () => normalizeAxiosResponse,
|
|
66
|
+
normalizeHttpUrl: () => normalizeHttpUrl,
|
|
67
|
+
parseDeprecationHeaders: () => parseDeprecationHeaders,
|
|
68
|
+
parseJsonApiDocument: () => parseJsonApiDocument,
|
|
69
|
+
parseJsonApiErrorBody: () => parseJsonApiErrorBody,
|
|
70
|
+
parseMultiStatusBody: () => parseMultiStatusBody,
|
|
71
|
+
parsePaginationKind: () => parsePaginationKind,
|
|
72
|
+
parseRetryAfterSeconds: () => parseRetryAfterSeconds,
|
|
73
|
+
pollAsyncResult: () => pollAsyncResult,
|
|
74
|
+
readResourceVersion: () => readResourceVersion,
|
|
75
|
+
redactHeaderRecord: () => redactHeaderRecord,
|
|
76
|
+
resolveAcceptLanguage: () => resolveAcceptLanguage,
|
|
77
|
+
resolveAcceptedLocation: () => resolveAcceptedLocation,
|
|
78
|
+
resolveAuthorizationHeader: () => resolveAuthorizationHeader,
|
|
79
|
+
resolveIncluded: () => resolveIncluded,
|
|
80
|
+
resolveResourcePath: () => resolveResourcePath,
|
|
81
|
+
retryAllowed: () => retryAllowed,
|
|
82
|
+
truncateForLog: () => truncateForLog
|
|
83
|
+
});
|
|
84
|
+
module.exports = __toCommonJS(index_exports);
|
|
85
|
+
|
|
86
|
+
// package.json
|
|
87
|
+
var package_default = {
|
|
88
|
+
name: "@vahidkaargar/customized-api-client",
|
|
89
|
+
version: "0.2.0",
|
|
90
|
+
description: "TypeScript Axios client for JSON:API v1.1 with idempotency, retries, and normalized results",
|
|
91
|
+
type: "module",
|
|
92
|
+
engines: {
|
|
93
|
+
node: ">=20"
|
|
94
|
+
},
|
|
95
|
+
files: [
|
|
96
|
+
"dist",
|
|
97
|
+
"README.md"
|
|
98
|
+
],
|
|
99
|
+
exports: {
|
|
100
|
+
".": {
|
|
101
|
+
types: "./dist/index.d.ts",
|
|
102
|
+
import: "./dist/index.js",
|
|
103
|
+
require: "./dist/index.cjs"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
main: "./dist/index.cjs",
|
|
107
|
+
module: "./dist/index.js",
|
|
108
|
+
types: "./dist/index.d.ts",
|
|
109
|
+
scripts: {
|
|
110
|
+
build: "tsup src/index.ts --dts --format esm,cjs --clean --sourcemap",
|
|
111
|
+
prepublishOnly: "npm run build",
|
|
112
|
+
test: "vitest run",
|
|
113
|
+
"test:watch": "vitest",
|
|
114
|
+
"test:coverage": "vitest run --coverage",
|
|
115
|
+
lint: 'eslint "src/**/*.ts" "test/**/*.ts" && npm run lint:md',
|
|
116
|
+
"lint:md": "markdownlint-cli2",
|
|
117
|
+
"lint:md:fix": "markdownlint-cli2 --fix",
|
|
118
|
+
format: "prettier --write .",
|
|
119
|
+
"format:check": "prettier --check .",
|
|
120
|
+
typecheck: "tsc --noEmit"
|
|
121
|
+
},
|
|
122
|
+
dependencies: {
|
|
123
|
+
axios: "^1.7.9",
|
|
124
|
+
ulid: "^2.3.0"
|
|
125
|
+
},
|
|
126
|
+
devDependencies: {
|
|
127
|
+
"@eslint/js": "^9.17.0",
|
|
128
|
+
"@types/node": "^22.10.2",
|
|
129
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
130
|
+
eslint: "^9.17.0",
|
|
131
|
+
"eslint-config-prettier": "^9.1.0",
|
|
132
|
+
"eslint-plugin-import": "^2.31.0",
|
|
133
|
+
"markdownlint-cli2": "^0.22.1",
|
|
134
|
+
msw: "^2.7.0",
|
|
135
|
+
prettier: "^3.4.2",
|
|
136
|
+
tsup: "^8.3.5",
|
|
137
|
+
typescript: "^5.7.2",
|
|
138
|
+
"typescript-eslint": "^8.18.2",
|
|
139
|
+
vitest: "^2.1.8"
|
|
140
|
+
},
|
|
141
|
+
keywords: [
|
|
142
|
+
"jsonapi",
|
|
143
|
+
"axios",
|
|
144
|
+
"typescript",
|
|
145
|
+
"api-client"
|
|
146
|
+
],
|
|
147
|
+
license: "MIT",
|
|
148
|
+
repository: {
|
|
149
|
+
type: "git",
|
|
150
|
+
url: "git+https://github.com/vahidkaargar/customized-api-client.git"
|
|
151
|
+
},
|
|
152
|
+
bugs: {
|
|
153
|
+
url: "https://github.com/vahidkaargar/customized-api-client/issues"
|
|
154
|
+
},
|
|
155
|
+
homepage: "https://github.com/vahidkaargar/customized-api-client#readme",
|
|
156
|
+
publishConfig: {
|
|
157
|
+
access: "public"
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/create-api-client.ts
|
|
162
|
+
var import_axios = __toESM(require("axios"), 1);
|
|
163
|
+
|
|
164
|
+
// src/types/config.ts
|
|
165
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
166
|
+
var DEFAULT_PAGE_SIZE_CAP = 100;
|
|
167
|
+
|
|
168
|
+
// src/headers/jsonapi-headers.ts
|
|
169
|
+
var JSON_API = "application/vnd.api+json";
|
|
170
|
+
function applyJsonApiHeaders(config, method) {
|
|
171
|
+
const m = method.toUpperCase();
|
|
172
|
+
const headers = { ...config.headers };
|
|
173
|
+
headers.Accept = headers.Accept ?? JSON_API;
|
|
174
|
+
if (hasJsonBody(m, config)) {
|
|
175
|
+
headers["Content-Type"] = headers["Content-Type"] ?? JSON_API;
|
|
176
|
+
}
|
|
177
|
+
return { ...config, headers };
|
|
178
|
+
}
|
|
179
|
+
function hasJsonBody(method, config) {
|
|
180
|
+
if (!["POST", "PATCH", "PUT"].includes(method)) return false;
|
|
181
|
+
return config.data !== void 0 && config.data !== null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/headers/auth.ts
|
|
185
|
+
async function resolveAuthorizationHeader(auth) {
|
|
186
|
+
if (!auth) return void 0;
|
|
187
|
+
if (auth.type === "bearer") {
|
|
188
|
+
const t = await auth.getToken();
|
|
189
|
+
if (!t) return void 0;
|
|
190
|
+
return `Bearer ${t}`;
|
|
191
|
+
}
|
|
192
|
+
const s = await auth.getSecret();
|
|
193
|
+
if (!s) return void 0;
|
|
194
|
+
return `Bearer ${s}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/headers/locale.ts
|
|
198
|
+
async function resolveAcceptLanguage(provider) {
|
|
199
|
+
if (!provider) return void 0;
|
|
200
|
+
const v = await provider();
|
|
201
|
+
return v ?? void 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/headers/idempotency.ts
|
|
205
|
+
var import_ulid = require("ulid");
|
|
206
|
+
var IDEMPOTENCY_MAX_LENGTH = 64;
|
|
207
|
+
function defaultIdempotencyKey() {
|
|
208
|
+
return (0, import_ulid.ulid)();
|
|
209
|
+
}
|
|
210
|
+
function assertValidIdempotencyKey(key) {
|
|
211
|
+
if (!key || key.trim().length === 0) {
|
|
212
|
+
throw new Error("Idempotency-Key must be non-empty");
|
|
213
|
+
}
|
|
214
|
+
if (key.length > IDEMPOTENCY_MAX_LENGTH) {
|
|
215
|
+
throw new Error(`Idempotency-Key exceeds ${String(IDEMPOTENCY_MAX_LENGTH)} characters`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function isMutationMethod(method) {
|
|
219
|
+
const m = method.toUpperCase();
|
|
220
|
+
return m === "POST" || m === "PATCH" || m === "PUT" || m === "DELETE";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/headers/if-match.ts
|
|
224
|
+
function formatIfMatch(version) {
|
|
225
|
+
if (!Number.isFinite(version) || version < 0) {
|
|
226
|
+
throw new Error("If-Match version must be a non-negative finite number");
|
|
227
|
+
}
|
|
228
|
+
return `"v=${String(version)}"`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/headers/resolve-url.ts
|
|
232
|
+
function resolveResourcePath(baseURL, path, mode) {
|
|
233
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
234
|
+
const base = baseURL.replace(/\/+$/, "");
|
|
235
|
+
if (mode === "modeB") {
|
|
236
|
+
if (normalizedPath.startsWith("/api/v1/") && base.endsWith("/api/v1")) {
|
|
237
|
+
const rest = normalizedPath.slice("/api/v1".length);
|
|
238
|
+
return rest.length > 0 ? rest : "/";
|
|
239
|
+
}
|
|
240
|
+
return normalizedPath;
|
|
241
|
+
}
|
|
242
|
+
return normalizedPath.startsWith("/api/v1") ? normalizedPath : `/api/v1${normalizedPath}`;
|
|
243
|
+
}
|
|
244
|
+
function normalizeHttpUrl(fullUrl) {
|
|
245
|
+
return fullUrl;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/retry/policy.ts
|
|
249
|
+
function retryAllowed(ctx) {
|
|
250
|
+
const m = ctx.method.toUpperCase();
|
|
251
|
+
const isRead = m === "GET" || m === "HEAD";
|
|
252
|
+
if (ctx.isNetworkError) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
const s = ctx.status;
|
|
256
|
+
if (s === void 0) return false;
|
|
257
|
+
if (s === 401 || s === 403 || s === 412 || s === 428) return false;
|
|
258
|
+
if (s === 422 || s === 400 || s === 406 || s === 415 || s === 413) return false;
|
|
259
|
+
if (s === 409) {
|
|
260
|
+
if (ctx.primaryErrorCode === "IDEMPOTENCY_KEY_REUSED") return false;
|
|
261
|
+
if (ctx.primaryErrorCode === "IDEMPOTENCY_REQUEST_IN_PROGRESS") return true;
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (!isRead) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (s === 408) return true;
|
|
268
|
+
if (s === 429) return true;
|
|
269
|
+
if (s >= 502 && s <= 504) return true;
|
|
270
|
+
if (s >= 500 && s < 600) return true;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/retry/retry-after.ts
|
|
275
|
+
function parseRetryAfterSeconds(value) {
|
|
276
|
+
if (!value) return void 0;
|
|
277
|
+
const n = Number.parseInt(value, 10);
|
|
278
|
+
if (!Number.isNaN(n)) return n;
|
|
279
|
+
const ms = Date.parse(value);
|
|
280
|
+
if (!Number.isNaN(ms)) {
|
|
281
|
+
return Math.max(0, Math.ceil((ms - Date.now()) / 1e3));
|
|
282
|
+
}
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/http/header-utils.ts
|
|
287
|
+
function flattenAxiosHeaders(headers) {
|
|
288
|
+
if (!headers) return {};
|
|
289
|
+
if (typeof headers.forEach === "function") {
|
|
290
|
+
const out2 = {};
|
|
291
|
+
headers.forEach((value, key) => {
|
|
292
|
+
out2[key.toLowerCase()] = value;
|
|
293
|
+
});
|
|
294
|
+
return out2;
|
|
295
|
+
}
|
|
296
|
+
const o = headers;
|
|
297
|
+
const out = {};
|
|
298
|
+
for (const [k, v] of Object.entries(o)) {
|
|
299
|
+
if (typeof v === "string") out[k.toLowerCase()] = v;
|
|
300
|
+
else if (Array.isArray(v) && v[0]) out[k.toLowerCase()] = v[0];
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
function getHeader(headers, name) {
|
|
305
|
+
return headers[name.toLowerCase()];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/retry/execute-with-retry.ts
|
|
309
|
+
var DEFAULT_MAX_ATTEMPTS = 4;
|
|
310
|
+
var DEFAULT_BASE_MS = 200;
|
|
311
|
+
async function dispatchWithRetry(instance, config, options) {
|
|
312
|
+
const max = options.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
313
|
+
const baseDelay = options.retry?.baseDelayMs ?? DEFAULT_BASE_MS;
|
|
314
|
+
const maxDelay = options.retry?.maxDelayMs ?? 1e4;
|
|
315
|
+
const jitterRatio = options.retry?.jitterRatio ?? 0.2;
|
|
316
|
+
let attempt = 0;
|
|
317
|
+
let lastResponse;
|
|
318
|
+
while (attempt < max) {
|
|
319
|
+
try {
|
|
320
|
+
const res = await instance.request(config);
|
|
321
|
+
lastResponse = res;
|
|
322
|
+
const status = res.status;
|
|
323
|
+
const flat = flattenAxiosHeaders(res.headers);
|
|
324
|
+
const primary = status >= 400 ? extractPrimaryCode(res.data, options.getPrimaryErrorCodeFromBody) : void 0;
|
|
325
|
+
const allowed = retryAllowed({
|
|
326
|
+
method: String(config.method ?? "GET"),
|
|
327
|
+
status,
|
|
328
|
+
primaryErrorCode: primary,
|
|
329
|
+
isNetworkError: false
|
|
330
|
+
});
|
|
331
|
+
if (allowed && attempt < max - 1) {
|
|
332
|
+
const retryAfter = parseRetryAfterSeconds(flat["retry-after"]);
|
|
333
|
+
const delayMs = computeDelay(
|
|
334
|
+
attempt,
|
|
335
|
+
baseDelay,
|
|
336
|
+
maxDelay,
|
|
337
|
+
jitterRatio,
|
|
338
|
+
retryAfter
|
|
339
|
+
);
|
|
340
|
+
await sleep(delayMs);
|
|
341
|
+
attempt += 1;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
return res;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const isNet = isAxiosNetworkError(err);
|
|
347
|
+
const allowed = retryAllowed({
|
|
348
|
+
method: String(config.method ?? "GET"),
|
|
349
|
+
isNetworkError: isNet
|
|
350
|
+
});
|
|
351
|
+
if (!allowed || attempt >= max - 1) {
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
const delayMs = computeDelay(attempt, baseDelay, maxDelay, jitterRatio, void 0);
|
|
355
|
+
await sleep(delayMs);
|
|
356
|
+
attempt += 1;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (lastResponse) return lastResponse;
|
|
360
|
+
throw new Error("dispatchWithRetry: exhausted without response");
|
|
361
|
+
}
|
|
362
|
+
function extractPrimaryCode(body, fn) {
|
|
363
|
+
if (fn) return fn(body);
|
|
364
|
+
if (body && typeof body === "object" && "errors" in body) {
|
|
365
|
+
const errors = body.errors;
|
|
366
|
+
if (Array.isArray(errors) && errors[0]?.code) {
|
|
367
|
+
return errors[0].code;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return void 0;
|
|
371
|
+
}
|
|
372
|
+
function isAxiosNetworkError(err) {
|
|
373
|
+
if (!err || typeof err !== "object") return false;
|
|
374
|
+
const e = err;
|
|
375
|
+
return e.isAxiosError === true && !e.response;
|
|
376
|
+
}
|
|
377
|
+
function computeDelay(attempt, base, max, jitterRatio, retryAfterSec) {
|
|
378
|
+
let exp = Math.min(max, base * 2 ** attempt);
|
|
379
|
+
if (retryAfterSec !== void 0) {
|
|
380
|
+
exp = Math.min(max, Math.max(exp, retryAfterSec * 1e3));
|
|
381
|
+
}
|
|
382
|
+
const jitter = exp * jitterRatio * (Math.random() * 2 - 1);
|
|
383
|
+
return Math.max(0, Math.round(exp + jitter));
|
|
384
|
+
}
|
|
385
|
+
function sleep(ms) {
|
|
386
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/parse/success.ts
|
|
390
|
+
function parseJsonApiDocument(payload) {
|
|
391
|
+
if (!payload || typeof payload !== "object") {
|
|
392
|
+
throw new TypeError("JSON:API document must be an object");
|
|
393
|
+
}
|
|
394
|
+
const o = payload;
|
|
395
|
+
if (!("data" in o)) {
|
|
396
|
+
throw new TypeError('JSON:API document must include "data"');
|
|
397
|
+
}
|
|
398
|
+
return payload;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/security/redact.ts
|
|
402
|
+
var DEFAULT_MAX_BODY_LOG = 2048;
|
|
403
|
+
function truncateForLog(body, maxLen = DEFAULT_MAX_BODY_LOG) {
|
|
404
|
+
let s;
|
|
405
|
+
try {
|
|
406
|
+
s = typeof body === "string" ? body : JSON.stringify(body);
|
|
407
|
+
} catch {
|
|
408
|
+
s = "[Unserializable]";
|
|
409
|
+
}
|
|
410
|
+
if (s.length <= maxLen) return s;
|
|
411
|
+
return `${s.slice(0, maxLen)}\u2026`;
|
|
412
|
+
}
|
|
413
|
+
function redactHeaderRecord(headers) {
|
|
414
|
+
const out = { ...headers };
|
|
415
|
+
for (const key of Object.keys(out)) {
|
|
416
|
+
const lower = key.toLowerCase();
|
|
417
|
+
if (lower === "authorization" || lower === "idempotency-key") {
|
|
418
|
+
out[key] = "[REDACTED]";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/types/api-client-error.ts
|
|
425
|
+
var ApiClientError = class _ApiClientError extends Error {
|
|
426
|
+
constructor(status, errors, primaryCode, responseHeaders, retryAfterSeconds, requestMethod, options) {
|
|
427
|
+
super(_ApiClientError.#messageFrom(errors, status), options);
|
|
428
|
+
this.status = status;
|
|
429
|
+
this.errors = errors;
|
|
430
|
+
this.primaryCode = primaryCode;
|
|
431
|
+
this.responseHeaders = responseHeaders;
|
|
432
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
433
|
+
this.requestMethod = requestMethod;
|
|
434
|
+
}
|
|
435
|
+
status;
|
|
436
|
+
errors;
|
|
437
|
+
primaryCode;
|
|
438
|
+
responseHeaders;
|
|
439
|
+
retryAfterSeconds;
|
|
440
|
+
requestMethod;
|
|
441
|
+
name = "ApiClientError";
|
|
442
|
+
static #messageFrom(errors, status) {
|
|
443
|
+
const first = errors[0];
|
|
444
|
+
const code = first?.code ?? "";
|
|
445
|
+
const detail = first?.detail ?? first?.title ?? "";
|
|
446
|
+
const base = detail || code || `HTTP ${String(status)}`;
|
|
447
|
+
return `[ApiClientError ${String(status)}] ${base}`;
|
|
448
|
+
}
|
|
449
|
+
toJSON() {
|
|
450
|
+
return {
|
|
451
|
+
name: this.name,
|
|
452
|
+
status: this.status,
|
|
453
|
+
errors: this.errors,
|
|
454
|
+
primaryCode: this.primaryCode,
|
|
455
|
+
retryAfterSeconds: this.retryAfterSeconds,
|
|
456
|
+
requestMethod: this.requestMethod,
|
|
457
|
+
responseHeaders: this.responseHeaders ? redactHeaderRecord({ ...this.responseHeaders }) : void 0
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
function isApiClientError(value) {
|
|
462
|
+
return value instanceof ApiClientError;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/parse/errors.ts
|
|
466
|
+
function parseJsonApiErrorBody(status, rawBody, headers, requestMethod) {
|
|
467
|
+
const retryAfterSeconds = headers["retry-after"] ? parseRetryAfterHeader(headers["retry-after"]) : void 0;
|
|
468
|
+
if (rawBody === null || rawBody === void 0 || rawBody === "") {
|
|
469
|
+
return new ApiClientError(
|
|
470
|
+
status,
|
|
471
|
+
[{ detail: `HTTP ${String(status)}`, code: "EMPTY_ERROR_BODY" }],
|
|
472
|
+
"EMPTY_ERROR_BODY",
|
|
473
|
+
headers,
|
|
474
|
+
retryAfterSeconds,
|
|
475
|
+
requestMethod
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (typeof rawBody === "string") {
|
|
479
|
+
try {
|
|
480
|
+
const parsed = JSON.parse(rawBody);
|
|
481
|
+
return parseFromObject(status, parsed, headers, retryAfterSeconds, requestMethod);
|
|
482
|
+
} catch {
|
|
483
|
+
return syntheticError(status, "INVALID_JSON", "Response body is not valid JSON", requestMethod);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (typeof rawBody === "object") {
|
|
487
|
+
return parseFromObject(status, rawBody, headers, retryAfterSeconds, requestMethod);
|
|
488
|
+
}
|
|
489
|
+
return syntheticError(
|
|
490
|
+
status,
|
|
491
|
+
"INVALID_ERROR_DOCUMENT",
|
|
492
|
+
"Unknown error payload shape",
|
|
493
|
+
requestMethod
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
function parseFromObject(status, body, headers, retryAfterSeconds, requestMethod) {
|
|
497
|
+
if (!body || typeof body !== "object") {
|
|
498
|
+
return syntheticError(
|
|
499
|
+
status,
|
|
500
|
+
"INVALID_ERROR_DOCUMENT",
|
|
501
|
+
"Error payload must be an object",
|
|
502
|
+
requestMethod
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const doc = body;
|
|
506
|
+
const errors = doc.errors;
|
|
507
|
+
if (!Array.isArray(errors) || errors.length === 0) {
|
|
508
|
+
return syntheticError(
|
|
509
|
+
status,
|
|
510
|
+
"MISSING_ERRORS_ARRAY",
|
|
511
|
+
"errors[] missing or empty",
|
|
512
|
+
requestMethod
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const list = errors;
|
|
516
|
+
const primaryCode = list[0]?.code;
|
|
517
|
+
return new ApiClientError(status, list, primaryCode, headers, retryAfterSeconds, requestMethod);
|
|
518
|
+
}
|
|
519
|
+
function syntheticError(status, code, detail, requestMethod) {
|
|
520
|
+
return new ApiClientError(status, [{ code, detail }], code, void 0, void 0, requestMethod);
|
|
521
|
+
}
|
|
522
|
+
function parseRetryAfterHeader(v) {
|
|
523
|
+
const n = Number.parseInt(v, 10);
|
|
524
|
+
if (!Number.isNaN(n)) return n;
|
|
525
|
+
const ms = Date.parse(v);
|
|
526
|
+
if (!Number.isNaN(ms)) {
|
|
527
|
+
const delta = Math.max(0, Math.ceil((ms - Date.now()) / 1e3));
|
|
528
|
+
return delta;
|
|
529
|
+
}
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/parse/bulk-207.ts
|
|
534
|
+
function parseMultiStatusBody(raw) {
|
|
535
|
+
if (raw && typeof raw === "object" && "items" in raw) {
|
|
536
|
+
const items = raw.items;
|
|
537
|
+
if (Array.isArray(items)) {
|
|
538
|
+
return items.map((it) => normalizeItem(it));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(raw)) {
|
|
542
|
+
return raw.map((it) => normalizeItem(it));
|
|
543
|
+
}
|
|
544
|
+
return [{ httpStatus: 500, body: raw }];
|
|
545
|
+
}
|
|
546
|
+
function normalizeItem(it) {
|
|
547
|
+
if (it && typeof it === "object") {
|
|
548
|
+
const o = it;
|
|
549
|
+
const s = o.httpStatus ?? o.status;
|
|
550
|
+
const httpStatus = typeof s === "number" && Number.isFinite(s) ? s : 500;
|
|
551
|
+
return { httpStatus, body: o.body ?? o.response ?? it };
|
|
552
|
+
}
|
|
553
|
+
return { httpStatus: 500, body: it };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/parse/accepted-202.ts
|
|
557
|
+
function resolveAcceptedLocation(headers, requestUrl) {
|
|
558
|
+
const flat = flattenAxiosHeaders(headers);
|
|
559
|
+
const loc = getHeader(flat, "location");
|
|
560
|
+
if (!loc) return requestUrl;
|
|
561
|
+
try {
|
|
562
|
+
return new URL(loc, requestUrl).href;
|
|
563
|
+
} catch {
|
|
564
|
+
return loc;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/helpers/transform-keys.ts
|
|
569
|
+
function applyTransformKeys(doc, mode) {
|
|
570
|
+
if (mode === "none") return doc;
|
|
571
|
+
const data = doc.data;
|
|
572
|
+
const mapResource = (r) => {
|
|
573
|
+
const o = { ...r };
|
|
574
|
+
if (o.attributes && typeof o.attributes === "object") {
|
|
575
|
+
o.attributes = camelKeys(o.attributes);
|
|
576
|
+
}
|
|
577
|
+
if (o.meta && typeof o.meta === "object") {
|
|
578
|
+
o.meta = camelKeys(o.meta);
|
|
579
|
+
}
|
|
580
|
+
return o;
|
|
581
|
+
};
|
|
582
|
+
if (data === null) {
|
|
583
|
+
return {
|
|
584
|
+
...doc,
|
|
585
|
+
meta: doc.meta ? camelKeys(doc.meta) : doc.meta
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if (Array.isArray(data)) {
|
|
589
|
+
return {
|
|
590
|
+
...doc,
|
|
591
|
+
data: data.map((r) => mapResource(r)),
|
|
592
|
+
meta: doc.meta ? camelKeys(doc.meta) : doc.meta
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
...doc,
|
|
597
|
+
data: mapResource(data),
|
|
598
|
+
/* v8 ignore next -- document meta absent on single-resource responses */
|
|
599
|
+
meta: doc.meta ? camelKeys(doc.meta) : doc.meta
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function camelKeys(obj) {
|
|
603
|
+
const out = {};
|
|
604
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
605
|
+
out[toCamelCase(k)] = v;
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function toCamelCase(s) {
|
|
610
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/http/normalize-response.ts
|
|
614
|
+
function normalizeAxiosResponse(response, options) {
|
|
615
|
+
const status = response.status;
|
|
616
|
+
const headers = flattenAxiosHeaders(response.headers);
|
|
617
|
+
if (status >= 400) {
|
|
618
|
+
throw parseJsonApiErrorBody(status, response.data, headers, options.requestMethod);
|
|
619
|
+
}
|
|
620
|
+
const normHeaders = normalizeHeaders(headers);
|
|
621
|
+
if (status === 204) {
|
|
622
|
+
return { kind: "no-content", status: 204, headers: normHeaders };
|
|
623
|
+
}
|
|
624
|
+
if (status === 202) {
|
|
625
|
+
const location = resolveAcceptedLocation(headers, options.requestUrl);
|
|
626
|
+
return {
|
|
627
|
+
kind: "accepted",
|
|
628
|
+
status: 202,
|
|
629
|
+
location,
|
|
630
|
+
rawBody: response.data,
|
|
631
|
+
headers: normHeaders
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (status === 207) {
|
|
635
|
+
return {
|
|
636
|
+
kind: "multi-status",
|
|
637
|
+
status: 207,
|
|
638
|
+
items: parseMultiStatusBody(response.data),
|
|
639
|
+
headers: normHeaders
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
if (status === 200 || status === 201) {
|
|
643
|
+
const doc = parseJsonApiDocument(response.data);
|
|
644
|
+
const transformed = applyTransformResponse(
|
|
645
|
+
doc,
|
|
646
|
+
options.transformResponseKeys ?? "none"
|
|
647
|
+
);
|
|
648
|
+
const body = {
|
|
649
|
+
kind: "jsonapi-success",
|
|
650
|
+
status,
|
|
651
|
+
headers: normHeaders,
|
|
652
|
+
document: transformed
|
|
653
|
+
};
|
|
654
|
+
return body;
|
|
655
|
+
}
|
|
656
|
+
throw new ApiClientError(
|
|
657
|
+
status,
|
|
658
|
+
[{ code: "UNSUPPORTED_SUCCESS_STATUS", detail: String(status) }],
|
|
659
|
+
"UNSUPPORTED_SUCCESS_STATUS",
|
|
660
|
+
headers,
|
|
661
|
+
parseRetryAfterSeconds(getHeader(headers, "retry-after")),
|
|
662
|
+
options.requestMethod
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
function normalizeHeaders(headers) {
|
|
666
|
+
const idem = getHeader(headers, "idempotent-replayed");
|
|
667
|
+
return {
|
|
668
|
+
etag: getHeader(headers, "etag"),
|
|
669
|
+
contentLanguage: getHeader(headers, "content-language"),
|
|
670
|
+
idempotentReplayed: idem === "true" || idem === "True",
|
|
671
|
+
retryAfterSeconds: parseRetryAfterSeconds(getHeader(headers, "retry-after"))
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function applyTransformResponse(doc, mode) {
|
|
675
|
+
if (mode === "none") return doc;
|
|
676
|
+
return applyTransformKeys(doc, mode);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/helpers/deprecation.ts
|
|
680
|
+
function parseDeprecationHeaders(headerBag) {
|
|
681
|
+
const flat = flattenAxiosHeaders(headerBag);
|
|
682
|
+
const dep = flat.deprecation;
|
|
683
|
+
const sunset = flat.sunset;
|
|
684
|
+
if (!dep && !sunset) return null;
|
|
685
|
+
return {
|
|
686
|
+
deprecation: dep,
|
|
687
|
+
sunset,
|
|
688
|
+
rawHeaders: flat
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/create-api-client.ts
|
|
693
|
+
function readHeader(ax, name) {
|
|
694
|
+
const h = ax.headers;
|
|
695
|
+
if (!h) return void 0;
|
|
696
|
+
const v = h[name] ?? h[name.toLowerCase()];
|
|
697
|
+
if (Array.isArray(v)) return v[0];
|
|
698
|
+
return typeof v === "string" ? v : void 0;
|
|
699
|
+
}
|
|
700
|
+
function buildRequestUrl(cfg, fallback) {
|
|
701
|
+
if (typeof cfg.url === "string" && /^https?:\/\//i.test(cfg.url)) {
|
|
702
|
+
return cfg.url;
|
|
703
|
+
}
|
|
704
|
+
const base = cfg.baseURL ?? "";
|
|
705
|
+
const p = cfg.url ?? "";
|
|
706
|
+
try {
|
|
707
|
+
return new URL(p, base).href;
|
|
708
|
+
} catch {
|
|
709
|
+
return fallback;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function warnInsecureBaseUrl(baseURL) {
|
|
713
|
+
try {
|
|
714
|
+
const u = new URL(baseURL);
|
|
715
|
+
if (u.protocol !== "http:") return;
|
|
716
|
+
const host = u.hostname.toLowerCase();
|
|
717
|
+
if (host === "localhost" || host === "127.0.0.1") return;
|
|
718
|
+
console.warn(
|
|
719
|
+
"[@vahidkaargar/customized-api-client] baseURL uses HTTP outside localhost; prefer HTTPS in production."
|
|
720
|
+
);
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function safe(fn) {
|
|
725
|
+
try {
|
|
726
|
+
const value = await fn();
|
|
727
|
+
return { ok: true, value };
|
|
728
|
+
} catch (e) {
|
|
729
|
+
if (e instanceof ApiClientError) {
|
|
730
|
+
return { ok: false, error: e };
|
|
731
|
+
}
|
|
732
|
+
throw e;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function createApiClient(config) {
|
|
736
|
+
const mode = config.baseUrlMode ?? "modeB";
|
|
737
|
+
const genKey = config.generateIdempotencyKey ?? defaultIdempotencyKey;
|
|
738
|
+
warnInsecureBaseUrl(config.baseURL);
|
|
739
|
+
const instance = import_axios.default.create({
|
|
740
|
+
baseURL: config.baseURL,
|
|
741
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
742
|
+
validateStatus: () => true,
|
|
743
|
+
headers: {
|
|
744
|
+
Accept: "application/vnd.api+json",
|
|
745
|
+
...config.defaultHeaders
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
instance.interceptors.request.use(async (req) => {
|
|
749
|
+
const method = (req.method ?? "get").toUpperCase();
|
|
750
|
+
const next = applyJsonApiHeaders(req, method);
|
|
751
|
+
const authHeader = await resolveAuthorizationHeader(config.auth);
|
|
752
|
+
if (authHeader) {
|
|
753
|
+
next.headers.Authorization = authHeader;
|
|
754
|
+
}
|
|
755
|
+
const lang = await resolveAcceptLanguage(config.getAcceptLanguage);
|
|
756
|
+
if (lang) {
|
|
757
|
+
next.headers["Accept-Language"] = lang;
|
|
758
|
+
}
|
|
759
|
+
if (isMutationMethod(method)) {
|
|
760
|
+
const h = next.headers;
|
|
761
|
+
const existing = h["Idempotency-Key"] ?? h["idempotency-key"];
|
|
762
|
+
const key = typeof existing === "string" && existing.length > 0 ? existing : genKey();
|
|
763
|
+
assertValidIdempotencyKey(key);
|
|
764
|
+
next.headers["Idempotency-Key"] = key;
|
|
765
|
+
}
|
|
766
|
+
return next;
|
|
767
|
+
});
|
|
768
|
+
instance.interceptors.response.use(
|
|
769
|
+
(res) => {
|
|
770
|
+
const flat = flattenAxiosHeaders(res.headers);
|
|
771
|
+
if (flat["idempotent-replayed"] === "true" && config.onIdempotencyReplay) {
|
|
772
|
+
config.onIdempotencyReplay({
|
|
773
|
+
/* v8 ignore next 2 -- config url/method are strings from axios */
|
|
774
|
+
url: typeof res.config.url === "string" ? res.config.url : void 0,
|
|
775
|
+
method: typeof res.config.method === "string" ? res.config.method : void 0
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const dep = parseDeprecationHeaders(res.headers);
|
|
779
|
+
if (dep && config.onDeprecated) {
|
|
780
|
+
config.onDeprecated(dep);
|
|
781
|
+
}
|
|
782
|
+
return res;
|
|
783
|
+
},
|
|
784
|
+
(err) => Promise.reject(
|
|
785
|
+
/* v8 ignore next -- axios rejects with Error; non-Error is defensive */
|
|
786
|
+
err instanceof Error ? err : new Error(String(err))
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
function resolvePath(path) {
|
|
790
|
+
return resolveResourcePath(config.baseURL, path, mode);
|
|
791
|
+
}
|
|
792
|
+
async function perform(method, url, options) {
|
|
793
|
+
const headers = {};
|
|
794
|
+
if (options.idempotencyKey !== void 0) {
|
|
795
|
+
assertValidIdempotencyKey(options.idempotencyKey);
|
|
796
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
797
|
+
}
|
|
798
|
+
if (options.ifMatchVersion !== void 0) {
|
|
799
|
+
headers["If-Match"] = formatIfMatch(options.ifMatchVersion);
|
|
800
|
+
}
|
|
801
|
+
const axConfig = {
|
|
802
|
+
method,
|
|
803
|
+
url,
|
|
804
|
+
data: options.data,
|
|
805
|
+
headers: { ...headers }
|
|
806
|
+
};
|
|
807
|
+
try {
|
|
808
|
+
const response = await dispatchWithRetry(instance, axConfig, {
|
|
809
|
+
retry: config.retry
|
|
810
|
+
});
|
|
811
|
+
try {
|
|
812
|
+
return normalizeAxiosResponse(response, {
|
|
813
|
+
transformResponseKeys: config.transformResponseKeys,
|
|
814
|
+
requestUrl: buildRequestUrl(response.config, url),
|
|
815
|
+
requestMethod: method.toUpperCase()
|
|
816
|
+
});
|
|
817
|
+
} catch (e) {
|
|
818
|
+
if (e instanceof ApiClientError && e.status === 401 && config.onUnauthorized) {
|
|
819
|
+
await config.onUnauthorized(e);
|
|
820
|
+
}
|
|
821
|
+
throw e;
|
|
822
|
+
}
|
|
823
|
+
} catch (err) {
|
|
824
|
+
if ((0, import_axios.isAxiosError)(err) && err.response) {
|
|
825
|
+
const res = err.response;
|
|
826
|
+
try {
|
|
827
|
+
return normalizeAxiosResponse(res, {
|
|
828
|
+
transformResponseKeys: config.transformResponseKeys,
|
|
829
|
+
requestUrl: buildRequestUrl(res.config, url),
|
|
830
|
+
requestMethod: method.toUpperCase()
|
|
831
|
+
});
|
|
832
|
+
} catch (e) {
|
|
833
|
+
if (e instanceof ApiClientError && e.status === 401 && config.onUnauthorized) {
|
|
834
|
+
await config.onUnauthorized(e);
|
|
835
|
+
}
|
|
836
|
+
throw e;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
throw err;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const client = {
|
|
843
|
+
async get(path, opts) {
|
|
844
|
+
return perform("GET", resolvePath(path), opts ?? {});
|
|
845
|
+
},
|
|
846
|
+
async head(path, opts) {
|
|
847
|
+
return perform("HEAD", resolvePath(path), opts ?? {});
|
|
848
|
+
},
|
|
849
|
+
async post(path, data, opts) {
|
|
850
|
+
return perform("POST", resolvePath(path), { ...opts, data });
|
|
851
|
+
},
|
|
852
|
+
async patch(path, data, opts) {
|
|
853
|
+
return perform("PATCH", resolvePath(path), { ...opts, data });
|
|
854
|
+
},
|
|
855
|
+
async put(path, data, opts) {
|
|
856
|
+
return perform("PUT", resolvePath(path), { ...opts, data });
|
|
857
|
+
},
|
|
858
|
+
async delete(path, opts) {
|
|
859
|
+
return perform("DELETE", resolvePath(path), opts ?? {});
|
|
860
|
+
},
|
|
861
|
+
async request(ax, opts) {
|
|
862
|
+
const method = (ax.method ?? "GET").toUpperCase();
|
|
863
|
+
const rawUrl = ax.url ?? "/";
|
|
864
|
+
const u = (
|
|
865
|
+
/* v8 ignore next -- axios types url as string; non-string is defensive */
|
|
866
|
+
typeof rawUrl === "string" && /^https?:\/\//i.test(rawUrl) ? rawUrl : resolvePath(rawUrl)
|
|
867
|
+
);
|
|
868
|
+
return perform(method, u, {
|
|
869
|
+
...opts,
|
|
870
|
+
data: ax.data,
|
|
871
|
+
idempotencyKey: opts?.idempotencyKey ?? readHeader(ax, "Idempotency-Key")
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
async getByUrl(fullUrl, opts) {
|
|
875
|
+
return perform("GET", fullUrl, opts ?? {});
|
|
876
|
+
},
|
|
877
|
+
async patchWithVersion(path, data, version, opts) {
|
|
878
|
+
return perform("PATCH", resolvePath(path), {
|
|
879
|
+
...opts,
|
|
880
|
+
data,
|
|
881
|
+
ifMatchVersion: version
|
|
882
|
+
});
|
|
883
|
+
},
|
|
884
|
+
safeGet: (path, opts) => safe(() => client.get(path, opts)),
|
|
885
|
+
safePost: (path, data, opts) => safe(() => client.post(path, data, opts)),
|
|
886
|
+
safePatch: (path, data, opts) => safe(() => client.patch(path, data, opts)),
|
|
887
|
+
safePut: (path, data, opts) => safe(() => client.put(path, data, opts)),
|
|
888
|
+
safeDelete: (path, opts) => safe(() => client.delete(path, opts)),
|
|
889
|
+
safeHead: (path, opts) => safe(() => client.head(path, opts)),
|
|
890
|
+
safeRequest: (ax, opts) => safe(() => client.request(ax, opts))
|
|
891
|
+
};
|
|
892
|
+
return client;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/guards.ts
|
|
896
|
+
function isAuthenticationError(e) {
|
|
897
|
+
return isApiErr(e, 401);
|
|
898
|
+
}
|
|
899
|
+
function isForbiddenError(e) {
|
|
900
|
+
return isApiErr(e, 403);
|
|
901
|
+
}
|
|
902
|
+
function isPreconditionRequiredError(e) {
|
|
903
|
+
return isApiErr(e, 428);
|
|
904
|
+
}
|
|
905
|
+
function isPreconditionFailedError(e) {
|
|
906
|
+
return isApiErr(e, 412);
|
|
907
|
+
}
|
|
908
|
+
function isValidationError(e) {
|
|
909
|
+
return isApiErr(e, 422);
|
|
910
|
+
}
|
|
911
|
+
function isConflictError(e) {
|
|
912
|
+
return isApiErr(e, 409);
|
|
913
|
+
}
|
|
914
|
+
function isPayloadTooLargeError(e) {
|
|
915
|
+
return isApiErr(e, 413);
|
|
916
|
+
}
|
|
917
|
+
function isRetryablePerPolicy(e) {
|
|
918
|
+
if (!(e instanceof ApiClientError)) return false;
|
|
919
|
+
return retryAllowed({
|
|
920
|
+
method: e.requestMethod ?? "GET",
|
|
921
|
+
status: e.status,
|
|
922
|
+
primaryErrorCode: e.primaryCode,
|
|
923
|
+
isNetworkError: false
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
function isApiErr(e, status) {
|
|
927
|
+
return e instanceof ApiClientError && e.status === status;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/parse/pagination.ts
|
|
931
|
+
function parsePaginationKind(meta, links) {
|
|
932
|
+
const m = meta ?? {};
|
|
933
|
+
const linksNext = typeof links?.next === "string" ? links.next : void 0;
|
|
934
|
+
if (linksNext) {
|
|
935
|
+
const fromCursor = extractQueryParam(linksNext, "page[cursor]") ?? extractQueryParam(linksNext, "page%5Bcursor%5D");
|
|
936
|
+
if (fromCursor) {
|
|
937
|
+
return { kind: "cursor", hasMore: true, nextCursor: fromCursor };
|
|
938
|
+
}
|
|
939
|
+
const legacy = parseLegacyOffsetFromUrl(linksNext);
|
|
940
|
+
if (legacy) return legacy;
|
|
941
|
+
}
|
|
942
|
+
const lastPage = num(m.last_page);
|
|
943
|
+
const current = num(m.current_page) ?? num(m.page);
|
|
944
|
+
const total = num(m.total);
|
|
945
|
+
const hasMore = bool(m.has_more);
|
|
946
|
+
if (lastPage !== void 0 || current !== void 0 || total !== void 0) {
|
|
947
|
+
return {
|
|
948
|
+
kind: "offset",
|
|
949
|
+
page: current ?? 1,
|
|
950
|
+
totalPages: lastPage,
|
|
951
|
+
total
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
if (hasMore !== void 0) {
|
|
955
|
+
return {
|
|
956
|
+
kind: "cursor",
|
|
957
|
+
hasMore,
|
|
958
|
+
nextCursor: typeof m.next_cursor === "string" ? m.next_cursor : void 0
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
return { kind: "unknown" };
|
|
962
|
+
}
|
|
963
|
+
function getNextPageUrl(links) {
|
|
964
|
+
const n = links?.next;
|
|
965
|
+
return typeof n === "string" ? n : void 0;
|
|
966
|
+
}
|
|
967
|
+
function parseLegacyOffsetFromUrl(url) {
|
|
968
|
+
try {
|
|
969
|
+
const u = new URL(url, "http://local.test");
|
|
970
|
+
const hasPerPage = u.searchParams.has("per_page");
|
|
971
|
+
const pagePlain = u.searchParams.get("page");
|
|
972
|
+
const pageBracket = u.searchParams.get("page[number]");
|
|
973
|
+
if (!hasPerPage && !pagePlain && !pageBracket) return void 0;
|
|
974
|
+
const pageNum = num(pagePlain) ?? num(pageBracket) ?? 1;
|
|
975
|
+
return { kind: "offset", page: pageNum, totalPages: void 0, total: void 0 };
|
|
976
|
+
} catch {
|
|
977
|
+
return void 0;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function num(v) {
|
|
981
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
982
|
+
if (typeof v === "string" && /^-?\d+$/.test(v)) return Number.parseInt(v, 10);
|
|
983
|
+
return void 0;
|
|
984
|
+
}
|
|
985
|
+
function bool(v) {
|
|
986
|
+
if (typeof v === "boolean") return v;
|
|
987
|
+
return void 0;
|
|
988
|
+
}
|
|
989
|
+
function extractQueryParam(url, key) {
|
|
990
|
+
try {
|
|
991
|
+
const u = new URL(url, "http://local.test");
|
|
992
|
+
return u.searchParams.get(key) ?? u.searchParams.get(key.replace(/\[/g, "").replace(/\]/g, ""));
|
|
993
|
+
} catch {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/helpers/query.ts
|
|
999
|
+
function buildJsonApiQuery(input) {
|
|
1000
|
+
const params = {};
|
|
1001
|
+
if (input.filter) {
|
|
1002
|
+
for (const [k, v] of Object.entries(input.filter)) {
|
|
1003
|
+
if (v === void 0) continue;
|
|
1004
|
+
params[`filter[${k}]`] = String(v);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (input.sort?.length) {
|
|
1008
|
+
params.sort = input.sort.join(",");
|
|
1009
|
+
}
|
|
1010
|
+
if (input.fields) {
|
|
1011
|
+
for (const [type, fields] of Object.entries(input.fields)) {
|
|
1012
|
+
params[`fields[${type}]`] = fields.join(",");
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (input.include?.length) {
|
|
1016
|
+
params.include = input.include.join(",");
|
|
1017
|
+
}
|
|
1018
|
+
return params;
|
|
1019
|
+
}
|
|
1020
|
+
function buildOffsetPageParams(input) {
|
|
1021
|
+
const size = Math.min(Math.max(1, input.size), DEFAULT_PAGE_SIZE_CAP);
|
|
1022
|
+
return {
|
|
1023
|
+
"page[number]": input.number,
|
|
1024
|
+
"page[size]": size
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function buildCursorPageParams(input) {
|
|
1028
|
+
const size = Math.min(Math.max(1, input.size), DEFAULT_PAGE_SIZE_CAP);
|
|
1029
|
+
return {
|
|
1030
|
+
"page[cursor]": input.cursor,
|
|
1031
|
+
"page[size]": size
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/helpers/included-index.ts
|
|
1036
|
+
function indexIncluded(included) {
|
|
1037
|
+
const map = /* @__PURE__ */ new Map();
|
|
1038
|
+
if (!included) return map;
|
|
1039
|
+
for (const r of included) {
|
|
1040
|
+
map.set(`${r.type}:${r.id}`, r);
|
|
1041
|
+
}
|
|
1042
|
+
return map;
|
|
1043
|
+
}
|
|
1044
|
+
function resolveIncluded(ref, index) {
|
|
1045
|
+
return index.get(`${ref.type}:${ref.id}`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/helpers/version.ts
|
|
1049
|
+
function readResourceVersion(resource, etagHeader) {
|
|
1050
|
+
const meta = resource.meta;
|
|
1051
|
+
const mv = meta?.version;
|
|
1052
|
+
const fromMeta = coerceUnsigned(mv);
|
|
1053
|
+
if (fromMeta !== void 0) return fromMeta;
|
|
1054
|
+
const etag = etagHeader;
|
|
1055
|
+
if (typeof etag === "string") {
|
|
1056
|
+
const m = /v=(\d+)/i.exec(etag);
|
|
1057
|
+
if (m?.[1]) return Number.parseInt(m[1], 10);
|
|
1058
|
+
}
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
function coerceUnsigned(v) {
|
|
1062
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) return v;
|
|
1063
|
+
if (typeof v === "string" && /^\d+$/.test(v)) return Number.parseInt(v, 10);
|
|
1064
|
+
return void 0;
|
|
1065
|
+
}
|
|
1066
|
+
function etagFromResponseHeaders(headerBag) {
|
|
1067
|
+
const h = flattenAxiosHeaders(headerBag);
|
|
1068
|
+
return getHeader(h, "etag");
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/helpers/form-errors.ts
|
|
1072
|
+
function groupValidationErrorsByPointer(errors) {
|
|
1073
|
+
const out = {};
|
|
1074
|
+
for (const e of errors) {
|
|
1075
|
+
const ptr = e.source?.pointer ?? "/";
|
|
1076
|
+
const msg = e.detail ?? e.title ?? e.code ?? "Error";
|
|
1077
|
+
out[ptr] ??= [];
|
|
1078
|
+
out[ptr].push(msg);
|
|
1079
|
+
}
|
|
1080
|
+
return out;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/helpers/health.ts
|
|
1084
|
+
function createHealthCheck(target) {
|
|
1085
|
+
const client = isGettable(target) ? target : createApiClient(target);
|
|
1086
|
+
return async () => {
|
|
1087
|
+
try {
|
|
1088
|
+
await client.get("/health/live");
|
|
1089
|
+
return true;
|
|
1090
|
+
} catch {
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function isGettable(target) {
|
|
1096
|
+
return typeof target.get === "function";
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/poll-async.ts
|
|
1100
|
+
async function pollAsyncResult(client, initial, options) {
|
|
1101
|
+
const max = options?.maxAttempts ?? 10;
|
|
1102
|
+
const delay = options?.delayMs ?? 200;
|
|
1103
|
+
let url = initial.location;
|
|
1104
|
+
for (let i = 0; i < max; i += 1) {
|
|
1105
|
+
const res = await client.getByUrl(url);
|
|
1106
|
+
if (res.kind !== "accepted") return res;
|
|
1107
|
+
url = res.location;
|
|
1108
|
+
await sleep2(delay);
|
|
1109
|
+
}
|
|
1110
|
+
throw new Error("pollAsyncResult: max attempts exceeded");
|
|
1111
|
+
}
|
|
1112
|
+
function sleep2(ms) {
|
|
1113
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/index.ts
|
|
1117
|
+
var PACKAGE_VERSION = package_default.version;
|
|
1118
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1119
|
+
0 && (module.exports = {
|
|
1120
|
+
ApiClientError,
|
|
1121
|
+
DEFAULT_PAGE_SIZE_CAP,
|
|
1122
|
+
DEFAULT_TIMEOUT_MS,
|
|
1123
|
+
IDEMPOTENCY_MAX_LENGTH,
|
|
1124
|
+
PACKAGE_VERSION,
|
|
1125
|
+
applyJsonApiHeaders,
|
|
1126
|
+
applyTransformKeys,
|
|
1127
|
+
assertValidIdempotencyKey,
|
|
1128
|
+
buildCursorPageParams,
|
|
1129
|
+
buildJsonApiQuery,
|
|
1130
|
+
buildOffsetPageParams,
|
|
1131
|
+
createApiClient,
|
|
1132
|
+
createHealthCheck,
|
|
1133
|
+
defaultIdempotencyKey,
|
|
1134
|
+
dispatchWithRetry,
|
|
1135
|
+
etagFromResponseHeaders,
|
|
1136
|
+
flattenAxiosHeaders,
|
|
1137
|
+
formatIfMatch,
|
|
1138
|
+
getHeader,
|
|
1139
|
+
getNextPageUrl,
|
|
1140
|
+
groupValidationErrorsByPointer,
|
|
1141
|
+
indexIncluded,
|
|
1142
|
+
isApiClientError,
|
|
1143
|
+
isAuthenticationError,
|
|
1144
|
+
isConflictError,
|
|
1145
|
+
isForbiddenError,
|
|
1146
|
+
isMutationMethod,
|
|
1147
|
+
isPayloadTooLargeError,
|
|
1148
|
+
isPreconditionFailedError,
|
|
1149
|
+
isPreconditionRequiredError,
|
|
1150
|
+
isRetryablePerPolicy,
|
|
1151
|
+
isValidationError,
|
|
1152
|
+
normalizeAxiosResponse,
|
|
1153
|
+
normalizeHttpUrl,
|
|
1154
|
+
parseDeprecationHeaders,
|
|
1155
|
+
parseJsonApiDocument,
|
|
1156
|
+
parseJsonApiErrorBody,
|
|
1157
|
+
parseMultiStatusBody,
|
|
1158
|
+
parsePaginationKind,
|
|
1159
|
+
parseRetryAfterSeconds,
|
|
1160
|
+
pollAsyncResult,
|
|
1161
|
+
readResourceVersion,
|
|
1162
|
+
redactHeaderRecord,
|
|
1163
|
+
resolveAcceptLanguage,
|
|
1164
|
+
resolveAcceptedLocation,
|
|
1165
|
+
resolveAuthorizationHeader,
|
|
1166
|
+
resolveIncluded,
|
|
1167
|
+
resolveResourcePath,
|
|
1168
|
+
retryAllowed,
|
|
1169
|
+
truncateForLog
|
|
1170
|
+
});
|
|
1171
|
+
//# sourceMappingURL=index.cjs.map
|