@classytic/arc-next 0.4.1 → 0.6.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 +272 -660
- package/dist/api.d.ts +115 -194
- package/dist/api.js +69 -116
- package/dist/cache.d.ts +125 -0
- package/dist/cache.js +211 -0
- package/dist/client.d.ts +295 -5
- package/dist/client.js +392 -12
- package/dist/hooks.d.ts +162 -22
- package/dist/hooks.js +249 -89
- package/dist/mutation.d.ts +18 -1
- package/dist/mutation.js +20 -1
- package/dist/prefetch.d.ts +36 -2
- package/dist/prefetch.js +47 -3
- package/dist/presets/bulk.d.ts +43 -0
- package/dist/presets/bulk.js +50 -0
- package/dist/presets/search.d.ts +56 -0
- package/dist/presets/search.js +60 -0
- package/dist/presets/slug.d.ts +28 -0
- package/dist/presets/slug.js +27 -0
- package/dist/presets/soft-delete.d.ts +34 -0
- package/dist/presets/soft-delete.js +45 -0
- package/dist/presets/tree.d.ts +32 -0
- package/dist/presets/tree.js +47 -0
- package/dist/query.d.ts +86 -69
- package/dist/query.js +59 -161
- package/dist/sse.d.ts +115 -36
- package/dist/sse.js +179 -103
- package/dist/upload.d.ts +181 -0
- package/dist/upload.js +346 -0
- package/dist/ws.d.ts +167 -0
- package/dist/ws.js +274 -0
- package/package.json +39 -1
package/dist/client.js
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
//#region src/client.ts
|
|
2
2
|
/**
|
|
3
|
+
* Canonical error codes arc and repo-core emit on `json.code`. Single
|
|
4
|
+
* top-level slot — arc 2.13's `createError` lifts business codes from
|
|
5
|
+
* `details` to top-level so `repo-core`'s `toErrorContract` round-trips
|
|
6
|
+
* them on the wire. There is no separate `detailsCode` slot; everything
|
|
7
|
+
* lives at `error.code`.
|
|
8
|
+
*
|
|
9
|
+
* Three families compose this list:
|
|
10
|
+
* 1. **repo-core canonical** (`validation_error`, `not_found`, ...) — RFC 7807
|
|
11
|
+
* / Stripe-shaped lowercase + snake_case. Cross-package universals.
|
|
12
|
+
* 2. **arc hierarchical** (`arc.forbidden`, `arc.validation_error`,
|
|
13
|
+
* `arc.org.access_denied`, ...) — what arc's `errorHandlerPlugin`
|
|
14
|
+
* emits for HTTP-status throws + arc-classified errors.
|
|
15
|
+
* 3. **arc business** (`ORG_CONTEXT_REQUIRED`, `ALL_FIELDS_STRIPPED`,
|
|
16
|
+
* `OWNERSHIP_DENIED`, ...) — emitted by mixins / org guards via
|
|
17
|
+
* `createError(status, msg, { code })`. The UPPER_SNAKE form is
|
|
18
|
+
* intentional: these are reason codes, not HTTP-status codes.
|
|
19
|
+
*
|
|
20
|
+
* `(string & {})` keeps the type open so domain packages and custom
|
|
21
|
+
* `errorMappers` codes still satisfy it.
|
|
22
|
+
*/
|
|
23
|
+
const KNOWN_ARC_ERROR_CODES = [
|
|
24
|
+
"validation_error",
|
|
25
|
+
"not_found",
|
|
26
|
+
"conflict",
|
|
27
|
+
"unauthorized",
|
|
28
|
+
"forbidden",
|
|
29
|
+
"rate_limited",
|
|
30
|
+
"idempotency_conflict",
|
|
31
|
+
"precondition_failed",
|
|
32
|
+
"internal_error",
|
|
33
|
+
"service_unavailable",
|
|
34
|
+
"timeout",
|
|
35
|
+
"arc.bad_request",
|
|
36
|
+
"arc.unauthorized",
|
|
37
|
+
"arc.forbidden",
|
|
38
|
+
"arc.not_found",
|
|
39
|
+
"arc.conflict",
|
|
40
|
+
"arc.unprocessable_entity",
|
|
41
|
+
"arc.rate_limited",
|
|
42
|
+
"arc.internal_error",
|
|
43
|
+
"arc.bad_gateway",
|
|
44
|
+
"arc.service_unavailable",
|
|
45
|
+
"arc.gateway_timeout",
|
|
46
|
+
"arc.validation_error",
|
|
47
|
+
"arc.invalid_id",
|
|
48
|
+
"arc.org.selection_required",
|
|
49
|
+
"arc.org.access_denied",
|
|
50
|
+
"ORG_CONTEXT_REQUIRED",
|
|
51
|
+
"ORG_ROLE_REQUIRED",
|
|
52
|
+
"OWNERSHIP_DENIED",
|
|
53
|
+
"MIXED_UPDATE_SHAPE",
|
|
54
|
+
"ALL_FIELDS_STRIPPED",
|
|
55
|
+
"BEFORE_RESTORE_HOOK_ERROR",
|
|
56
|
+
"duplicate_key"
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
3
59
|
* Rich API error with status code, response payload, and request metadata.
|
|
4
60
|
* Extends Error so existing `catch (e) { if (e instanceof Error) }` still works.
|
|
5
61
|
*
|
|
@@ -28,9 +84,71 @@ var ArcApiError = class extends Error {
|
|
|
28
84
|
this.endpoint = options.endpoint;
|
|
29
85
|
this.method = options.method;
|
|
30
86
|
}
|
|
31
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Canonical error code from arc's wire envelope (`json.code`).
|
|
89
|
+
*
|
|
90
|
+
* Arc 2.13 + `repo-core` 0.4 emit one canonical {@link ErrorContract}
|
|
91
|
+
* shape — `{ code, message, status, details? }` — with the business
|
|
92
|
+
* code at top-level. Hosts switch on `error.code` directly:
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* if (error.code === 'ORG_CONTEXT_REQUIRED') promptOrgSelector();
|
|
96
|
+
* if (error.code === 'arc.not_found') router.replace('/404');
|
|
97
|
+
* if (error.code === 'duplicate_key') showRetryAsAdmin();
|
|
98
|
+
*/
|
|
99
|
+
get code() {
|
|
100
|
+
const j = this.json;
|
|
101
|
+
return j && typeof j.code === "string" ? j.code : null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Canonical structured details — populated for validation failures
|
|
105
|
+
* (one entry per offending field) and duplicate-key conflicts (one entry
|
|
106
|
+
* per offending field). Shape matches `repo-core`'s {@link ErrorDetail}:
|
|
107
|
+
* `{ path?, code, message, meta? }`. Returns `null` for non-arc backends
|
|
108
|
+
* or responses without details.
|
|
109
|
+
*/
|
|
110
|
+
get details() {
|
|
111
|
+
const j = this.json;
|
|
112
|
+
return Array.isArray(j?.details) ? j.details : null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Extract field-level validation errors as `{ field: message }` map.
|
|
116
|
+
*
|
|
117
|
+
* Reads the canonical `ErrorContract.details: ErrorDetail[]` shape first
|
|
118
|
+
* (what arc 2.13 + repo-core emit), then falls back to legacy shapes for
|
|
119
|
+
* non-arc backends:
|
|
120
|
+
* 1. `details: [{ path, code, message }]` — canonical (arc / repo-core).
|
|
121
|
+
* 2. `errors: { email: 'invalid' }` — record form (legacy app handlers).
|
|
122
|
+
* 3. `details: { errors: [{ field|instancePath, message }] }` — pre-2.13 AJV.
|
|
123
|
+
* 4. `errors: [...]` at the top level — third-party frameworks.
|
|
124
|
+
*/
|
|
32
125
|
get fieldErrors() {
|
|
33
|
-
|
|
126
|
+
const j = this.json;
|
|
127
|
+
if (!j) return null;
|
|
128
|
+
if (Array.isArray(j.details) && j.details.length > 0) {
|
|
129
|
+
const map = {};
|
|
130
|
+
for (const d of j.details) {
|
|
131
|
+
if (!d || typeof d !== "object") continue;
|
|
132
|
+
const key = typeof d.path === "string" ? d.path : "";
|
|
133
|
+
const msg = typeof d.message === "string" ? d.message : "Invalid value";
|
|
134
|
+
if (key && !(key in map)) map[key] = msg;
|
|
135
|
+
}
|
|
136
|
+
if (Object.keys(map).length > 0) return map;
|
|
137
|
+
}
|
|
138
|
+
if (j.errors && !Array.isArray(j.errors) && typeof j.errors === "object") return j.errors;
|
|
139
|
+
const detailsObj = j.details;
|
|
140
|
+
const errorList = Array.isArray(j.errors) ? j.errors : Array.isArray(detailsObj?.errors) ? detailsObj.errors : null;
|
|
141
|
+
if (!errorList) return null;
|
|
142
|
+
const map = {};
|
|
143
|
+
for (const item of errorList) {
|
|
144
|
+
if (!item || typeof item !== "object") continue;
|
|
145
|
+
const e = item;
|
|
146
|
+
const key = typeof e.field === "string" && e.field || typeof e.instancePath === "string" && e.instancePath.replace(/^\//, "") || typeof e.path === "string" && e.path || typeof e.params?.missingProperty === "string" && e.params.missingProperty || "";
|
|
147
|
+
if (!key) continue;
|
|
148
|
+
const msg = typeof e.message === "string" ? e.message : "Invalid value";
|
|
149
|
+
if (!(key in map)) map[key] = msg;
|
|
150
|
+
}
|
|
151
|
+
return Object.keys(map).length > 0 ? map : null;
|
|
34
152
|
}
|
|
35
153
|
};
|
|
36
154
|
/**
|
|
@@ -39,6 +157,91 @@ var ArcApiError = class extends Error {
|
|
|
39
157
|
function isArcApiError(error) {
|
|
40
158
|
return error instanceof ArcApiError;
|
|
41
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Detects request cancellation (AbortSignal triggered) regardless of runtime.
|
|
162
|
+
*
|
|
163
|
+
* Filtering out abort errors from real failures is a common need — without it,
|
|
164
|
+
* unmounting a React component mid-fetch produces noisy logs and bogus error
|
|
165
|
+
* toasts that look like API failures. Use this predicate to skip the catch
|
|
166
|
+
* branch when the request was deliberately cancelled.
|
|
167
|
+
*
|
|
168
|
+
* Handles the three AbortError shapes you'll see in the wild:
|
|
169
|
+
* 1. Browser `DOMException { name: 'AbortError' }`
|
|
170
|
+
* 2. Node 18+ / undici `Error { name: 'AbortError' }` (no DOMException)
|
|
171
|
+
* 3. Some polyfills / older runtimes where the error has `code: 'ERR_ABORTED'`
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* try {
|
|
175
|
+
* await api.getAll({ options: { signal } });
|
|
176
|
+
* } catch (err) {
|
|
177
|
+
* if (isAbortError(err)) return; // user navigated away — silence
|
|
178
|
+
* showToast('Failed to load: ' + err.message);
|
|
179
|
+
* }
|
|
180
|
+
*/
|
|
181
|
+
function isAbortError(error) {
|
|
182
|
+
if (!error) return false;
|
|
183
|
+
if (typeof error !== "object") return false;
|
|
184
|
+
const e = error;
|
|
185
|
+
if (e.name === "AbortError") return true;
|
|
186
|
+
if (e.code === "ERR_ABORTED") return true;
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Generic check: is this an `ArcApiError` carrying a specific `code`?
|
|
191
|
+
* Single-slot — arc 2.13 + repo-core 0.4 emit one canonical `code` at
|
|
192
|
+
* top-level. Pass either the canonical lowercase form (`'arc.not_found'`,
|
|
193
|
+
* `'validation_error'`) or arc's UPPER_SNAKE business form
|
|
194
|
+
* (`'ORG_CONTEXT_REQUIRED'`).
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* if (isArcErrorCode(error, 'duplicate_key')) showRetryUI();
|
|
198
|
+
* if (isArcErrorCode(error, 'ORG_CONTEXT_REQUIRED')) promptOrgSelector();
|
|
199
|
+
*/
|
|
200
|
+
function isArcErrorCode(error, code) {
|
|
201
|
+
return isArcApiError(error) && error.code === code;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Specific predicate for arc's bulk-preset + orgGuard safety code.
|
|
205
|
+
*
|
|
206
|
+
* Arc's bulk endpoints (`POST/PATCH/DELETE /:resource/bulk`) reject any call
|
|
207
|
+
* where `request.scope.organizationId` is missing — the wire signal is
|
|
208
|
+
* `403 { code: 'ORG_CONTEXT_REQUIRED', message, status: 403 }`. Hosts hitting
|
|
209
|
+
* this need to call `configureAuth({ getOrgId })` before retrying.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* try { await api.bulkCreate({ data: [...] }); }
|
|
213
|
+
* catch (e) {
|
|
214
|
+
* if (isOrgContextRequiredError(e)) {
|
|
215
|
+
* console.warn('Bulk requires org context. Configure: configureAuth({ getOrgId })');
|
|
216
|
+
* } else throw e;
|
|
217
|
+
* }
|
|
218
|
+
*/
|
|
219
|
+
function isOrgContextRequiredError(error) {
|
|
220
|
+
return isArcErrorCode(error, "ORG_CONTEXT_REQUIRED");
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Specific predicate for validation failures (Fastify AJV + Mongoose
|
|
224
|
+
* ValidationError). When true, `error.fieldErrors` is populated with the
|
|
225
|
+
* `{ field: message }` map. Matches arc's `arc.validation_error` and the
|
|
226
|
+
* canonical `validation_error` from repo-core.
|
|
227
|
+
*/
|
|
228
|
+
function isValidationError(error) {
|
|
229
|
+
if (!isArcApiError(error)) return false;
|
|
230
|
+
return error.code === "arc.validation_error" || error.code === "validation_error";
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Specific predicate for unique-constraint violations. Arc's errorHandler
|
|
234
|
+
* classifies these uniformly across MongoDB E11000, Postgres 23505,
|
|
235
|
+
* Prisma P2002 → `arc.conflict` (with `details[].code === 'duplicate_key'`).
|
|
236
|
+
*/
|
|
237
|
+
function isDuplicateKeyError(error) {
|
|
238
|
+
if (!isArcApiError(error)) return false;
|
|
239
|
+
if (error.code === "arc.conflict" || error.code === "duplicate_key" || error.code === "conflict") {
|
|
240
|
+
if (error.code === "arc.conflict") return error.details?.some((d) => d.code === "duplicate_key") ?? false;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
42
245
|
let clientConfig = null;
|
|
43
246
|
/**
|
|
44
247
|
* Configure the API client. Call once at app init before any API requests.
|
|
@@ -73,17 +276,22 @@ function isAutoIdempotency() {
|
|
|
73
276
|
return clientConfig?.autoIdempotency ?? false;
|
|
74
277
|
}
|
|
75
278
|
let authConfig = null;
|
|
279
|
+
let hasWarnedAsyncToken = false;
|
|
76
280
|
/**
|
|
77
281
|
* Configure auth context for automatic token/orgId injection.
|
|
78
282
|
* When configured, hooks auto-inject these values so you don't need to pass them manually.
|
|
79
283
|
*
|
|
80
284
|
* **SSR safety:** This sets module-level state. Call only in client-side code.
|
|
81
285
|
*
|
|
286
|
+
* **Token resolution is synchronous.** `getToken` must return `string | null`
|
|
287
|
+
* synchronously — never a Promise. See {@link AuthConfig.getToken} for guidance
|
|
288
|
+
* on bridging async auth libraries via cached values.
|
|
289
|
+
*
|
|
82
290
|
* @example
|
|
83
291
|
* // Cookie auth (no token needed)
|
|
84
292
|
* configureAuth({ getOrgId: () => currentOrg.id });
|
|
85
293
|
*
|
|
86
|
-
* // Bearer auth
|
|
294
|
+
* // Bearer auth — token is cached synchronously by the auth library
|
|
87
295
|
* configureAuth({
|
|
88
296
|
* getToken: () => session?.accessToken ?? null,
|
|
89
297
|
* getOrgId: () => currentOrg?.id ?? null,
|
|
@@ -92,16 +300,58 @@ let authConfig = null;
|
|
|
92
300
|
function configureAuth(config) {
|
|
93
301
|
if (typeof window === "undefined") console.warn("[arc-next] configureAuth() called on the server. This sets module-level state that persists across requests. Call only in client-side code (e.g., a 'use client' provider).");
|
|
94
302
|
authConfig = config;
|
|
303
|
+
hasWarnedAsyncToken = false;
|
|
304
|
+
}
|
|
305
|
+
function readToken(getToken) {
|
|
306
|
+
if (!getToken) return null;
|
|
307
|
+
const result = getToken();
|
|
308
|
+
if (result && typeof result.then === "function") {
|
|
309
|
+
if (!hasWarnedAsyncToken) {
|
|
310
|
+
hasWarnedAsyncToken = true;
|
|
311
|
+
console.warn("[arc-next] configureAuth({ getToken }) returned a Promise. Tokens MUST resolve synchronously — async returns are dropped and requests will be unauthenticated. Cache your token (localStorage, memory, signal) and have getToken() return the cached value.");
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return result ?? null;
|
|
95
316
|
}
|
|
96
317
|
/**
|
|
97
318
|
* Get the current auth context. Returns nulls when not configured.
|
|
98
319
|
*/
|
|
99
320
|
function getAuthContext() {
|
|
100
321
|
return {
|
|
101
|
-
token: authConfig?.getToken
|
|
322
|
+
token: readToken(authConfig?.getToken),
|
|
102
323
|
organizationId: authConfig?.getOrgId?.() ?? null
|
|
103
324
|
};
|
|
104
325
|
}
|
|
326
|
+
/** @internal — exposed for tests; resets the dev-warn dedup flag. */
|
|
327
|
+
function _resetAuthWarnings() {
|
|
328
|
+
hasWarnedAsyncToken = false;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Build an auth-aware URL using the global client + auth singletons.
|
|
332
|
+
*
|
|
333
|
+
* Single source of truth for {@link import('./sse.js').buildSseUrl} (HTTP) and
|
|
334
|
+
* {@link import('./ws.js').buildWsUrl} (WebSocket). Both delegate here so the
|
|
335
|
+
* auth-injection rule (org always, token only when `authMode !== 'cookie'`)
|
|
336
|
+
* stays in one place and can't drift.
|
|
337
|
+
*
|
|
338
|
+
* @param path Path appended to the base URL (leading slash recommended).
|
|
339
|
+
* @param params Caller-supplied params; merged with auth params. Caller wins on key collision.
|
|
340
|
+
* @param protocol `'http'` (default) or `'ws'` — controls the protocol rewrite.
|
|
341
|
+
*/
|
|
342
|
+
function buildStreamUrl(path, params = {}, protocol = "http") {
|
|
343
|
+
const auth = getAuthContext();
|
|
344
|
+
const qs = new URLSearchParams();
|
|
345
|
+
for (const [key, value] of Object.entries(params)) {
|
|
346
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
347
|
+
qs.set(key, String(value));
|
|
348
|
+
}
|
|
349
|
+
if (auth.organizationId && !qs.has("organizationId")) qs.set("organizationId", auth.organizationId);
|
|
350
|
+
if (auth.token && getAuthMode() !== "cookie" && !qs.has("token")) qs.set("token", auth.token);
|
|
351
|
+
const origin = protocol === "ws" ? getBaseUrl().replace(/^http(s?):\/\//, "ws$1://") : getBaseUrl();
|
|
352
|
+
const suffix = qs.toString();
|
|
353
|
+
return `${origin}${path}${suffix ? `?${suffix}` : ""}`;
|
|
354
|
+
}
|
|
105
355
|
/**
|
|
106
356
|
* Create an isolated API client for a specific backend.
|
|
107
357
|
* Use this when your app needs to talk to multiple APIs with different auth.
|
|
@@ -132,7 +382,7 @@ function createClient(config) {
|
|
|
132
382
|
request: (method, endpoint, options) => {
|
|
133
383
|
if (clientAuth) {
|
|
134
384
|
const resolved = { ...options };
|
|
135
|
-
if (resolved.token === void 0 && clientAuth.getToken) resolved.token = clientAuth.getToken
|
|
385
|
+
if (resolved.token === void 0 && clientAuth.getToken) resolved.token = readToken(clientAuth.getToken);
|
|
136
386
|
if (resolved.organizationId === void 0 && clientAuth.getOrgId) resolved.organizationId = clientAuth.getOrgId();
|
|
137
387
|
if (clientCfg.authMode === "header" && resolved.token) {
|
|
138
388
|
resolved.headerOptions = {
|
|
@@ -152,17 +402,115 @@ function createClient(config) {
|
|
|
152
402
|
};
|
|
153
403
|
}
|
|
154
404
|
/**
|
|
405
|
+
* Create an `ArcClient` wired to the global `configureClient` + `configureAuth` setup.
|
|
406
|
+
*
|
|
407
|
+
* Removes the boilerplate every consumer SDK writes by hand (auth-injection adapter
|
|
408
|
+
* + `BaseApi` constructor with `client: { request: handleApiRequest, config: ... }`).
|
|
409
|
+
* The returned client reads `getToken` / `getOrgId` lazily on each request, so token
|
|
410
|
+
* rotation in the global auth layer takes effect immediately.
|
|
411
|
+
*
|
|
412
|
+
* Equivalent to `createClient(...)` with `getToken`/`getOrgId` pulled from
|
|
413
|
+
* `getAuthContext()`. Pass `overrides` to customize specific fields without
|
|
414
|
+
* rebuilding the whole transport.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* // App init (somewhere in a "use client" provider)
|
|
418
|
+
* configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! });
|
|
419
|
+
* configureAuth({
|
|
420
|
+
* getToken: () => session?.accessToken ?? null,
|
|
421
|
+
* getOrgId: () => currentOrg?.id ?? null,
|
|
422
|
+
* });
|
|
423
|
+
*
|
|
424
|
+
* // SDK module
|
|
425
|
+
* import { createAuthAwareClient } from '@classytic/arc-next/client';
|
|
426
|
+
* import { createCrudApi } from '@classytic/arc-next/api';
|
|
427
|
+
*
|
|
428
|
+
* const client = createAuthAwareClient();
|
|
429
|
+
* export const productsApi = createCrudApi('products', { client });
|
|
430
|
+
*
|
|
431
|
+
* // With per-call overrides
|
|
432
|
+
* const analyticsClient = createAuthAwareClient({
|
|
433
|
+
* baseUrl: 'https://analytics.example.com',
|
|
434
|
+
* authMode: 'header',
|
|
435
|
+
* headerName: 'x-api-key',
|
|
436
|
+
* });
|
|
437
|
+
*/
|
|
438
|
+
function createAuthAwareClient(overrides = {}) {
|
|
439
|
+
return createClient({
|
|
440
|
+
baseUrl: overrides.baseUrl ?? getBaseUrl(),
|
|
441
|
+
authMode: overrides.authMode ?? getAuthMode(),
|
|
442
|
+
autoIdempotency: overrides.autoIdempotency ?? isAutoIdempotency(),
|
|
443
|
+
elevated: overrides.elevated ?? clientConfig?.elevated,
|
|
444
|
+
...overrides,
|
|
445
|
+
getToken: overrides.getToken ?? (() => readToken(authConfig?.getToken)),
|
|
446
|
+
getOrgId: overrides.getOrgId ?? (() => authConfig?.getOrgId?.() ?? null),
|
|
447
|
+
headerName: overrides.headerName ?? authConfig?.headerName
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
155
451
|
* Get auth context for a specific client instance, falling back to global.
|
|
156
452
|
*/
|
|
157
453
|
function getClientAuthContext(client) {
|
|
158
454
|
if (client?.auth) return {
|
|
159
|
-
token: client.auth.getToken
|
|
455
|
+
token: readToken(client.auth.getToken) ?? readToken(authConfig?.getToken),
|
|
160
456
|
organizationId: client.auth.getOrgId?.() ?? authConfig?.getOrgId?.() ?? null
|
|
161
457
|
};
|
|
162
458
|
return getAuthContext();
|
|
163
459
|
}
|
|
460
|
+
/** Default retry predicate: retry on network failures and 5xx, never on Abort or 4xx. */
|
|
461
|
+
function defaultShouldRetry(error) {
|
|
462
|
+
if (isAbortError(error)) return false;
|
|
463
|
+
if (isArcApiError(error)) return error.status >= 500 && error.status < 600;
|
|
464
|
+
return error instanceof Error;
|
|
465
|
+
}
|
|
466
|
+
/** Compute the delay in ms before the Nth retry. */
|
|
467
|
+
function computeBackoff(retry, attempt) {
|
|
468
|
+
const strategy = retry.backoff ?? "exponential";
|
|
469
|
+
if (typeof strategy === "function") return Math.max(0, strategy(attempt));
|
|
470
|
+
if (strategy === "linear") return 300 * (attempt + 1);
|
|
471
|
+
return Math.min(300 * Math.pow(2, attempt), 1e4);
|
|
472
|
+
}
|
|
164
473
|
async function executeRequest(config, method, endpoint, options = {}) {
|
|
165
|
-
const
|
|
474
|
+
const totalAttempts = Math.max(1, config.retry?.attempts ?? 1);
|
|
475
|
+
const shouldRetry = (() => {
|
|
476
|
+
const r = config.retry?.retryOn;
|
|
477
|
+
if (typeof r === "function") return r;
|
|
478
|
+
if (Array.isArray(r)) return (e) => isArcApiError(e) && r.includes(e.status);
|
|
479
|
+
return defaultShouldRetry;
|
|
480
|
+
})();
|
|
481
|
+
let lastError;
|
|
482
|
+
for (let attempt = 0; attempt < totalAttempts; attempt++) try {
|
|
483
|
+
return await executeAttempt(config, method, endpoint, options, attempt);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
lastError = error;
|
|
486
|
+
if (attempt === totalAttempts - 1 || !shouldRetry(error)) throw error;
|
|
487
|
+
const delay = computeBackoff(config.retry ?? {}, attempt);
|
|
488
|
+
if (delay > 0) await sleepAbortable(delay, options.signal);
|
|
489
|
+
}
|
|
490
|
+
throw lastError;
|
|
491
|
+
}
|
|
492
|
+
/** Sleep that resolves early if the signal aborts. */
|
|
493
|
+
function sleepAbortable(ms, signal) {
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
if (signal?.aborted) {
|
|
496
|
+
reject(Object.assign(/* @__PURE__ */ new Error("Aborted"), { name: "AbortError" }));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const timer = setTimeout(() => {
|
|
500
|
+
signal?.removeEventListener("abort", onAbort);
|
|
501
|
+
resolve();
|
|
502
|
+
}, ms);
|
|
503
|
+
const onAbort = () => {
|
|
504
|
+
clearTimeout(timer);
|
|
505
|
+
reject(Object.assign(/* @__PURE__ */ new Error("Aborted"), { name: "AbortError" }));
|
|
506
|
+
};
|
|
507
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/** A single fetch attempt — used by executeRequest's retry loop. */
|
|
511
|
+
async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
512
|
+
const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey, elevated } = options;
|
|
513
|
+
const startTime = Date.now();
|
|
166
514
|
try {
|
|
167
515
|
let headers = {
|
|
168
516
|
...organizationId ? { "x-organization-id": organizationId } : {},
|
|
@@ -175,19 +523,34 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
175
523
|
} else headers["Authorization"] = `Bearer ${token}`;
|
|
176
524
|
if (config.apiVersion) headers["Accept-Version"] = config.apiVersion;
|
|
177
525
|
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
|
526
|
+
if (elevated ?? config.elevated ?? false) headers["x-arc-scope"] = "platform";
|
|
178
527
|
if (body !== void 0 && body !== null && !(body instanceof FormData)) headers["Content-Type"] = "application/json";
|
|
179
528
|
if (headerOptions) headers = {
|
|
180
529
|
...headers,
|
|
181
530
|
...headerOptions
|
|
182
531
|
};
|
|
183
532
|
const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
|
|
533
|
+
let serializedBody = void 0;
|
|
534
|
+
if (body !== void 0 && body !== null) serializedBody = body instanceof FormData ? body : JSON.stringify(body);
|
|
535
|
+
if (config.beforeRequest) {
|
|
536
|
+
const ctx = await config.beforeRequest({
|
|
537
|
+
method,
|
|
538
|
+
endpoint,
|
|
539
|
+
headers,
|
|
540
|
+
body: serializedBody,
|
|
541
|
+
signal,
|
|
542
|
+
attempt
|
|
543
|
+
});
|
|
544
|
+
headers = ctx.headers;
|
|
545
|
+
serializedBody = ctx.body;
|
|
546
|
+
}
|
|
184
547
|
const fetchOptions = {
|
|
185
548
|
method,
|
|
186
549
|
headers,
|
|
187
550
|
credentials,
|
|
188
551
|
...signal ? { signal } : {}
|
|
189
552
|
};
|
|
190
|
-
if (
|
|
553
|
+
if (serializedBody !== void 0) fetchOptions.body = serializedBody;
|
|
191
554
|
if (cache) fetchOptions.cache = cache;
|
|
192
555
|
if (revalidate !== void 0) fetchOptions.next = {
|
|
193
556
|
...fetchOptions.next,
|
|
@@ -203,7 +566,8 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
203
566
|
let errorMessage = response.statusText;
|
|
204
567
|
try {
|
|
205
568
|
json = await response.clone().json();
|
|
206
|
-
|
|
569
|
+
const j = json;
|
|
570
|
+
errorMessage = typeof j?.error === "string" && j.error || typeof j?.message === "string" && j.message || response.statusText;
|
|
207
571
|
} catch {
|
|
208
572
|
try {
|
|
209
573
|
const text = await response.text();
|
|
@@ -251,6 +615,14 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
251
615
|
throw new Error(`Failed to parse response body from ${method} ${endpoint}: blob error: ${blobError instanceof Error ? blobError.message : String(blobError)}`);
|
|
252
616
|
}
|
|
253
617
|
}
|
|
618
|
+
if (config.afterResponse) data = (await config.afterResponse({
|
|
619
|
+
method,
|
|
620
|
+
endpoint,
|
|
621
|
+
status: response.status,
|
|
622
|
+
body: data,
|
|
623
|
+
durationMs: Date.now() - startTime,
|
|
624
|
+
response
|
|
625
|
+
})).body;
|
|
254
626
|
return data;
|
|
255
627
|
} catch (error) {
|
|
256
628
|
if (error instanceof Error) throw error;
|
|
@@ -262,8 +634,8 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
262
634
|
* Handles JSON, binary (PDF, images), CSV, and text responses.
|
|
263
635
|
*
|
|
264
636
|
* @example
|
|
265
|
-
* const
|
|
266
|
-
* const response = await handleApiRequest<
|
|
637
|
+
* const user = await handleApiRequest<User>('GET', '/users/me');
|
|
638
|
+
* const response = await handleApiRequest<PaginatedResult<Product>>('GET', '/products?page=1');
|
|
267
639
|
*/
|
|
268
640
|
async function handleApiRequest(method, endpoint, options = {}) {
|
|
269
641
|
if (!clientConfig) throw new Error("arc-next: Client not configured. Call configureClient({ baseUrl }) before making API requests.");
|
|
@@ -308,10 +680,18 @@ function createQueryString(params = {}) {
|
|
|
308
680
|
if (value.length > 1) searchParams.append(`${key}[in]`, value.join(","));
|
|
309
681
|
else if (value.length === 1) searchParams.append(key, String(value[0]));
|
|
310
682
|
} else if (value === null) searchParams.append(key, "null");
|
|
683
|
+
else if (typeof value === "object") for (const [op, opValue] of Object.entries(value)) {
|
|
684
|
+
if (opValue === void 0 || opValue === "") continue;
|
|
685
|
+
const bracketKey = `${key}[${op}]`;
|
|
686
|
+
if (Array.isArray(opValue)) {
|
|
687
|
+
if (opValue.length > 0) searchParams.append(bracketKey, opValue.join(","));
|
|
688
|
+
} else if (opValue === null) searchParams.append(bracketKey, "null");
|
|
689
|
+
else searchParams.append(bracketKey, String(opValue));
|
|
690
|
+
}
|
|
311
691
|
else searchParams.append(key, String(value));
|
|
312
692
|
});
|
|
313
693
|
return searchParams.toString();
|
|
314
694
|
}
|
|
315
695
|
|
|
316
696
|
//#endregion
|
|
317
|
-
export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isArcApiError, isAutoIdempotency };
|
|
697
|
+
export { ArcApiError, KNOWN_ARC_ERROR_CODES, _resetAuthWarnings, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };
|