@classytic/arc-next 0.4.0 → 0.5.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 -616
- package/dist/api.d.ts +64 -127
- package/dist/api.js +55 -121
- package/dist/cache.d.ts +108 -0
- package/dist/cache.js +197 -0
- package/dist/client.d.ts +340 -8
- package/dist/client.js +427 -22
- package/dist/hooks.d.ts +96 -20
- package/dist/hooks.js +247 -115
- package/dist/mutation.d.ts +18 -4
- package/dist/mutation.js +20 -4
- package/dist/prefetch.d.ts +37 -3
- package/dist/prefetch.js +56 -17
- package/dist/presets/bulk.d.ts +42 -0
- package/dist/presets/bulk.js +50 -0
- package/dist/presets/search.d.ts +55 -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 +33 -0
- package/dist/presets/soft-delete.js +45 -0
- package/dist/presets/tree.d.ts +31 -0
- package/dist/presets/tree.js +47 -0
- package/dist/query.d.ts +86 -64
- package/dist/query.js +69 -151
- package/dist/sse.d.ts +116 -30
- package/dist/sse.js +179 -102
- 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 +37 -1
package/dist/client.js
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
//#region src/client.ts
|
|
2
2
|
/**
|
|
3
|
+
* Top-level error codes arc's `errorHandlerPlugin` emits on the response root
|
|
4
|
+
* (`json.code`). Mirrors `statusCodeToCode()` + the special `VALIDATION_ERROR`
|
|
5
|
+
* / `DUPLICATE_KEY` / `INTERNAL_ERROR` branches.
|
|
6
|
+
*
|
|
7
|
+
* Open via intersection with `(string & {})` so unknown codes from custom
|
|
8
|
+
* `errorMappers` / `errorMap` still satisfy the type.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Top-level HTTP-status error codes arc's `errorHandlerPlugin` emits on
|
|
12
|
+
* `json.code`. Exported as a `const` array first so callers get both
|
|
13
|
+
* compile-time exhaustiveness AND runtime iteration (e.g. building a
|
|
14
|
+
* client-side i18n lookup, or whitelisting which codes get retried).
|
|
15
|
+
*/
|
|
16
|
+
const KNOWN_TOP_LEVEL_CODES = [
|
|
17
|
+
"BAD_REQUEST",
|
|
18
|
+
"UNAUTHORIZED",
|
|
19
|
+
"FORBIDDEN",
|
|
20
|
+
"NOT_FOUND",
|
|
21
|
+
"METHOD_NOT_ALLOWED",
|
|
22
|
+
"CONFLICT",
|
|
23
|
+
"UNPROCESSABLE_ENTITY",
|
|
24
|
+
"RATE_LIMITED",
|
|
25
|
+
"INTERNAL_ERROR",
|
|
26
|
+
"BAD_GATEWAY",
|
|
27
|
+
"SERVICE_UNAVAILABLE",
|
|
28
|
+
"GATEWAY_TIMEOUT",
|
|
29
|
+
"VALIDATION_ERROR",
|
|
30
|
+
"DUPLICATE_KEY",
|
|
31
|
+
"DOMAIN_ERROR"
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* Nested business-logic codes arc's mixins / preset routes / org guards emit
|
|
35
|
+
* on `json.details.code`. These are distinct from the HTTP-status code above —
|
|
36
|
+
* `error.code` (top-level) vs `error.detailsCode` (nested). When a route
|
|
37
|
+
* returns 403 with `details.code: 'ORG_CONTEXT_REQUIRED'`, hosts read
|
|
38
|
+
* `detailsCode` to disambiguate "missing org context" from "permission denied".
|
|
39
|
+
*/
|
|
40
|
+
const KNOWN_DETAILS_CODES = [
|
|
41
|
+
"ORG_CONTEXT_REQUIRED",
|
|
42
|
+
"ORG_ROLE_REQUIRED",
|
|
43
|
+
"OWNERSHIP_DENIED",
|
|
44
|
+
"MIXED_UPDATE_SHAPE",
|
|
45
|
+
"ALL_FIELDS_STRIPPED",
|
|
46
|
+
"BEFORE_RESTORE_HOOK_ERROR"
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
3
49
|
* Rich API error with status code, response payload, and request metadata.
|
|
4
50
|
* Extends Error so existing `catch (e) { if (e instanceof Error) }` still works.
|
|
5
51
|
*
|
|
@@ -28,9 +74,63 @@ var ArcApiError = class extends Error {
|
|
|
28
74
|
this.endpoint = options.endpoint;
|
|
29
75
|
this.method = options.method;
|
|
30
76
|
}
|
|
31
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Top-level error code from arc's response envelope (`json.code`).
|
|
79
|
+
*
|
|
80
|
+
* One of {@link ArcTopLevelErrorCode} — covers HTTP-status codes (`FORBIDDEN`,
|
|
81
|
+
* `CONFLICT`...) plus the dedicated `VALIDATION_ERROR` / `DUPLICATE_KEY`
|
|
82
|
+
* branches arc emits. Returns `null` if the response carries no envelope.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* if (error.code === 'DUPLICATE_KEY') showRetryAsAdmin();
|
|
86
|
+
*/
|
|
87
|
+
get code() {
|
|
88
|
+
const j = this.json;
|
|
89
|
+
return j && typeof j.code === "string" ? j.code : null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Nested business-logic code from arc's `json.details.code` slot.
|
|
93
|
+
*
|
|
94
|
+
* Arc's preset mixins (bulk, softDelete) and org guards emit specific reason
|
|
95
|
+
* codes here — distinct from the HTTP-status `code` above. The most common
|
|
96
|
+
* is `ORG_CONTEXT_REQUIRED` (bulk operations + orgGuard reject when the
|
|
97
|
+
* caller's `request.scope.organizationId` is missing).
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* if (error.detailsCode === 'ORG_CONTEXT_REQUIRED') {
|
|
101
|
+
* alert('Configure auth before bulk operations: configureAuth({ getOrgId })');
|
|
102
|
+
* }
|
|
103
|
+
*/
|
|
104
|
+
get detailsCode() {
|
|
105
|
+
const c = this.json?.details?.code;
|
|
106
|
+
return typeof c === "string" ? c : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extract field-level validation errors as `{ field: message }` map.
|
|
110
|
+
*
|
|
111
|
+
* Reads three response shapes used by Arc:
|
|
112
|
+
* 1. `{ errors: { email: 'invalid' } }` — record form (legacy / app-level handlers).
|
|
113
|
+
* 2. `{ details: { errors: [{ field, message }] } }` — Fastify AJV + Arc errorHandler emit this.
|
|
114
|
+
* 3. `{ details: { errors: [{ instancePath, message, params }] } }` — raw AJV passthrough.
|
|
115
|
+
*
|
|
116
|
+
* Returns null when none of the shapes match.
|
|
117
|
+
*/
|
|
32
118
|
get fieldErrors() {
|
|
33
|
-
|
|
119
|
+
const j = this.json;
|
|
120
|
+
if (!j) return null;
|
|
121
|
+
if (j.errors && !Array.isArray(j.errors) && typeof j.errors === "object") return j.errors;
|
|
122
|
+
const errorList = Array.isArray(j.errors) ? j.errors : Array.isArray(j.details?.errors) ? j.details.errors : null;
|
|
123
|
+
if (!errorList) return null;
|
|
124
|
+
const map = {};
|
|
125
|
+
for (const item of errorList) {
|
|
126
|
+
if (!item || typeof item !== "object") continue;
|
|
127
|
+
const e = item;
|
|
128
|
+
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 || "";
|
|
129
|
+
if (!key) continue;
|
|
130
|
+
const msg = typeof e.message === "string" ? e.message : "Invalid value";
|
|
131
|
+
if (!(key in map)) map[key] = msg;
|
|
132
|
+
}
|
|
133
|
+
return Object.keys(map).length > 0 ? map : null;
|
|
34
134
|
}
|
|
35
135
|
};
|
|
36
136
|
/**
|
|
@@ -39,6 +139,84 @@ var ArcApiError = class extends Error {
|
|
|
39
139
|
function isArcApiError(error) {
|
|
40
140
|
return error instanceof ArcApiError;
|
|
41
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Detects request cancellation (AbortSignal triggered) regardless of runtime.
|
|
144
|
+
*
|
|
145
|
+
* Filtering out abort errors from real failures is a common need — without it,
|
|
146
|
+
* unmounting a React component mid-fetch produces noisy logs and bogus error
|
|
147
|
+
* toasts that look like API failures. Use this predicate to skip the catch
|
|
148
|
+
* branch when the request was deliberately cancelled.
|
|
149
|
+
*
|
|
150
|
+
* Handles the three AbortError shapes you'll see in the wild:
|
|
151
|
+
* 1. Browser `DOMException { name: 'AbortError' }`
|
|
152
|
+
* 2. Node 18+ / undici `Error { name: 'AbortError' }` (no DOMException)
|
|
153
|
+
* 3. Some polyfills / older runtimes where the error has `code: 'ERR_ABORTED'`
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* try {
|
|
157
|
+
* await api.getAll({ options: { signal } });
|
|
158
|
+
* } catch (err) {
|
|
159
|
+
* if (isAbortError(err)) return; // user navigated away — silence
|
|
160
|
+
* showToast('Failed to load: ' + err.message);
|
|
161
|
+
* }
|
|
162
|
+
*/
|
|
163
|
+
function isAbortError(error) {
|
|
164
|
+
if (!error) return false;
|
|
165
|
+
if (typeof error !== "object") return false;
|
|
166
|
+
const e = error;
|
|
167
|
+
if (e.name === "AbortError") return true;
|
|
168
|
+
if (e.code === "ERR_ABORTED") return true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Generic check: is this an `ArcApiError` carrying a specific top-level OR
|
|
173
|
+
* nested `details.code`? Matches whichever slot the code lives in — saves
|
|
174
|
+
* call sites from having to know whether arc emitted it at the root or under
|
|
175
|
+
* `details`.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* if (isArcErrorCode(error, 'DUPLICATE_KEY')) showRetryUI();
|
|
179
|
+
* if (isArcErrorCode(error, 'ORG_CONTEXT_REQUIRED')) promptOrgSelector();
|
|
180
|
+
*/
|
|
181
|
+
function isArcErrorCode(error, code) {
|
|
182
|
+
if (!isArcApiError(error)) return false;
|
|
183
|
+
return error.code === code || error.detailsCode === code;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Specific predicate for arc's bulk-preset + orgGuard safety code.
|
|
187
|
+
*
|
|
188
|
+
* Arc's bulk endpoints (`POST/PATCH/DELETE /:resource/bulk`) reject any call
|
|
189
|
+
* where `request.scope.organizationId` is missing — the wire signal is
|
|
190
|
+
* `403 { details: { code: 'ORG_CONTEXT_REQUIRED' } }`. Hosts hitting this
|
|
191
|
+
* need to call `configureAuth({ getOrgId })` before retrying.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* try { await api.bulkCreate({ data: [...] }); }
|
|
195
|
+
* catch (e) {
|
|
196
|
+
* if (isOrgContextRequiredError(e)) {
|
|
197
|
+
* console.warn('Bulk requires org context. Configure: configureAuth({ getOrgId })');
|
|
198
|
+
* } else throw e;
|
|
199
|
+
* }
|
|
200
|
+
*/
|
|
201
|
+
function isOrgContextRequiredError(error) {
|
|
202
|
+
return isArcErrorCode(error, "ORG_CONTEXT_REQUIRED");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Specific predicate for arc's `VALIDATION_ERROR` (Fastify AJV + Mongoose
|
|
206
|
+
* ValidationError). When true, `error.fieldErrors` is populated with the
|
|
207
|
+
* `{ field: message }` map.
|
|
208
|
+
*/
|
|
209
|
+
function isValidationError(error) {
|
|
210
|
+
return isArcErrorCode(error, "VALIDATION_ERROR");
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Specific predicate for arc's `DUPLICATE_KEY` (unique-constraint violation).
|
|
214
|
+
* Arc's errorHandler classifies these uniformly across MongoDB E11000,
|
|
215
|
+
* Postgres 23505, and Prisma P2002.
|
|
216
|
+
*/
|
|
217
|
+
function isDuplicateKeyError(error) {
|
|
218
|
+
return isArcErrorCode(error, "DUPLICATE_KEY");
|
|
219
|
+
}
|
|
42
220
|
let clientConfig = null;
|
|
43
221
|
/**
|
|
44
222
|
* Configure the API client. Call once at app init before any API requests.
|
|
@@ -64,22 +242,31 @@ function configureClient(config) {
|
|
|
64
242
|
function getAuthMode() {
|
|
65
243
|
return clientConfig?.authMode ?? "bearer";
|
|
66
244
|
}
|
|
245
|
+
/** Get the configured base URL. Returns empty string if not configured. */
|
|
246
|
+
function getBaseUrl() {
|
|
247
|
+
return clientConfig?.baseUrl ?? "";
|
|
248
|
+
}
|
|
67
249
|
/** Whether auto-idempotency is enabled on the global client. */
|
|
68
250
|
function isAutoIdempotency() {
|
|
69
251
|
return clientConfig?.autoIdempotency ?? false;
|
|
70
252
|
}
|
|
71
253
|
let authConfig = null;
|
|
254
|
+
let hasWarnedAsyncToken = false;
|
|
72
255
|
/**
|
|
73
256
|
* Configure auth context for automatic token/orgId injection.
|
|
74
257
|
* When configured, hooks auto-inject these values so you don't need to pass them manually.
|
|
75
258
|
*
|
|
76
259
|
* **SSR safety:** This sets module-level state. Call only in client-side code.
|
|
77
260
|
*
|
|
261
|
+
* **Token resolution is synchronous.** `getToken` must return `string | null`
|
|
262
|
+
* synchronously — never a Promise. See {@link AuthConfig.getToken} for guidance
|
|
263
|
+
* on bridging async auth libraries via cached values.
|
|
264
|
+
*
|
|
78
265
|
* @example
|
|
79
266
|
* // Cookie auth (no token needed)
|
|
80
267
|
* configureAuth({ getOrgId: () => currentOrg.id });
|
|
81
268
|
*
|
|
82
|
-
* // Bearer auth
|
|
269
|
+
* // Bearer auth — token is cached synchronously by the auth library
|
|
83
270
|
* configureAuth({
|
|
84
271
|
* getToken: () => session?.accessToken ?? null,
|
|
85
272
|
* getOrgId: () => currentOrg?.id ?? null,
|
|
@@ -88,40 +275,217 @@ let authConfig = null;
|
|
|
88
275
|
function configureAuth(config) {
|
|
89
276
|
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).");
|
|
90
277
|
authConfig = config;
|
|
278
|
+
hasWarnedAsyncToken = false;
|
|
279
|
+
}
|
|
280
|
+
function readToken(getToken) {
|
|
281
|
+
if (!getToken) return null;
|
|
282
|
+
const result = getToken();
|
|
283
|
+
if (result && typeof result.then === "function") {
|
|
284
|
+
if (!hasWarnedAsyncToken) {
|
|
285
|
+
hasWarnedAsyncToken = true;
|
|
286
|
+
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.");
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return result ?? null;
|
|
91
291
|
}
|
|
92
292
|
/**
|
|
93
293
|
* Get the current auth context. Returns nulls when not configured.
|
|
94
294
|
*/
|
|
95
295
|
function getAuthContext() {
|
|
96
296
|
return {
|
|
97
|
-
token: authConfig?.getToken
|
|
297
|
+
token: readToken(authConfig?.getToken),
|
|
98
298
|
organizationId: authConfig?.getOrgId?.() ?? null
|
|
99
299
|
};
|
|
100
300
|
}
|
|
301
|
+
/** @internal — exposed for tests; resets the dev-warn dedup flag. */
|
|
302
|
+
function _resetAuthWarnings() {
|
|
303
|
+
hasWarnedAsyncToken = false;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Build an auth-aware URL using the global client + auth singletons.
|
|
307
|
+
*
|
|
308
|
+
* Single source of truth for {@link import('./sse.js').buildSseUrl} (HTTP) and
|
|
309
|
+
* {@link import('./ws.js').buildWsUrl} (WebSocket). Both delegate here so the
|
|
310
|
+
* auth-injection rule (org always, token only when `authMode !== 'cookie'`)
|
|
311
|
+
* stays in one place and can't drift.
|
|
312
|
+
*
|
|
313
|
+
* @param path Path appended to the base URL (leading slash recommended).
|
|
314
|
+
* @param params Caller-supplied params; merged with auth params. Caller wins on key collision.
|
|
315
|
+
* @param protocol `'http'` (default) or `'ws'` — controls the protocol rewrite.
|
|
316
|
+
*/
|
|
317
|
+
function buildStreamUrl(path, params = {}, protocol = "http") {
|
|
318
|
+
const auth = getAuthContext();
|
|
319
|
+
const qs = new URLSearchParams();
|
|
320
|
+
for (const [key, value] of Object.entries(params)) {
|
|
321
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
322
|
+
qs.set(key, String(value));
|
|
323
|
+
}
|
|
324
|
+
if (auth.organizationId && !qs.has("organizationId")) qs.set("organizationId", auth.organizationId);
|
|
325
|
+
if (auth.token && getAuthMode() !== "cookie" && !qs.has("token")) qs.set("token", auth.token);
|
|
326
|
+
const origin = protocol === "ws" ? getBaseUrl().replace(/^http(s?):\/\//, "ws$1://") : getBaseUrl();
|
|
327
|
+
const suffix = qs.toString();
|
|
328
|
+
return `${origin}${path}${suffix ? `?${suffix}` : ""}`;
|
|
329
|
+
}
|
|
101
330
|
/**
|
|
102
331
|
* Create an isolated API client for a specific backend.
|
|
103
|
-
* Use this when your app needs to talk to multiple APIs.
|
|
332
|
+
* Use this when your app needs to talk to multiple APIs with different auth.
|
|
104
333
|
*
|
|
105
334
|
* @example
|
|
335
|
+
* // Bearer auth for main API
|
|
336
|
+
* const mainClient = createClient({
|
|
337
|
+
* baseUrl: 'https://api.example.com',
|
|
338
|
+
* getToken: () => session.accessToken,
|
|
339
|
+
* });
|
|
340
|
+
*
|
|
341
|
+
* // API key auth for analytics
|
|
106
342
|
* const analyticsClient = createClient({
|
|
107
343
|
* baseUrl: 'https://analytics.example.com',
|
|
108
|
-
*
|
|
109
|
-
*
|
|
344
|
+
* authMode: 'header',
|
|
345
|
+
* getToken: () => env.ANALYTICS_KEY,
|
|
346
|
+
* headerName: 'x-api-key',
|
|
110
347
|
* });
|
|
111
|
-
*
|
|
112
|
-
* const eventsApi = createCrudApi('events', { client: analyticsClient });
|
|
113
348
|
*/
|
|
114
349
|
function createClient(config) {
|
|
115
|
-
const { toast, navigation, ...clientCfg } = config;
|
|
350
|
+
const { toast, navigation, getToken, getOrgId, headerName, ...clientCfg } = config;
|
|
351
|
+
const clientAuth = getToken || getOrgId || headerName ? {
|
|
352
|
+
getToken,
|
|
353
|
+
getOrgId,
|
|
354
|
+
headerName
|
|
355
|
+
} : void 0;
|
|
116
356
|
return {
|
|
117
|
-
request: (method, endpoint, options) =>
|
|
357
|
+
request: (method, endpoint, options) => {
|
|
358
|
+
if (clientAuth) {
|
|
359
|
+
const resolved = { ...options };
|
|
360
|
+
if (resolved.token === void 0 && clientAuth.getToken) resolved.token = readToken(clientAuth.getToken);
|
|
361
|
+
if (resolved.organizationId === void 0 && clientAuth.getOrgId) resolved.organizationId = clientAuth.getOrgId();
|
|
362
|
+
if (clientCfg.authMode === "header" && resolved.token) {
|
|
363
|
+
resolved.headerOptions = {
|
|
364
|
+
[clientAuth.headerName ?? "x-api-key"]: resolved.token,
|
|
365
|
+
...resolved.headerOptions ?? {}
|
|
366
|
+
};
|
|
367
|
+
resolved.token = void 0;
|
|
368
|
+
}
|
|
369
|
+
return executeRequest(clientCfg, method, endpoint, resolved);
|
|
370
|
+
}
|
|
371
|
+
return executeRequest(clientCfg, method, endpoint, options);
|
|
372
|
+
},
|
|
118
373
|
config: clientCfg,
|
|
119
374
|
toast,
|
|
120
|
-
navigation
|
|
375
|
+
navigation,
|
|
376
|
+
auth: clientAuth
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Create an `ArcClient` wired to the global `configureClient` + `configureAuth` setup.
|
|
381
|
+
*
|
|
382
|
+
* Removes the boilerplate every consumer SDK writes by hand (auth-injection adapter
|
|
383
|
+
* + `BaseApi` constructor with `client: { request: handleApiRequest, config: ... }`).
|
|
384
|
+
* The returned client reads `getToken` / `getOrgId` lazily on each request, so token
|
|
385
|
+
* rotation in the global auth layer takes effect immediately.
|
|
386
|
+
*
|
|
387
|
+
* Equivalent to `createClient(...)` with `getToken`/`getOrgId` pulled from
|
|
388
|
+
* `getAuthContext()`. Pass `overrides` to customize specific fields without
|
|
389
|
+
* rebuilding the whole transport.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* // App init (somewhere in a "use client" provider)
|
|
393
|
+
* configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! });
|
|
394
|
+
* configureAuth({
|
|
395
|
+
* getToken: () => session?.accessToken ?? null,
|
|
396
|
+
* getOrgId: () => currentOrg?.id ?? null,
|
|
397
|
+
* });
|
|
398
|
+
*
|
|
399
|
+
* // SDK module
|
|
400
|
+
* import { createAuthAwareClient } from '@classytic/arc-next/client';
|
|
401
|
+
* import { createCrudApi } from '@classytic/arc-next/api';
|
|
402
|
+
*
|
|
403
|
+
* const client = createAuthAwareClient();
|
|
404
|
+
* export const productsApi = createCrudApi('products', { client });
|
|
405
|
+
*
|
|
406
|
+
* // With per-call overrides
|
|
407
|
+
* const analyticsClient = createAuthAwareClient({
|
|
408
|
+
* baseUrl: 'https://analytics.example.com',
|
|
409
|
+
* authMode: 'header',
|
|
410
|
+
* headerName: 'x-api-key',
|
|
411
|
+
* });
|
|
412
|
+
*/
|
|
413
|
+
function createAuthAwareClient(overrides = {}) {
|
|
414
|
+
return createClient({
|
|
415
|
+
baseUrl: overrides.baseUrl ?? getBaseUrl(),
|
|
416
|
+
authMode: overrides.authMode ?? getAuthMode(),
|
|
417
|
+
autoIdempotency: overrides.autoIdempotency ?? isAutoIdempotency(),
|
|
418
|
+
elevated: overrides.elevated ?? clientConfig?.elevated,
|
|
419
|
+
...overrides,
|
|
420
|
+
getToken: overrides.getToken ?? (() => readToken(authConfig?.getToken)),
|
|
421
|
+
getOrgId: overrides.getOrgId ?? (() => authConfig?.getOrgId?.() ?? null),
|
|
422
|
+
headerName: overrides.headerName ?? authConfig?.headerName
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get auth context for a specific client instance, falling back to global.
|
|
427
|
+
*/
|
|
428
|
+
function getClientAuthContext(client) {
|
|
429
|
+
if (client?.auth) return {
|
|
430
|
+
token: readToken(client.auth.getToken) ?? readToken(authConfig?.getToken),
|
|
431
|
+
organizationId: client.auth.getOrgId?.() ?? authConfig?.getOrgId?.() ?? null
|
|
121
432
|
};
|
|
433
|
+
return getAuthContext();
|
|
434
|
+
}
|
|
435
|
+
/** Default retry predicate: retry on network failures and 5xx, never on Abort or 4xx. */
|
|
436
|
+
function defaultShouldRetry(error) {
|
|
437
|
+
if (isAbortError(error)) return false;
|
|
438
|
+
if (isArcApiError(error)) return error.status >= 500 && error.status < 600;
|
|
439
|
+
return error instanceof Error;
|
|
440
|
+
}
|
|
441
|
+
/** Compute the delay in ms before the Nth retry. */
|
|
442
|
+
function computeBackoff(retry, attempt) {
|
|
443
|
+
const strategy = retry.backoff ?? "exponential";
|
|
444
|
+
if (typeof strategy === "function") return Math.max(0, strategy(attempt));
|
|
445
|
+
if (strategy === "linear") return 300 * (attempt + 1);
|
|
446
|
+
return Math.min(300 * Math.pow(2, attempt), 1e4);
|
|
122
447
|
}
|
|
123
448
|
async function executeRequest(config, method, endpoint, options = {}) {
|
|
124
|
-
const
|
|
449
|
+
const totalAttempts = Math.max(1, config.retry?.attempts ?? 1);
|
|
450
|
+
const shouldRetry = (() => {
|
|
451
|
+
const r = config.retry?.retryOn;
|
|
452
|
+
if (typeof r === "function") return r;
|
|
453
|
+
if (Array.isArray(r)) return (e) => isArcApiError(e) && r.includes(e.status);
|
|
454
|
+
return defaultShouldRetry;
|
|
455
|
+
})();
|
|
456
|
+
let lastError;
|
|
457
|
+
for (let attempt = 0; attempt < totalAttempts; attempt++) try {
|
|
458
|
+
return await executeAttempt(config, method, endpoint, options, attempt);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
lastError = error;
|
|
461
|
+
if (attempt === totalAttempts - 1 || !shouldRetry(error)) throw error;
|
|
462
|
+
const delay = computeBackoff(config.retry ?? {}, attempt);
|
|
463
|
+
if (delay > 0) await sleepAbortable(delay, options.signal);
|
|
464
|
+
}
|
|
465
|
+
throw lastError;
|
|
466
|
+
}
|
|
467
|
+
/** Sleep that resolves early if the signal aborts. */
|
|
468
|
+
function sleepAbortable(ms, signal) {
|
|
469
|
+
return new Promise((resolve, reject) => {
|
|
470
|
+
if (signal?.aborted) {
|
|
471
|
+
reject(Object.assign(/* @__PURE__ */ new Error("Aborted"), { name: "AbortError" }));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const timer = setTimeout(() => {
|
|
475
|
+
signal?.removeEventListener("abort", onAbort);
|
|
476
|
+
resolve();
|
|
477
|
+
}, ms);
|
|
478
|
+
const onAbort = () => {
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
reject(Object.assign(/* @__PURE__ */ new Error("Aborted"), { name: "AbortError" }));
|
|
481
|
+
};
|
|
482
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/** A single fetch attempt — used by executeRequest's retry loop. */
|
|
486
|
+
async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
487
|
+
const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey, elevated } = options;
|
|
488
|
+
const startTime = Date.now();
|
|
125
489
|
try {
|
|
126
490
|
let headers = {
|
|
127
491
|
...organizationId ? { "x-organization-id": organizationId } : {},
|
|
@@ -134,19 +498,34 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
134
498
|
} else headers["Authorization"] = `Bearer ${token}`;
|
|
135
499
|
if (config.apiVersion) headers["Accept-Version"] = config.apiVersion;
|
|
136
500
|
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
|
501
|
+
if (elevated ?? config.elevated ?? false) headers["x-arc-scope"] = "platform";
|
|
137
502
|
if (body !== void 0 && body !== null && !(body instanceof FormData)) headers["Content-Type"] = "application/json";
|
|
138
503
|
if (headerOptions) headers = {
|
|
139
504
|
...headers,
|
|
140
505
|
...headerOptions
|
|
141
506
|
};
|
|
142
507
|
const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
|
|
508
|
+
let serializedBody = void 0;
|
|
509
|
+
if (body !== void 0 && body !== null) serializedBody = body instanceof FormData ? body : JSON.stringify(body);
|
|
510
|
+
if (config.beforeRequest) {
|
|
511
|
+
const ctx = await config.beforeRequest({
|
|
512
|
+
method,
|
|
513
|
+
endpoint,
|
|
514
|
+
headers,
|
|
515
|
+
body: serializedBody,
|
|
516
|
+
signal,
|
|
517
|
+
attempt
|
|
518
|
+
});
|
|
519
|
+
headers = ctx.headers;
|
|
520
|
+
serializedBody = ctx.body;
|
|
521
|
+
}
|
|
143
522
|
const fetchOptions = {
|
|
144
523
|
method,
|
|
145
524
|
headers,
|
|
146
525
|
credentials,
|
|
147
526
|
...signal ? { signal } : {}
|
|
148
527
|
};
|
|
149
|
-
if (
|
|
528
|
+
if (serializedBody !== void 0) fetchOptions.body = serializedBody;
|
|
150
529
|
if (cache) fetchOptions.cache = cache;
|
|
151
530
|
if (revalidate !== void 0) fetchOptions.next = {
|
|
152
531
|
...fetchOptions.next,
|
|
@@ -158,8 +537,22 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
158
537
|
};
|
|
159
538
|
const response = await fetch(`${config.baseUrl}${endpoint}`, fetchOptions);
|
|
160
539
|
if (!response.ok) {
|
|
161
|
-
|
|
162
|
-
|
|
540
|
+
let json = null;
|
|
541
|
+
let errorMessage = response.statusText;
|
|
542
|
+
try {
|
|
543
|
+
json = await response.clone().json();
|
|
544
|
+
const j = json;
|
|
545
|
+
errorMessage = typeof j?.error === "string" && j.error || typeof j?.message === "string" && j.message || response.statusText;
|
|
546
|
+
} catch {
|
|
547
|
+
try {
|
|
548
|
+
const text = await response.text();
|
|
549
|
+
if (text) {
|
|
550
|
+
json = { rawBody: text };
|
|
551
|
+
errorMessage = text.slice(0, 200) || response.statusText;
|
|
552
|
+
}
|
|
553
|
+
} catch {}
|
|
554
|
+
}
|
|
555
|
+
throw new ArcApiError(errorMessage, {
|
|
163
556
|
status: response.status,
|
|
164
557
|
statusText: response.statusText,
|
|
165
558
|
json,
|
|
@@ -187,12 +580,24 @@ async function executeRequest(config, method, endpoint, options = {}) {
|
|
|
187
580
|
data: await response.clone().blob(),
|
|
188
581
|
response
|
|
189
582
|
};
|
|
190
|
-
} catch {
|
|
191
|
-
|
|
192
|
-
data
|
|
193
|
-
|
|
194
|
-
|
|
583
|
+
} catch (blobError) {
|
|
584
|
+
try {
|
|
585
|
+
data = {
|
|
586
|
+
data: await response.text(),
|
|
587
|
+
response
|
|
588
|
+
};
|
|
589
|
+
} catch {
|
|
590
|
+
throw new Error(`Failed to parse response body from ${method} ${endpoint}: blob error: ${blobError instanceof Error ? blobError.message : String(blobError)}`);
|
|
591
|
+
}
|
|
195
592
|
}
|
|
593
|
+
if (config.afterResponse) data = (await config.afterResponse({
|
|
594
|
+
method,
|
|
595
|
+
endpoint,
|
|
596
|
+
status: response.status,
|
|
597
|
+
body: data,
|
|
598
|
+
durationMs: Date.now() - startTime,
|
|
599
|
+
response
|
|
600
|
+
})).body;
|
|
196
601
|
return data;
|
|
197
602
|
} catch (error) {
|
|
198
603
|
if (error instanceof Error) throw error;
|
|
@@ -256,4 +661,4 @@ function createQueryString(params = {}) {
|
|
|
256
661
|
}
|
|
257
662
|
|
|
258
663
|
//#endregion
|
|
259
|
-
export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
|
|
664
|
+
export { ArcApiError, KNOWN_DETAILS_CODES, KNOWN_TOP_LEVEL_CODES, _resetAuthWarnings, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };
|