@vahidkaargar/customized-api-client 0.3.0 → 0.4.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 +26 -1
- package/dist/index.cjs +113 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +105 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,7 +229,8 @@ Client-level options for `createApiClient({ … })`:
|
|
|
229
229
|
| `defaultHeaders` | — | Merged into every request |
|
|
230
230
|
| `retry` | see [Retries](#retries) | `maxAttempts`, backoff, jitter |
|
|
231
231
|
| `generateIdempotencyKey` | ULID | Factory for mutation keys |
|
|
232
|
-
| `
|
|
232
|
+
| `locale` | — | `getLocale`, `defaultLocale`, `onLocaleMismatch` — see [Locale](#locale-accept-language--content-language) |
|
|
233
|
+
| `getAcceptLanguage` | — | **Deprecated** — use `locale.getLocale`; sets `Accept-Language` when non-empty |
|
|
233
234
|
| `onIdempotencyReplay` | — | Fired when `Idempotent-Replayed: true` |
|
|
234
235
|
| `onUnauthorized` | — | Fired on normalized **401** |
|
|
235
236
|
| `onDeprecated` | — | Deprecation / sunset headers |
|
|
@@ -247,6 +248,30 @@ auth: { type: 'partner-bearer', getSecret: () => process.env.PARTNER_SECRET }
|
|
|
247
248
|
|
|
248
249
|
If `getToken` / `getSecret` returns `null` or `undefined`, no `Authorization` header is sent.
|
|
249
250
|
|
|
251
|
+
### Locale (Accept-Language / Content-Language)
|
|
252
|
+
|
|
253
|
+
Configure locale once on the client (e.g. for backends with `SetLocaleMiddleware`). This package only sets HTTP headers and optional mismatch reporting — not vue-i18n, `GET /locales`, or `GET /translations`.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const client = createApiClient({
|
|
257
|
+
baseURL: 'https://api.example.com/api/v1',
|
|
258
|
+
locale: {
|
|
259
|
+
getLocale: () => getStoredLocale(), // 'en' | 'fr' | 'fa'
|
|
260
|
+
defaultLocale: 'en', // omit Accept-Language when UI locale is English (server default)
|
|
261
|
+
onLocaleMismatch: import.meta.env.DEV ? 'warn' : undefined,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
| Behavior | Detail |
|
|
267
|
+
|----------|--------|
|
|
268
|
+
| **Accept-Language** | From `locale.getLocale()` on every request via this client |
|
|
269
|
+
| **Omit for default** | When resolved locale matches `defaultLocale` (primary subtag), header is not sent |
|
|
270
|
+
| **Content-Language** | Exposed on success as `res.headers.contentLanguage` |
|
|
271
|
+
| **Mismatch** | If response `Content-Language` differs from requested locale (base tag: `fr` vs `fr-FR` match), `'warn'` or your callback runs — UI locale is never changed |
|
|
272
|
+
|
|
273
|
+
Legacy `getAcceptLanguage` still works; `locale.getLocale` takes precedence when both are set.
|
|
274
|
+
|
|
250
275
|
---
|
|
251
276
|
|
|
252
277
|
## Making requests
|
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
36
36
|
IDEMPOTENCY_MAX_LENGTH: () => IDEMPOTENCY_MAX_LENGTH,
|
|
37
37
|
PACKAGE_VERSION: () => PACKAGE_VERSION,
|
|
38
|
+
acceptLanguageForRequest: () => acceptLanguageForRequest,
|
|
38
39
|
applyJsonApiHeaders: () => applyJsonApiHeaders,
|
|
39
40
|
applyTransformKeys: () => applyTransformKeys,
|
|
40
41
|
assertValidIdempotencyKey: () => assertValidIdempotencyKey,
|
|
@@ -67,8 +68,12 @@ __export(index_exports, {
|
|
|
67
68
|
isPreconditionRequiredError: () => isPreconditionRequiredError,
|
|
68
69
|
isRetryablePerPolicy: () => isRetryablePerPolicy,
|
|
69
70
|
isValidationError: () => isValidationError,
|
|
71
|
+
localesMatch: () => localesMatch,
|
|
70
72
|
normalizeAxiosResponse: () => normalizeAxiosResponse,
|
|
71
73
|
normalizeHttpUrl: () => normalizeHttpUrl,
|
|
74
|
+
normalizeLocaleCode: () => normalizeLocaleCode,
|
|
75
|
+
notifyLocaleMismatch: () => notifyLocaleMismatch,
|
|
76
|
+
parseContentLanguage: () => parseContentLanguage,
|
|
72
77
|
parseDeprecationHeaders: () => parseDeprecationHeaders,
|
|
73
78
|
parseJsonApiDocument: () => parseJsonApiDocument,
|
|
74
79
|
parseJsonApiErrorBody: () => parseJsonApiErrorBody,
|
|
@@ -77,11 +82,14 @@ __export(index_exports, {
|
|
|
77
82
|
parseRetryAfterSeconds: () => parseRetryAfterSeconds,
|
|
78
83
|
pollAsyncResult: () => pollAsyncResult,
|
|
79
84
|
readResourceVersion: () => readResourceVersion,
|
|
85
|
+
readResponseContentLanguage: () => readResponseContentLanguage,
|
|
80
86
|
redactHeaderRecord: () => redactHeaderRecord,
|
|
81
87
|
resolveAcceptLanguage: () => resolveAcceptLanguage,
|
|
82
88
|
resolveAcceptedLocation: () => resolveAcceptedLocation,
|
|
83
89
|
resolveAuthorizationHeader: () => resolveAuthorizationHeader,
|
|
84
90
|
resolveIncluded: () => resolveIncluded,
|
|
91
|
+
resolveLocaleProvider: () => resolveLocaleProvider,
|
|
92
|
+
resolveRequestLocale: () => resolveRequestLocale,
|
|
85
93
|
resolveResourcePath: () => resolveResourcePath,
|
|
86
94
|
retryAllowed: () => retryAllowed,
|
|
87
95
|
truncateForLog: () => truncateForLog
|
|
@@ -91,7 +99,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
91
99
|
// package.json
|
|
92
100
|
var package_default = {
|
|
93
101
|
name: "@vahidkaargar/customized-api-client",
|
|
94
|
-
version: "0.
|
|
102
|
+
version: "0.4.0",
|
|
95
103
|
description: "TypeScript Axios client for JSON:API v1.1 with idempotency, retries, and normalized results",
|
|
96
104
|
type: "module",
|
|
97
105
|
engines: {
|
|
@@ -213,11 +221,85 @@ async function resolveAuthorizationHeader(auth) {
|
|
|
213
221
|
return `Bearer ${s}`;
|
|
214
222
|
}
|
|
215
223
|
|
|
224
|
+
// src/http/header-utils.ts
|
|
225
|
+
function flattenAxiosHeaders(headers) {
|
|
226
|
+
if (!headers) return {};
|
|
227
|
+
if (typeof headers.forEach === "function") {
|
|
228
|
+
const out2 = {};
|
|
229
|
+
headers.forEach((value, key) => {
|
|
230
|
+
out2[key.toLowerCase()] = value;
|
|
231
|
+
});
|
|
232
|
+
return out2;
|
|
233
|
+
}
|
|
234
|
+
const o = headers;
|
|
235
|
+
const out = {};
|
|
236
|
+
for (const [k, v] of Object.entries(o)) {
|
|
237
|
+
if (typeof v === "string") out[k.toLowerCase()] = v;
|
|
238
|
+
else if (Array.isArray(v) && v[0]) out[k.toLowerCase()] = v[0];
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
function getHeader(headers, name) {
|
|
243
|
+
return headers[name.toLowerCase()];
|
|
244
|
+
}
|
|
245
|
+
|
|
216
246
|
// src/headers/locale.ts
|
|
247
|
+
function normalizeLocaleCode(tag) {
|
|
248
|
+
if (tag === void 0) return void 0;
|
|
249
|
+
const trimmed = tag.trim();
|
|
250
|
+
if (!trimmed) return void 0;
|
|
251
|
+
const base = trimmed.split(/[-_]/)[0]?.trim();
|
|
252
|
+
return base ? base.toLowerCase() : void 0;
|
|
253
|
+
}
|
|
254
|
+
function parseContentLanguage(header) {
|
|
255
|
+
if (header === void 0) return void 0;
|
|
256
|
+
const first = header.split(",")[0]?.trim();
|
|
257
|
+
if (!first) return void 0;
|
|
258
|
+
const tag = first.split(";")[0]?.trim();
|
|
259
|
+
return tag && tag.length > 0 ? tag : void 0;
|
|
260
|
+
}
|
|
261
|
+
function localesMatch(a, b) {
|
|
262
|
+
const na = normalizeLocaleCode(a);
|
|
263
|
+
const nb = normalizeLocaleCode(b);
|
|
264
|
+
if (na === void 0 || nb === void 0) return false;
|
|
265
|
+
return na === nb;
|
|
266
|
+
}
|
|
267
|
+
function resolveLocaleProvider(getAcceptLanguage, locale) {
|
|
268
|
+
return locale?.getLocale ?? getAcceptLanguage;
|
|
269
|
+
}
|
|
270
|
+
async function resolveRequestLocale(getAcceptLanguage, locale) {
|
|
271
|
+
return resolveAcceptLanguage(resolveLocaleProvider(getAcceptLanguage, locale));
|
|
272
|
+
}
|
|
273
|
+
function acceptLanguageForRequest(resolved, defaultLocale) {
|
|
274
|
+
if (resolved === void 0) return void 0;
|
|
275
|
+
if (defaultLocale !== void 0 && localesMatch(resolved, defaultLocale)) {
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
return resolved;
|
|
279
|
+
}
|
|
217
280
|
async function resolveAcceptLanguage(provider) {
|
|
218
281
|
if (!provider) return void 0;
|
|
219
282
|
const v = await provider();
|
|
220
|
-
|
|
283
|
+
if (v === null || v === void 0) return void 0;
|
|
284
|
+
const trimmed = v.trim();
|
|
285
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
286
|
+
}
|
|
287
|
+
function readResponseContentLanguage(flatHeaders) {
|
|
288
|
+
return parseContentLanguage(getHeader(flatHeaders, "content-language"));
|
|
289
|
+
}
|
|
290
|
+
function notifyLocaleMismatch(locale, ctx) {
|
|
291
|
+
const handler = locale?.onLocaleMismatch;
|
|
292
|
+
if (!handler) return;
|
|
293
|
+
if (ctx.requested === void 0) return;
|
|
294
|
+
if (localesMatch(ctx.requested, ctx.resolved)) return;
|
|
295
|
+
if (handler === "warn") {
|
|
296
|
+
console.warn(
|
|
297
|
+
"[@vahidkaargar/customized-api-client] Content-Language mismatch",
|
|
298
|
+
ctx
|
|
299
|
+
);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
handler(ctx);
|
|
221
303
|
}
|
|
222
304
|
|
|
223
305
|
// src/headers/idempotency.ts
|
|
@@ -305,28 +387,6 @@ function parseRetryAfterSeconds(value) {
|
|
|
305
387
|
return void 0;
|
|
306
388
|
}
|
|
307
389
|
|
|
308
|
-
// src/http/header-utils.ts
|
|
309
|
-
function flattenAxiosHeaders(headers) {
|
|
310
|
-
if (!headers) return {};
|
|
311
|
-
if (typeof headers.forEach === "function") {
|
|
312
|
-
const out2 = {};
|
|
313
|
-
headers.forEach((value, key) => {
|
|
314
|
-
out2[key.toLowerCase()] = value;
|
|
315
|
-
});
|
|
316
|
-
return out2;
|
|
317
|
-
}
|
|
318
|
-
const o = headers;
|
|
319
|
-
const out = {};
|
|
320
|
-
for (const [k, v] of Object.entries(o)) {
|
|
321
|
-
if (typeof v === "string") out[k.toLowerCase()] = v;
|
|
322
|
-
else if (Array.isArray(v) && v[0]) out[k.toLowerCase()] = v[0];
|
|
323
|
-
}
|
|
324
|
-
return out;
|
|
325
|
-
}
|
|
326
|
-
function getHeader(headers, name) {
|
|
327
|
-
return headers[name.toLowerCase()];
|
|
328
|
-
}
|
|
329
|
-
|
|
330
390
|
// src/retry/execute-with-retry.ts
|
|
331
391
|
var DEFAULT_MAX_ATTEMPTS = 4;
|
|
332
392
|
var DEFAULT_BASE_MS = 200;
|
|
@@ -750,6 +810,7 @@ function createApiClient(config) {
|
|
|
750
810
|
const mode = config.baseUrlMode ?? "modeB";
|
|
751
811
|
const genKey = config.generateIdempotencyKey ?? defaultIdempotencyKey;
|
|
752
812
|
warnInsecureBaseUrl(config.baseURL);
|
|
813
|
+
const localeByRequest = /* @__PURE__ */ new WeakMap();
|
|
753
814
|
const instance = import_axios.default.create({
|
|
754
815
|
baseURL: config.baseURL,
|
|
755
816
|
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -766,9 +827,15 @@ function createApiClient(config) {
|
|
|
766
827
|
if (authHeader) {
|
|
767
828
|
next.headers.Authorization = authHeader;
|
|
768
829
|
}
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
830
|
+
const resolved = await resolveRequestLocale(
|
|
831
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- legacy `getAcceptLanguage` fallback
|
|
832
|
+
config.getAcceptLanguage,
|
|
833
|
+
config.locale
|
|
834
|
+
);
|
|
835
|
+
localeByRequest.set(next, resolved);
|
|
836
|
+
const toSend = acceptLanguageForRequest(resolved, config.locale?.defaultLocale);
|
|
837
|
+
if (toSend) {
|
|
838
|
+
next.headers["Accept-Language"] = toSend;
|
|
772
839
|
}
|
|
773
840
|
if (isMutationMethod(method)) {
|
|
774
841
|
const h = next.headers;
|
|
@@ -794,6 +861,17 @@ function createApiClient(config) {
|
|
|
794
861
|
if (dep && config.onDeprecated) {
|
|
795
862
|
config.onDeprecated(dep);
|
|
796
863
|
}
|
|
864
|
+
const contentLang = readResponseContentLanguage(flat);
|
|
865
|
+
if (contentLang) {
|
|
866
|
+
notifyLocaleMismatch(config.locale, {
|
|
867
|
+
requested: localeByRequest.get(res.config),
|
|
868
|
+
resolved: contentLang,
|
|
869
|
+
/* v8 ignore start -- @preserve axios config url/method are strings */
|
|
870
|
+
url: typeof res.config.url === "string" ? res.config.url : void 0,
|
|
871
|
+
method: typeof res.config.method === "string" ? res.config.method : void 0
|
|
872
|
+
/* v8 ignore stop -- @preserve */
|
|
873
|
+
});
|
|
874
|
+
}
|
|
797
875
|
return res;
|
|
798
876
|
},
|
|
799
877
|
(err) => Promise.reject(
|
|
@@ -1178,6 +1256,7 @@ var PACKAGE_VERSION = package_default.version;
|
|
|
1178
1256
|
DEFAULT_TIMEOUT_MS,
|
|
1179
1257
|
IDEMPOTENCY_MAX_LENGTH,
|
|
1180
1258
|
PACKAGE_VERSION,
|
|
1259
|
+
acceptLanguageForRequest,
|
|
1181
1260
|
applyJsonApiHeaders,
|
|
1182
1261
|
applyTransformKeys,
|
|
1183
1262
|
assertValidIdempotencyKey,
|
|
@@ -1210,8 +1289,12 @@ var PACKAGE_VERSION = package_default.version;
|
|
|
1210
1289
|
isPreconditionRequiredError,
|
|
1211
1290
|
isRetryablePerPolicy,
|
|
1212
1291
|
isValidationError,
|
|
1292
|
+
localesMatch,
|
|
1213
1293
|
normalizeAxiosResponse,
|
|
1214
1294
|
normalizeHttpUrl,
|
|
1295
|
+
normalizeLocaleCode,
|
|
1296
|
+
notifyLocaleMismatch,
|
|
1297
|
+
parseContentLanguage,
|
|
1215
1298
|
parseDeprecationHeaders,
|
|
1216
1299
|
parseJsonApiDocument,
|
|
1217
1300
|
parseJsonApiErrorBody,
|
|
@@ -1220,11 +1303,14 @@ var PACKAGE_VERSION = package_default.version;
|
|
|
1220
1303
|
parseRetryAfterSeconds,
|
|
1221
1304
|
pollAsyncResult,
|
|
1222
1305
|
readResourceVersion,
|
|
1306
|
+
readResponseContentLanguage,
|
|
1223
1307
|
redactHeaderRecord,
|
|
1224
1308
|
resolveAcceptLanguage,
|
|
1225
1309
|
resolveAcceptedLocation,
|
|
1226
1310
|
resolveAuthorizationHeader,
|
|
1227
1311
|
resolveIncluded,
|
|
1312
|
+
resolveLocaleProvider,
|
|
1313
|
+
resolveRequestLocale,
|
|
1228
1314
|
resolveResourcePath,
|
|
1229
1315
|
retryAllowed,
|
|
1230
1316
|
truncateForLog
|