@classytic/arc-next 0.4.0 → 0.4.1

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 CHANGED
@@ -440,34 +440,55 @@ Subscribe to Arc SSE events with auto-reconnect and query invalidation:
440
440
  ```ts
441
441
  import { useEventStream } from "@classytic/arc-next/sse";
442
442
 
443
- const { isConnected, lastEvent } = useEventStream({
443
+ // Global stream all events (matches Arc's /events/stream)
444
+ const { isConnected } = useEventStream({
445
+ invalidateQueries: [agentKeys.lists()],
446
+ });
447
+
448
+ // Filtered by resource — auto-generates patterns: ['agents.*']
449
+ const { lastEvent } = useEventStream({
444
450
  resource: "agents",
445
- patterns: ["agents.created", "agents.updated"],
446
451
  invalidateQueries: [agentKeys.lists()],
447
452
  });
453
+
454
+ // Custom SSE path or explicit patterns
455
+ const { isConnected } = useEventStream({
456
+ path: "/api/v2/events",
457
+ patterns: ["orders.created", "orders.updated"],
458
+ });
448
459
  ```
449
460
 
450
461
  ### Query Keys (`KEYS`)
451
462
 
452
463
  ```ts
453
- KEYS.all // ["products"]
454
- KEYS.lists() // ["products", "list"]
455
- KEYS.list(params) // ["products", "list", params]
456
- KEYS.details() // ["products", "detail"]
457
- KEYS.detail(id) // ["products", "detail", id]
458
- KEYS.custom("stats", orgId) // ["products", "stats", orgId]
459
- KEYS.scopedList("tenant", params) // ["products", "list", { _scope: "tenant", ...params }]
464
+ KEYS.all // ["products"]
465
+ KEYS.lists() // ["products", "list"]
466
+ KEYS.list(params) // ["products", "list", params]
467
+ KEYS.details() // ["products", "detail"]
468
+ KEYS.detail(id) // ["products", "detail", id]
469
+ KEYS.scopedDetail(id, orgId) // ["products", "detail", id, { _org: orgId }] (or bare when null)
470
+ KEYS.custom("stats", orgId) // ["products", "stats", orgId]
471
+ KEYS.scopedList("tenant", params) // ["products", "list", { _scope: "tenant", ...params }]
460
472
  ```
461
473
 
462
474
  ### Cache Utilities (`cache`)
463
475
 
464
476
  ```ts
465
- await cache.invalidateAll(queryClient);
466
- await cache.invalidateLists(queryClient);
467
- await cache.invalidateDetail(queryClient, id);
477
+ // Bare (single-tenant or public)
468
478
  cache.setDetail(queryClient, id, data);
469
- cache.getDetail(queryClient, id); // T | undefined
479
+ cache.getDetail(queryClient, id);
470
480
  cache.removeDetail(queryClient, id);
481
+ await cache.invalidateDetail(queryClient, id); // prefix-matches ALL scoped variants
482
+
483
+ // Tenant-scoped (multi-tenant — isolated per org)
484
+ cache.setScopedDetail(queryClient, id, orgId, data);
485
+ cache.getScopedDetail(queryClient, id, orgId);
486
+ cache.removeScopedDetail(queryClient, id, orgId);
487
+ await cache.invalidateScopedDetail(queryClient, id, orgId);
488
+
489
+ // Global
490
+ await cache.invalidateAll(queryClient);
491
+ await cache.invalidateLists(queryClient);
471
492
  ```
472
493
 
473
494
  ### `getQueryClient(overrides?)`
@@ -521,7 +542,17 @@ export default async function ProductsPage() {
521
542
  }
522
543
  ```
523
544
 
524
- **Methods:** `prefetchList(queryClient, params?, options?)`, `prefetchDetail(queryClient, id, options?)`
545
+ **Methods:** `prefetchList`, `prefetchDetail`, `prefetchBySlug`, `prefetchDeleted`, `prefetchTree`
546
+
547
+ All accept auth options for protected routes:
548
+
549
+ ```ts
550
+ await prefetcher.prefetchList(queryClient, { limit: 20 }, {
551
+ token: serverToken, // for bearer/header auth
552
+ organizationId: "org-1", // for multi-tenant
553
+ headers: { "x-api-key": apiKey }, // for custom header auth
554
+ });
555
+ ```
525
556
 
526
557
  ## Custom Mutations
527
558
 
@@ -620,16 +651,30 @@ By default, `configureClient()` sets a single global `baseUrl`. Use `createClien
620
651
 
621
652
  ### Create isolated clients
622
653
 
654
+ Each client gets its own `baseUrl`, auth, and headers — fully independent from the global config:
655
+
623
656
  ```ts
624
657
  import { createClient } from "@classytic/arc-next/client";
625
- import { toast } from "sonner";
626
- import { useRouter } from "next/navigation";
627
658
 
659
+ // Bearer auth for main API
660
+ const mainClient = createClient({
661
+ baseUrl: "https://api.example.com",
662
+ getToken: () => session.jwt,
663
+ getOrgId: () => currentOrg.id,
664
+ });
665
+
666
+ // API key auth for analytics (no token needed — hooks auto-enable)
628
667
  const analyticsClient = createClient({
629
- baseUrl: "https://analytics.example.com",
630
- internalApiKey: "analytics-key",
631
- toast: { success: toast.success, error: toast.error },
632
- navigation: useRouter,
668
+ baseUrl: "https://analytics.internal",
669
+ authMode: "header",
670
+ getToken: () => env.ANALYTICS_KEY,
671
+ headerName: "x-analytics-key",
672
+ });
673
+
674
+ // Cookie auth for auth service
675
+ const authClient = createClient({
676
+ baseUrl: "https://auth.example.com",
677
+ authMode: "cookie",
633
678
  });
634
679
  ```
635
680
 
@@ -763,19 +808,20 @@ const adminApi = createCrudApi("users", {
763
808
 
764
809
  - **CRUD Factory** — `createCrudApi` + `createCrudHooks` generates typed API clients and React Query hooks
765
810
  - **Optimistic Updates** — Create, update, delete with instant UI feedback and automatic rollback
766
- - **Multi-Tenant Scoping** — Tenant ID sent via `x-organization-id` header + scoped list query keys. Backend controls the tenant field name and access enforcement.
811
+ - **Multi-Tenant Scoping** — `scopedDetail(id, orgId)` + `scopedList` isolate cache per tenant. Scoped cache utils for reads/writes. Navigation prefill is tenant-aware.
767
812
  - **Pagination Normalization** — Handles `docs`/`data`/`items`/`results` + any custom key, offset/keyset/aggregate pagination
768
813
  - **Detail Cache Prefilling** — List results auto-populate detail query cache
769
814
  - **React 19 Transitions** — `useMutationWithTransition` wraps invalidation in `startTransition`
770
815
  - **Cookie, Bearer & Header Auth** — `authMode: 'cookie'` / `'bearer'` / `'header'` (custom header like `x-api-key`)
771
816
  - **Custom ID Fields** — `idField` on `createCrudHooks` for resources keyed by `sku`, `slug`, `code`, etc.
772
817
  - **Preset Hooks** — `useDeleted`, `useBulkActions`, `useDetailBySlug`, `useTree`, `useChildren`, `useFindBy`
773
- - **SSE Real-Time** — `useEventStream` with auto-reconnect, pattern filtering, query invalidation
818
+ - **SSE Real-Time** — `useEventStream` with auto-reconnect, auto-pattern derivation from `resource`, query invalidation. Works with zero config or custom `path`.
774
819
  - **Infinite Scroll** — `maxPages` for memory management with automatic scroll-back support
775
820
  - **Idempotency** — `autoIdempotency` generates retry-safe keys at mutation level
776
821
  - **API Versioning** — `apiVersion` sends `Accept-Version` header
777
822
  - **SSR Prefetch** — `createCrudPrefetcher` + `prefetchBySlug` / `prefetchDeleted` / `prefetchTree`
778
823
  - **SSR Safety** — warns when `configureClient`/`configureAuth` called on the server
824
+ - **Per-Client Auth** — `createClient({ getToken, getOrgId, headerName })` — each backend gets its own auth, queries auto-enable
779
825
  - **Multi-Client** — `createClient()` for multiple API backends side by side
780
826
  - **Pluggable Toast** — `configureToast()` — use sonner, react-hot-toast, or anything
781
827
  - **Pluggable Navigation** — `configureNavigation()` — use Next.js, React Router, or any router
package/dist/api.d.ts CHANGED
@@ -232,7 +232,7 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
232
232
  params,
233
233
  options
234
234
  }?: {
235
- token?: string;
235
+ token?: string | null;
236
236
  organizationId?: string | null;
237
237
  data?: unknown;
238
238
  params?: QueryParams;
package/dist/api.js CHANGED
@@ -96,7 +96,7 @@ var BaseApi = class {
96
96
  if (organizationId) requestOptions.organizationId = organizationId;
97
97
  return this.requestFn("GET", url, this.withHeaders(requestOptions));
98
98
  }
99
- async create({ token, organizationId = null, data, options = {} }) {
99
+ async create({ token = null, organizationId = null, data, options = {} }) {
100
100
  const requestOptions = {
101
101
  body: data,
102
102
  ...options
@@ -105,7 +105,7 @@ var BaseApi = class {
105
105
  if (organizationId) requestOptions.organizationId = organizationId;
106
106
  return this.requestFn("POST", this.baseUrl, this.withHeaders(requestOptions));
107
107
  }
108
- async update({ token, organizationId = null, id, data, options = {} }) {
108
+ async update({ token = null, organizationId = null, id, data, options = {} }) {
109
109
  if (!id) throw new Error("ID is required");
110
110
  const requestOptions = {
111
111
  body: data,
@@ -115,14 +115,14 @@ var BaseApi = class {
115
115
  if (organizationId) requestOptions.organizationId = organizationId;
116
116
  return this.requestFn("PATCH", `${this.baseUrl}/${id}`, this.withHeaders(requestOptions));
117
117
  }
118
- async delete({ token, organizationId = null, id, options = {} }) {
118
+ async delete({ token = null, organizationId = null, id, options = {} }) {
119
119
  if (!id) throw new Error("ID is required");
120
120
  const requestOptions = { ...options };
121
121
  if (token) requestOptions.token = token;
122
122
  if (organizationId) requestOptions.organizationId = organizationId;
123
123
  return this.requestFn("DELETE", `${this.baseUrl}/${id}`, this.withHeaders(requestOptions));
124
124
  }
125
- async upload({ token, organizationId = null, data, id, path, options = {} }) {
125
+ async upload({ token = null, organizationId = null, data, id, path, options = {} }) {
126
126
  const suffix = path ?? (id ? `${id}/upload` : void 0);
127
127
  const url = suffix ? `${this.baseUrl}/${suffix}` : this.baseUrl;
128
128
  const requestOptions = {
@@ -167,7 +167,7 @@ var BaseApi = class {
167
167
  if (organizationId) requestOptions.organizationId = organizationId;
168
168
  return this.requestFn("GET", `${this.baseUrl}?${queryString}`, this.withHeaders(requestOptions));
169
169
  }
170
- async request(method, endpoint, { token, organizationId = null, data, params, options = {} } = {}) {
170
+ async request(method, endpoint, { token = null, organizationId = null, data, params, options = {} } = {}) {
171
171
  let url = endpoint;
172
172
  if (params) {
173
173
  const processedParams = this.prepareParams(params);
@@ -197,14 +197,14 @@ var BaseApi = class {
197
197
  if (organizationId) requestOptions.organizationId = organizationId;
198
198
  return this.requestFn("GET", `${this.baseUrl}/deleted?${queryString}`, this.withHeaders(requestOptions));
199
199
  }
200
- async restore({ token, organizationId = null, id, options = {} }) {
200
+ async restore({ token = null, organizationId = null, id, options = {} }) {
201
201
  if (!id) throw new Error("ID is required");
202
202
  const requestOptions = { ...options };
203
203
  if (token) requestOptions.token = token;
204
204
  if (organizationId) requestOptions.organizationId = organizationId;
205
205
  return this.requestFn("POST", `${this.baseUrl}/${id}/restore`, this.withHeaders(requestOptions));
206
206
  }
207
- async bulkCreate({ token, organizationId = null, data, options = {} }) {
207
+ async bulkCreate({ token = null, organizationId = null, data, options = {} }) {
208
208
  const requestOptions = {
209
209
  body: data,
210
210
  ...options
@@ -213,7 +213,7 @@ var BaseApi = class {
213
213
  if (organizationId) requestOptions.organizationId = organizationId;
214
214
  return this.requestFn("POST", `${this.baseUrl}/bulk`, this.withHeaders(requestOptions));
215
215
  }
216
- async bulkUpdate({ token, organizationId = null, filter, data, options = {} }) {
216
+ async bulkUpdate({ token = null, organizationId = null, filter, data, options = {} }) {
217
217
  const requestOptions = {
218
218
  body: {
219
219
  filter,
@@ -225,7 +225,7 @@ var BaseApi = class {
225
225
  if (organizationId) requestOptions.organizationId = organizationId;
226
226
  return this.requestFn("PATCH", `${this.baseUrl}/bulk`, this.withHeaders(requestOptions));
227
227
  }
228
- async bulkDelete({ token, organizationId = null, filter, options = {} }) {
228
+ async bulkDelete({ token = null, organizationId = null, filter, options = {} }) {
229
229
  const requestOptions = {
230
230
  body: { filter },
231
231
  ...options
package/dist/client.d.ts CHANGED
@@ -99,6 +99,8 @@ declare function configureClient(config: ClientConfig): void;
99
99
  * Get the configured auth mode. Returns 'bearer' if not configured.
100
100
  */
101
101
  declare function getAuthMode(): 'bearer' | 'cookie' | 'header';
102
+ /** Get the configured base URL. Returns empty string if not configured. */
103
+ declare function getBaseUrl(): string;
102
104
  /** Whether auto-idempotency is enabled on the global client. */
103
105
  declare function isAutoIdempotency(): boolean;
104
106
  interface AuthConfig {
@@ -132,6 +134,16 @@ declare function getAuthContext(): {
132
134
  organizationId: string | null;
133
135
  };
134
136
  type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
137
+ /** Returned by `handleApiRequest` for PDF, image, and CSV responses. */
138
+ interface BlobResponse {
139
+ data: Blob;
140
+ response: Response;
141
+ }
142
+ /** Returned by `handleApiRequest` for text/plain and text/html responses. */
143
+ interface TextResponse {
144
+ data: string;
145
+ response: Response;
146
+ }
135
147
  interface ApiRequestOptions {
136
148
  body?: unknown;
137
149
  token?: string | null;
@@ -147,27 +159,52 @@ interface ApiRequestOptions {
147
159
  interface ArcClientConfig extends ClientConfig {
148
160
  toast?: ToastHandler;
149
161
  navigation?: UseRouterHook;
162
+ /** Per-client token provider. Overrides global configureAuth().getToken. */
163
+ getToken?: () => string | null;
164
+ /** Per-client org ID provider. Overrides global configureAuth().getOrgId. */
165
+ getOrgId?: () => string | null;
166
+ /** Per-client custom auth header name. Used when authMode is 'header'. */
167
+ headerName?: string;
150
168
  }
151
169
  interface ArcClient {
152
170
  request: <T = unknown>(method: HttpMethod, endpoint: string, options?: ApiRequestOptions) => Promise<T>;
153
171
  config: ClientConfig;
154
172
  toast?: ToastHandler;
155
173
  navigation?: UseRouterHook;
174
+ /** Per-client auth context. Falls back to global configureAuth() when not set. */
175
+ auth?: {
176
+ getToken?: () => string | null;
177
+ getOrgId?: () => string | null;
178
+ headerName?: string;
179
+ };
156
180
  }
157
181
  /**
158
182
  * Create an isolated API client for a specific backend.
159
- * Use this when your app needs to talk to multiple APIs.
183
+ * Use this when your app needs to talk to multiple APIs with different auth.
160
184
  *
161
185
  * @example
186
+ * // Bearer auth for main API
187
+ * const mainClient = createClient({
188
+ * baseUrl: 'https://api.example.com',
189
+ * getToken: () => session.accessToken,
190
+ * });
191
+ *
192
+ * // API key auth for analytics
162
193
  * const analyticsClient = createClient({
163
194
  * baseUrl: 'https://analytics.example.com',
164
- * toast: { success: toast.success, error: toast.error },
165
- * navigation: useRouter,
195
+ * authMode: 'header',
196
+ * getToken: () => env.ANALYTICS_KEY,
197
+ * headerName: 'x-api-key',
166
198
  * });
167
- *
168
- * const eventsApi = createCrudApi('events', { client: analyticsClient });
169
199
  */
170
200
  declare function createClient(config: ArcClientConfig): ArcClient;
201
+ /**
202
+ * Get auth context for a specific client instance, falling back to global.
203
+ */
204
+ declare function getClientAuthContext(client?: ArcClient): {
205
+ token: string | null;
206
+ organizationId: string | null;
207
+ };
171
208
  /**
172
209
  * Universal API request handler.
173
210
  * Handles JSON, binary (PDF, images), CSV, and text responses.
@@ -195,4 +232,4 @@ declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: str
195
232
  */
196
233
  declare function createQueryString<T extends Record<string, unknown>>(params?: T): string;
197
234
  //#endregion
198
- export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, ClientConfig, HttpMethod, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
235
+ export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, BlobResponse, ClientConfig, HttpMethod, TextResponse, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isArcApiError, isAutoIdempotency };
package/dist/client.js CHANGED
@@ -64,6 +64,10 @@ function configureClient(config) {
64
64
  function getAuthMode() {
65
65
  return clientConfig?.authMode ?? "bearer";
66
66
  }
67
+ /** Get the configured base URL. Returns empty string if not configured. */
68
+ function getBaseUrl() {
69
+ return clientConfig?.baseUrl ?? "";
70
+ }
67
71
  /** Whether auto-idempotency is enabled on the global client. */
68
72
  function isAutoIdempotency() {
69
73
  return clientConfig?.autoIdempotency ?? false;
@@ -100,25 +104,62 @@ function getAuthContext() {
100
104
  }
101
105
  /**
102
106
  * Create an isolated API client for a specific backend.
103
- * Use this when your app needs to talk to multiple APIs.
107
+ * Use this when your app needs to talk to multiple APIs with different auth.
104
108
  *
105
109
  * @example
110
+ * // Bearer auth for main API
111
+ * const mainClient = createClient({
112
+ * baseUrl: 'https://api.example.com',
113
+ * getToken: () => session.accessToken,
114
+ * });
115
+ *
116
+ * // API key auth for analytics
106
117
  * const analyticsClient = createClient({
107
118
  * baseUrl: 'https://analytics.example.com',
108
- * toast: { success: toast.success, error: toast.error },
109
- * navigation: useRouter,
119
+ * authMode: 'header',
120
+ * getToken: () => env.ANALYTICS_KEY,
121
+ * headerName: 'x-api-key',
110
122
  * });
111
- *
112
- * const eventsApi = createCrudApi('events', { client: analyticsClient });
113
123
  */
114
124
  function createClient(config) {
115
- const { toast, navigation, ...clientCfg } = config;
125
+ const { toast, navigation, getToken, getOrgId, headerName, ...clientCfg } = config;
126
+ const clientAuth = getToken || getOrgId || headerName ? {
127
+ getToken,
128
+ getOrgId,
129
+ headerName
130
+ } : void 0;
116
131
  return {
117
- request: (method, endpoint, options) => executeRequest(clientCfg, method, endpoint, options),
132
+ request: (method, endpoint, options) => {
133
+ if (clientAuth) {
134
+ const resolved = { ...options };
135
+ if (resolved.token === void 0 && clientAuth.getToken) resolved.token = clientAuth.getToken();
136
+ if (resolved.organizationId === void 0 && clientAuth.getOrgId) resolved.organizationId = clientAuth.getOrgId();
137
+ if (clientCfg.authMode === "header" && resolved.token) {
138
+ resolved.headerOptions = {
139
+ [clientAuth.headerName ?? "x-api-key"]: resolved.token,
140
+ ...resolved.headerOptions ?? {}
141
+ };
142
+ resolved.token = void 0;
143
+ }
144
+ return executeRequest(clientCfg, method, endpoint, resolved);
145
+ }
146
+ return executeRequest(clientCfg, method, endpoint, options);
147
+ },
118
148
  config: clientCfg,
119
149
  toast,
120
- navigation
150
+ navigation,
151
+ auth: clientAuth
152
+ };
153
+ }
154
+ /**
155
+ * Get auth context for a specific client instance, falling back to global.
156
+ */
157
+ function getClientAuthContext(client) {
158
+ if (client?.auth) return {
159
+ token: client.auth.getToken?.() ?? authConfig?.getToken?.() ?? null,
160
+ organizationId: client.auth.getOrgId?.() ?? authConfig?.getOrgId?.() ?? null
121
161
  };
162
+ return getAuthContext();
122
163
  }
123
164
  async function executeRequest(config, method, endpoint, options = {}) {
124
165
  const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey } = options;
@@ -158,8 +199,21 @@ async function executeRequest(config, method, endpoint, options = {}) {
158
199
  };
159
200
  const response = await fetch(`${config.baseUrl}${endpoint}`, fetchOptions);
160
201
  if (!response.ok) {
161
- const json = await response.json().catch(() => null);
162
- throw new ArcApiError(json?.message || response.statusText, {
202
+ let json = null;
203
+ let errorMessage = response.statusText;
204
+ try {
205
+ json = await response.clone().json();
206
+ errorMessage = json?.message || response.statusText;
207
+ } catch {
208
+ try {
209
+ const text = await response.text();
210
+ if (text) {
211
+ json = { rawBody: text };
212
+ errorMessage = text.slice(0, 200) || response.statusText;
213
+ }
214
+ } catch {}
215
+ }
216
+ throw new ArcApiError(errorMessage, {
163
217
  status: response.status,
164
218
  statusText: response.statusText,
165
219
  json,
@@ -187,11 +241,15 @@ async function executeRequest(config, method, endpoint, options = {}) {
187
241
  data: await response.clone().blob(),
188
242
  response
189
243
  };
190
- } catch {
191
- data = {
192
- data: await response.text(),
193
- response
194
- };
244
+ } catch (blobError) {
245
+ try {
246
+ data = {
247
+ data: await response.text(),
248
+ response
249
+ };
250
+ } catch {
251
+ throw new Error(`Failed to parse response body from ${method} ${endpoint}: blob error: ${blobError instanceof Error ? blobError.message : String(blobError)}`);
252
+ }
195
253
  }
196
254
  return data;
197
255
  } catch (error) {
@@ -256,4 +314,4 @@ function createQueryString(params = {}) {
256
314
  }
257
315
 
258
316
  //#endregion
259
- export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
317
+ export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isArcApiError, isAutoIdempotency };
package/dist/hooks.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ArcClient, UseRouterHook } from "./client.js";
2
2
  import { BaseApi, FilterOperator } from "./api.js";
3
- import { CacheUtils, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, QueryKeys } from "./query.js";
4
3
  import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
4
+ import { CacheUtils, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, QueryKeys } from "./query.js";
5
5
  import { QueryKey } from "@tanstack/react-query";
6
6
 
7
7
  //#region src/hooks.d.ts
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { getAuthContext, getAuthMode } from "./client.js";
3
+ import { getAuthMode, getClientAuthContext } from "./client.js";
4
4
  import { isKeysetPagination, isOffsetPagination } from "./api.js";
5
5
  import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery } from "./query.js";
6
6
  import { useMutationWithTransition, useOptimisticMutation } from "./mutation.js";
@@ -21,12 +21,17 @@ let useRouterHook = null;
21
21
  function configureNavigation(hook) {
22
22
  useRouterHook = hook;
23
23
  }
24
- function createEnabledRule(token, options, authMode = getAuthMode()) {
24
+ function createEnabledRule(token, options, authMode = getAuthMode(), hasStaticAuth = false) {
25
25
  if (authMode === "cookie" || options.public) return options.enabled ?? true;
26
+ if (hasStaticAuth) return options.enabled ?? true;
26
27
  return options.enabled !== void 0 ? options.enabled && !!token : !!token;
27
28
  }
28
29
  function createCrudHooks({ api, entityKey, singular, plural, idField, defaults = {}, callbacks = {}, client }) {
29
30
  const pluralName = plural ?? `${singular}s`;
31
+ /** Resolve auth context — per-client auth takes priority over global */
32
+ const resolveAuth = () => getClientAuthContext(client);
33
+ /** Whether auth is provided via static config (headers, internalApiKey, per-client auth) — no token needed for enablement */
34
+ const hasStaticAuth = !!(client?.config?.defaultHeaders || client?.config?.internalApiKey || client?.auth);
30
35
  /** Extract ID from an item using configured idField, falling back to _id → id */
31
36
  function resolveItemId(item) {
32
37
  if (!item || typeof item !== "object") return null;
@@ -60,12 +65,12 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
60
65
  let token;
61
66
  let params;
62
67
  let options;
63
- if (tokenOrParams === null || typeof tokenOrParams === "string") {
68
+ if (typeof tokenOrParams === "string" || tokenOrParams === null && maybeOptions !== void 0) {
64
69
  token = tokenOrParams;
65
70
  params = paramsOrOptions ?? {};
66
71
  options = maybeOptions ?? {};
67
72
  } else {
68
- const auth = getAuthContext();
73
+ const auth = resolveAuth();
69
74
  token = auth.token;
70
75
  params = tokenOrParams ?? {};
71
76
  options = paramsOrOptions ?? {};
@@ -91,7 +96,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
91
96
  ...requestOpts
92
97
  }
93
98
  }),
94
- enabled: createEnabledRule(token, queryOpts, resolveAuthMode()),
99
+ enabled: createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
95
100
  options: {
96
101
  staleTime: queryOpts.staleTime ?? config.staleTime,
97
102
  gcTime: queryOpts.gcTime ?? config.gcTime,
@@ -101,7 +106,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
101
106
  refetchIntervalInBackground: queryOpts.refetchIntervalInBackground
102
107
  },
103
108
  prefillDetailCache: queryOpts.prefillDetailCache ?? true,
104
- detailKeyBuilder: (id) => KEYS.detail(id),
109
+ detailKeyBuilder: (id) => KEYS.scopedDetail(id, organizationId ?? null),
105
110
  itemIdResolver: resolveItemId,
106
111
  select: queryOpts.select
107
112
  });
@@ -109,11 +114,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
109
114
  function useDetail(id, tokenOrOptions, maybeOptions) {
110
115
  let token;
111
116
  let options;
112
- if (tokenOrOptions === null || typeof tokenOrOptions === "string") {
117
+ if (typeof tokenOrOptions === "string" || tokenOrOptions === null && maybeOptions !== void 0) {
113
118
  token = tokenOrOptions;
114
119
  options = maybeOptions ?? {};
115
120
  } else {
116
- const auth = getAuthContext();
121
+ const auth = resolveAuth();
117
122
  token = auth.token;
118
123
  options = tokenOrOptions ?? {};
119
124
  if (auth.organizationId && !options.organizationId) options = {
@@ -122,8 +127,9 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
122
127
  };
123
128
  }
124
129
  const { organizationId, params: queryParams, request: requestOpts, ...restOptions } = options;
130
+ const detailKey = KEYS.scopedDetail(id || "", organizationId ?? null);
125
131
  return useDetailQuery({
126
- queryKey: queryParams ? [...KEYS.detail(id || ""), queryParams] : KEYS.detail(id || ""),
132
+ queryKey: queryParams ? [...detailKey, queryParams] : detailKey,
127
133
  queryFn: ({ signal }) => api.getById({
128
134
  id,
129
135
  token,
@@ -134,7 +140,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
134
140
  ...requestOpts
135
141
  }
136
142
  }),
137
- enabled: !!id && createEnabledRule(token, restOptions, resolveAuthMode()),
143
+ enabled: !!id && createEnabledRule(token, restOptions, resolveAuthMode(), hasStaticAuth),
138
144
  options: {
139
145
  staleTime: restOptions.staleTime ?? config.staleTime,
140
146
  gcTime: restOptions.gcTime ?? config.gcTime,
@@ -197,13 +203,16 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
197
203
  ...item,
198
204
  ...data
199
205
  } : item));
200
- queryClient.setQueryData(KEYS.detail(id), (current) => current ? {
206
+ const detailUpdater = (current) => current ? {
201
207
  ...current,
202
208
  data: {
203
209
  ...current.data || {},
204
210
  ...data
205
211
  }
206
- } : current);
212
+ } : current;
213
+ queryClient.getQueriesData({ queryKey: KEYS.detail(id) }).forEach(([qKey, qData]) => {
214
+ if (qData) queryClient.setQueryData(qKey, detailUpdater);
215
+ });
207
216
  return updated;
208
217
  },
209
218
  onSuccess: (raw, { id, data: updateData }) => {
@@ -276,8 +285,8 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
276
285
  },
277
286
  toastHandler: instanceToast
278
287
  });
279
- const resolveAuth = useCallback((params) => {
280
- const auth = getAuthContext();
288
+ const resolveActionAuth = useCallback((params) => {
289
+ const auth = resolveAuth();
281
290
  return {
282
291
  ...params,
283
292
  token: params.token ?? auth.token,
@@ -288,7 +297,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
288
297
  create: useCallback(async (params, options) => {
289
298
  silentRef.current = options?.silent ?? false;
290
299
  try {
291
- const entity = extractItem(await createMutation.mutateAsync(resolveAuth(params)));
300
+ const entity = extractItem(await createMutation.mutateAsync(resolveActionAuth(params)));
292
301
  options?.onSuccess?.(entity);
293
302
  options?.onSettled?.(entity, null);
294
303
  return entity;
@@ -299,11 +308,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
299
308
  } finally {
300
309
  silentRef.current = false;
301
310
  }
302
- }, [createMutation, resolveAuth]),
311
+ }, [createMutation, resolveActionAuth]),
303
312
  update: useCallback(async (params, options) => {
304
313
  silentRef.current = options?.silent ?? false;
305
314
  try {
306
- const entity = extractItem(await updateMutation.mutateAsync(resolveAuth(params)));
315
+ const entity = extractItem(await updateMutation.mutateAsync(resolveActionAuth(params)));
307
316
  options?.onSuccess?.(entity);
308
317
  options?.onSettled?.(entity, null);
309
318
  return entity;
@@ -314,11 +323,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
314
323
  } finally {
315
324
  silentRef.current = false;
316
325
  }
317
- }, [updateMutation, resolveAuth]),
326
+ }, [updateMutation, resolveActionAuth]),
318
327
  remove: useCallback(async (params, options) => {
319
328
  silentRef.current = options?.silent ?? false;
320
329
  try {
321
- const result = await deleteMutation.mutateAsync(resolveAuth(params));
330
+ const result = await deleteMutation.mutateAsync(resolveActionAuth(params));
322
331
  options?.onSuccess?.(result);
323
332
  options?.onSettled?.(result, null);
324
333
  return result;
@@ -329,11 +338,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
329
338
  } finally {
330
339
  silentRef.current = false;
331
340
  }
332
- }, [deleteMutation, resolveAuth]),
341
+ }, [deleteMutation, resolveActionAuth]),
333
342
  restore: useCallback(async (params, options) => {
334
343
  silentRef.current = options?.silent ?? false;
335
344
  try {
336
- const entity = extractItem(await restoreMutation.mutateAsync(resolveAuth(params)));
345
+ const entity = extractItem(await restoreMutation.mutateAsync(resolveActionAuth(params)));
337
346
  options?.onSuccess?.(entity);
338
347
  options?.onSettled?.(entity, null);
339
348
  return entity;
@@ -344,7 +353,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
344
353
  } finally {
345
354
  silentRef.current = false;
346
355
  }
347
- }, [restoreMutation, resolveAuth]),
356
+ }, [restoreMutation, resolveActionAuth]),
348
357
  isCreating: createMutation.isPending,
349
358
  isUpdating: updateMutation.isPending,
350
359
  isDeleting: deleteMutation.isPending,
@@ -356,12 +365,12 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
356
365
  let token;
357
366
  let params;
358
367
  let options;
359
- if (tokenOrParams === null || typeof tokenOrParams === "string") {
368
+ if (typeof tokenOrParams === "string" || tokenOrParams === null && maybeOptions !== void 0) {
360
369
  token = tokenOrParams;
361
370
  params = paramsOrOptions ?? {};
362
371
  options = maybeOptions ?? {};
363
372
  } else {
364
- const auth = getAuthContext();
373
+ const auth = resolveAuth();
365
374
  token = auth.token;
366
375
  params = tokenOrParams ?? {};
367
376
  options = paramsOrOptions ?? {};
@@ -393,7 +402,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
393
402
  }
394
403
  });
395
404
  },
396
- enabled: createEnabledRule(token, queryOpts, resolveAuthMode()),
405
+ enabled: createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
397
406
  initialPageParam: restParams.after ? restParams.after : 1,
398
407
  getNextPageParam: (lastPage) => {
399
408
  const page = lastPage;
@@ -423,7 +432,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
423
432
  return useMutationWithTransition({
424
433
  mutationFn: ({ data, id, path }) => {
425
434
  if (!api.upload) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define an upload method`));
426
- const auth = getAuthContext();
435
+ const auth = resolveAuth();
427
436
  return api.upload({
428
437
  token: auth.token,
429
438
  organizationId: auth.organizationId,
@@ -444,7 +453,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
444
453
  });
445
454
  }
446
455
  function useSearch(query, params, options) {
447
- const auth = getAuthContext();
456
+ const auth = resolveAuth();
448
457
  const token = params?.token ?? auth.token;
449
458
  const organizationId = params?.organizationId ?? auth.organizationId;
450
459
  const { organizationId: _, token: _t, ...restParams } = params ?? {};
@@ -475,7 +484,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
475
484
  }
476
485
  });
477
486
  },
478
- enabled: !!api.search && query.length > 0 && createEnabledRule(token, queryOpts, resolveAuthMode()),
487
+ enabled: !!api.search && query.length > 0 && createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
479
488
  options: {
480
489
  staleTime: queryOpts.staleTime ?? config.staleTime,
481
490
  gcTime: queryOpts.gcTime ?? config.gcTime
@@ -495,7 +504,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
495
504
  });
496
505
  }
497
506
  function useDeleted(params, options) {
498
- const auth = getAuthContext();
507
+ const auth = resolveAuth();
499
508
  const token = auth.token;
500
509
  const mergedParams = params ?? {};
501
510
  const organizationId = mergedParams.organizationId ?? auth.organizationId;
@@ -518,7 +527,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
518
527
  }
519
528
  });
520
529
  },
521
- enabled: !!api.getDeleted && createEnabledRule(token, queryOpts, resolveAuthMode()),
530
+ enabled: !!api.getDeleted && createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
522
531
  options: {
523
532
  staleTime: queryOpts.staleTime ?? config.staleTime,
524
533
  gcTime: queryOpts.gcTime ?? config.gcTime
@@ -527,7 +536,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
527
536
  });
528
537
  }
529
538
  function useDetailBySlug(slug, options) {
530
- const auth = getAuthContext();
539
+ const auth = resolveAuth();
531
540
  const token = auth.token;
532
541
  const resolvedOptions = options ?? {};
533
542
  const organizationId = resolvedOptions.organizationId ?? auth.organizationId;
@@ -547,7 +556,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
547
556
  }
548
557
  });
549
558
  },
550
- enabled: !!api.getBySlug && !!slug && createEnabledRule(token, restOptions, resolveAuthMode()),
559
+ enabled: !!api.getBySlug && !!slug && createEnabledRule(token, restOptions, resolveAuthMode(), hasStaticAuth),
551
560
  options: {
552
561
  staleTime: restOptions.staleTime ?? config.staleTime,
553
562
  gcTime: restOptions.gcTime ?? config.gcTime,
@@ -558,7 +567,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
558
567
  });
559
568
  }
560
569
  function useTree(params, options) {
561
- const auth = getAuthContext();
570
+ const auth = resolveAuth();
562
571
  const token = auth.token;
563
572
  const mergedParams = params ?? {};
564
573
  const organizationId = mergedParams.organizationId ?? auth.organizationId;
@@ -581,7 +590,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
581
590
  }
582
591
  });
583
592
  },
584
- enabled: !!api.getTree && createEnabledRule(token, queryOpts, resolveAuthMode()),
593
+ enabled: !!api.getTree && createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
585
594
  options: {
586
595
  staleTime: queryOpts.staleTime ?? config.staleTime,
587
596
  gcTime: queryOpts.gcTime ?? config.gcTime
@@ -590,7 +599,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
590
599
  });
591
600
  }
592
601
  function useChildren(parentId, params, options) {
593
- const auth = getAuthContext();
602
+ const auth = resolveAuth();
594
603
  const token = auth.token;
595
604
  const mergedParams = params ?? {};
596
605
  const organizationId = mergedParams.organizationId ?? auth.organizationId;
@@ -614,24 +623,29 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
614
623
  }
615
624
  });
616
625
  },
617
- enabled: !!api.getChildren && !!parentId && createEnabledRule(token, queryOpts, resolveAuthMode()),
626
+ enabled: !!api.getChildren && !!parentId && createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
618
627
  options: {
619
628
  staleTime: queryOpts.staleTime ?? config.staleTime,
620
629
  gcTime: queryOpts.gcTime ?? config.gcTime
621
630
  },
622
631
  prefillDetailCache: queryOpts.prefillDetailCache ?? true,
623
- detailKeyBuilder: (id) => KEYS.detail(id),
632
+ detailKeyBuilder: (id) => KEYS.scopedDetail(id, organizationId ?? null),
624
633
  itemIdResolver: resolveItemId,
625
634
  select: queryOpts.select
626
635
  });
627
636
  }
628
637
  function useFindBy(field, value, options) {
629
- const auth = getAuthContext();
638
+ const auth = resolveAuth();
630
639
  const token = auth.token;
631
640
  const organizationId = auth.organizationId;
632
641
  const { operator, request: requestOpts, ...queryOpts } = options ?? {};
633
642
  return useListQuery({
634
- queryKey: KEYS.custom("findBy", field, value, operator),
643
+ queryKey: KEYS.custom("findBy", {
644
+ field,
645
+ value,
646
+ operator,
647
+ organizationId
648
+ }),
635
649
  queryFn: ({ signal }) => {
636
650
  if (!api.findBy) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a findBy method`));
637
651
  return api.findBy({
@@ -646,13 +660,13 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
646
660
  }
647
661
  });
648
662
  },
649
- enabled: !!api.findBy && value !== void 0 && value !== null && createEnabledRule(token, queryOpts, resolveAuthMode()),
663
+ enabled: !!api.findBy && value !== void 0 && value !== null && createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
650
664
  options: {
651
665
  staleTime: queryOpts.staleTime ?? config.staleTime,
652
666
  gcTime: queryOpts.gcTime ?? config.gcTime
653
667
  },
654
668
  prefillDetailCache: queryOpts.prefillDetailCache ?? true,
655
- detailKeyBuilder: (id) => KEYS.detail(id),
669
+ detailKeyBuilder: (id) => KEYS.scopedDetail(id, organizationId ?? null),
656
670
  itemIdResolver: resolveItemId,
657
671
  select: queryOpts.select
658
672
  });
@@ -661,7 +675,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
661
675
  const bulkCreateMutation = useMutationWithTransition({
662
676
  mutationFn: (vars) => {
663
677
  if (!api.bulkCreate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a bulkCreate method`));
664
- const auth = getAuthContext();
678
+ const auth = resolveAuth();
665
679
  return api.bulkCreate({
666
680
  token: vars.token ?? auth.token,
667
681
  organizationId: vars.organizationId ?? auth.organizationId,
@@ -678,7 +692,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
678
692
  const bulkUpdateMutation = useMutationWithTransition({
679
693
  mutationFn: (vars) => {
680
694
  if (!api.bulkUpdate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a bulkUpdate method`));
681
- const auth = getAuthContext();
695
+ const auth = resolveAuth();
682
696
  return api.bulkUpdate({
683
697
  token: vars.token ?? auth.token,
684
698
  organizationId: vars.organizationId ?? auth.organizationId,
@@ -696,7 +710,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
696
710
  const bulkDeleteMutation = useMutationWithTransition({
697
711
  mutationFn: (vars) => {
698
712
  if (!api.bulkDelete) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a bulkDelete method`));
699
- const auth = getAuthContext();
713
+ const auth = resolveAuth();
700
714
  return api.bulkDelete({
701
715
  token: vars.token ?? auth.token,
702
716
  organizationId: vars.organizationId ?? auth.organizationId,
@@ -740,7 +754,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
740
754
  const router = resolvedRouterHook();
741
755
  return useCallback((href, item, options = {}) => {
742
756
  const id = resolveItemId(item);
743
- if (id) queryClient.setQueryData(KEYS.detail(id), { data: item });
757
+ if (id) {
758
+ const orgId = resolveAuth().organizationId;
759
+ queryClient.setQueryData(KEYS.scopedDetail(id, orgId), { data: item });
760
+ if (orgId) queryClient.setQueryData(KEYS.detail(id), { data: item });
761
+ }
744
762
  if (!router) return;
745
763
  const { scroll = true, replace = false } = options;
746
764
  if (replace) router.replace(href, { scroll });
@@ -1,5 +1,4 @@
1
1
  import { ToastHandler } from "./client.js";
2
- import { QUERY_CONFIGS } from "./query.js";
3
2
  import * as _$_tanstack_react_query0 from "@tanstack/react-query";
4
3
  import { QueryClient, QueryKey, UseMutateAsyncFunction, UseMutateFunction } from "@tanstack/react-query";
5
4
 
@@ -107,7 +106,5 @@ declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimist
107
106
  data: [readonly unknown[], unknown][];
108
107
  }[];
109
108
  }>;
110
- /** @deprecated Use `useOptimisticMutation` */
111
- declare const createOptimisticMutation: typeof useOptimisticMutation;
112
109
  //#endregion
113
- export { CreateOptimisticMutationConfig, MutationCallbacks, MutationMessages, OptimisticMutationConfig, QUERY_CONFIGS, TransitionMutationConfig, TransitionMutationReturn, configureToast, createOptimisticMutation, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
110
+ export { CreateOptimisticMutationConfig, MutationCallbacks, MutationMessages, OptimisticMutationConfig, TransitionMutationConfig, TransitionMutationReturn, configureToast, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
package/dist/mutation.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  import { isArcApiError, isAutoIdempotency } from "./client.js";
4
- import { QUERY_CONFIGS } from "./query.js";
5
4
  import { useMutation, useQueryClient } from "@tanstack/react-query";
6
5
  import { useCallback, useRef, useTransition } from "react";
7
6
 
@@ -162,8 +161,6 @@ function useOptimisticMutation(config) {
162
161
  }
163
162
  });
164
163
  }
165
- /** @deprecated Use `useOptimisticMutation` */
166
- const createOptimisticMutation = useOptimisticMutation;
167
164
 
168
165
  //#endregion
169
- export { QUERY_CONFIGS, configureToast, createOptimisticMutation, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
166
+ export { configureToast, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
@@ -1,7 +1,15 @@
1
1
  import { QueryClient, dehydrate } from "@tanstack/react-query";
2
2
 
3
3
  //#region src/prefetch.d.ts
4
- interface PrefetchOptions {
4
+ interface PrefetchAuthContext {
5
+ /** Auth token for protected endpoints. Required for bearer/header auth on server. */
6
+ token?: string | null;
7
+ /** Organization ID for multi-tenant prefetch. Sent as x-organization-id header. */
8
+ organizationId?: string | null;
9
+ /** Additional headers (e.g., x-api-key for header auth). */
10
+ headers?: Record<string, string>;
11
+ }
12
+ interface PrefetchOptions extends PrefetchAuthContext {
5
13
  staleTime?: number;
6
14
  }
7
15
  interface PrefetchDetailOptions extends PrefetchOptions {
@@ -74,28 +82,43 @@ declare function createCrudPrefetcher(api: {
74
82
  params?: Record<string, unknown>;
75
83
  token?: string | null;
76
84
  organizationId?: string | null;
85
+ options?: {
86
+ headerOptions?: Record<string, string>;
87
+ };
77
88
  }) => Promise<unknown>;
78
89
  getById: (opts: {
79
90
  id: string;
80
91
  token?: string | null;
81
92
  organizationId?: string | null;
93
+ options?: {
94
+ headerOptions?: Record<string, string>;
95
+ };
82
96
  }) => Promise<unknown>;
83
97
  getBySlug?: (opts: {
84
98
  slug: string;
85
99
  token?: string | null;
86
100
  organizationId?: string | null;
87
101
  params?: Record<string, unknown>;
102
+ options?: {
103
+ headerOptions?: Record<string, string>;
104
+ };
88
105
  }) => Promise<unknown>;
89
106
  getDeleted?: (opts: {
90
107
  params?: Record<string, unknown>;
91
108
  token?: string | null;
92
109
  organizationId?: string | null;
110
+ options?: {
111
+ headerOptions?: Record<string, string>;
112
+ };
93
113
  }) => Promise<unknown>;
94
114
  getTree?: (opts: {
95
115
  params?: Record<string, unknown>;
96
116
  token?: string | null;
97
117
  organizationId?: string | null;
118
+ options?: {
119
+ headerOptions?: Record<string, string>;
120
+ };
98
121
  }) => Promise<unknown>;
99
122
  }, entityKey: string): CrudPrefetcher;
100
123
  //#endregion
101
- export { CrudPrefetcher, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
124
+ export { CrudPrefetcher, PrefetchAuthContext, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
package/dist/prefetch.js CHANGED
@@ -31,75 +31,90 @@ function createCrudPrefetcher(api, entityKey) {
31
31
  const KEYS = createQueryKeys(entityKey);
32
32
  return {
33
33
  async prefetchList(queryClient, params = {}, options = {}) {
34
- const { organizationId, ...restParams } = params;
35
- const scope = organizationId ? "tenant" : "super-admin";
34
+ const { organizationId: paramOrgId, ...restParams } = params;
35
+ const orgId = paramOrgId ?? options.organizationId ?? null;
36
+ const scope = orgId ? "tenant" : "super-admin";
36
37
  const queryKey = KEYS.scopedList(scope, {
37
- organizationId,
38
+ ...orgId ? { organizationId: orgId } : {},
38
39
  ...restParams
39
40
  });
40
41
  await queryClient.prefetchQuery({
41
42
  queryKey,
42
43
  queryFn: () => api.getAll({
43
44
  params: restParams,
44
- organizationId: organizationId ?? null
45
+ token: options.token ?? null,
46
+ organizationId: orgId,
47
+ ...options.headers ? { options: { headerOptions: options.headers } } : {}
45
48
  }),
46
49
  staleTime: options.staleTime
47
50
  });
48
51
  },
49
52
  async prefetchDetail(queryClient, id, options = {}) {
50
- const { params, staleTime } = options;
53
+ const { params, staleTime, token, organizationId } = options;
51
54
  const baseKey = KEYS.detail(id);
52
55
  const queryKey = params ? [...baseKey, params] : baseKey;
53
56
  await queryClient.prefetchQuery({
54
57
  queryKey,
55
58
  queryFn: () => api.getById({
56
59
  id,
57
- ...params ? { params } : {}
60
+ token: token ?? null,
61
+ organizationId: organizationId ?? null,
62
+ ...params ? { params } : {},
63
+ ...options.headers ? { options: { headerOptions: options.headers } } : {}
58
64
  }),
59
65
  staleTime
60
66
  });
61
67
  },
62
68
  async prefetchBySlug(queryClient, slug, options = {}) {
63
69
  if (!api.getBySlug) throw new Error(`[arc-next] prefetchBySlug requires an api with getBySlug (slugLookup preset)`);
64
- const { params, staleTime } = options;
70
+ const { params, staleTime, token, organizationId } = options;
65
71
  const queryKey = params ? KEYS.custom("slug", slug, params) : KEYS.custom("slug", slug);
66
72
  await queryClient.prefetchQuery({
67
73
  queryKey,
68
74
  queryFn: () => api.getBySlug({
69
75
  slug,
70
- ...params ? { params } : {}
76
+ token: token ?? null,
77
+ organizationId: organizationId ?? null,
78
+ ...params ? { params } : {},
79
+ ...options.headers ? { options: { headerOptions: options.headers } } : {}
71
80
  }),
72
81
  staleTime
73
82
  });
74
83
  },
75
84
  async prefetchDeleted(queryClient, params = {}, options = {}) {
76
85
  if (!api.getDeleted) throw new Error(`[arc-next] prefetchDeleted requires an api with getDeleted (softDelete preset)`);
77
- const { organizationId, ...restParams } = params;
86
+ const { organizationId: paramOrgId, ...restParams } = params;
87
+ const orgId = paramOrgId ?? options.organizationId ?? null;
78
88
  const queryKey = KEYS.custom("deleted", {
79
- organizationId,
89
+ ...orgId ? { organizationId: orgId } : {},
80
90
  ...restParams
81
91
  });
82
92
  await queryClient.prefetchQuery({
83
93
  queryKey,
84
94
  queryFn: () => api.getDeleted({
85
95
  params: restParams,
86
- organizationId: organizationId ?? null
96
+ token: options.token ?? null,
97
+ organizationId: orgId,
98
+ ...options.headers ? { options: { headerOptions: options.headers } } : {}
87
99
  }),
88
100
  staleTime: options.staleTime
89
101
  });
90
102
  },
91
103
  async prefetchTree(queryClient, params = {}, options = {}) {
92
104
  if (!api.getTree) throw new Error(`[arc-next] prefetchTree requires an api with getTree (tree preset)`);
93
- const { organizationId, ...restParams } = params;
105
+ const { organizationId: paramOrgId, ...restParams } = params;
106
+ const orgId = paramOrgId ?? options.organizationId ?? null;
94
107
  const queryKey = KEYS.custom("tree", {
95
- organizationId,
108
+ ...orgId ? { organizationId: orgId } : {},
96
109
  ...restParams
97
110
  });
98
111
  await queryClient.prefetchQuery({
99
112
  queryKey,
100
113
  queryFn: () => api.getTree({
101
114
  params: restParams,
102
- organizationId: organizationId ?? null
115
+ token: options.token ?? null,
116
+ organizationId: orgId,
117
+ ...options.headers ? { options: { headerOptions: options.headers } } : {}
103
118
  }),
104
119
  staleTime: options.staleTime
105
120
  });
package/dist/query.d.ts CHANGED
@@ -86,16 +86,27 @@ interface QueryKeys {
86
86
  list: (params?: unknown) => QueryKey;
87
87
  details: () => QueryKey;
88
88
  detail: (id: string) => QueryKey;
89
+ /** Tenant-scoped detail key. Use when IDs are only unique within an org. */
90
+ scopedDetail: (id: string, organizationId: string | null) => QueryKey;
89
91
  custom: (key: string, ...args: unknown[]) => QueryKey;
90
92
  scopedList: (scope: string, params?: unknown) => QueryKey;
91
93
  }
92
94
  interface CacheUtils<T> {
93
95
  invalidateAll: (client: QueryClient) => Promise<void>;
94
96
  invalidateLists: (client: QueryClient) => Promise<void>;
97
+ /** Invalidate detail by ID (prefix-matches all scoped/parameterized variants). */
95
98
  invalidateDetail: (client: QueryClient, id: string) => Promise<void>;
96
99
  setDetail: (client: QueryClient, id: string, data: T) => void;
97
100
  getDetail: (client: QueryClient, id: string) => T | undefined;
98
101
  removeDetail: (client: QueryClient, id: string) => void;
102
+ /** Invalidate tenant-scoped detail (prefix-matches parameterized variants within org). */
103
+ invalidateScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => Promise<void>;
104
+ /** Set tenant-scoped detail cache. */
105
+ setScopedDetail: (client: QueryClient, id: string, organizationId: string | null, data: T) => void;
106
+ /** Get tenant-scoped detail from cache. */
107
+ getScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => T | undefined;
108
+ /** Remove tenant-scoped detail from cache. */
109
+ removeScopedDetail: (client: QueryClient, id: string, organizationId: string | null) => void;
99
110
  }
100
111
  declare const DEFAULT_QUERY_CONFIG: {
101
112
  readonly staleTime: number;
@@ -221,11 +232,5 @@ declare function useInfiniteListQuery<T>({
221
232
  getPreviousPageParam,
222
233
  maxPages
223
234
  }: CreateInfiniteListQueryConfig): InfiniteListQueryResult<T>;
224
- /** @deprecated Use `useListQuery` */
225
- declare const createListQuery: typeof useListQuery;
226
- /** @deprecated Use `useDetailQuery` */
227
- declare const createDetailQuery: typeof useDetailQuery;
228
- /** @deprecated Use `useInfiniteListQuery` */
229
- declare const createInfiniteListQuery: typeof useInfiniteListQuery;
230
235
  //#endregion
231
- export { CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, PaginationData, QUERY_CONFIGS, QueryKeys, RequestPassthrough, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
236
+ export { CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, PaginationData, QUERY_CONFIGS, QueryKeys, RequestPassthrough, createCacheUtils, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
package/dist/query.js CHANGED
@@ -120,6 +120,16 @@ function createQueryKeys(entityKey) {
120
120
  "detail",
121
121
  id
122
122
  ],
123
+ scopedDetail: (id, organizationId) => organizationId ? [
124
+ entityKey,
125
+ "detail",
126
+ id,
127
+ { _org: organizationId }
128
+ ] : [
129
+ entityKey,
130
+ "detail",
131
+ id
132
+ ],
123
133
  custom: (key, ...args) => [
124
134
  entityKey,
125
135
  key,
@@ -144,7 +154,13 @@ function createCacheUtils(KEYS) {
144
154
  getDetail: (client, id) => {
145
155
  return client.getQueryData(KEYS.detail(id))?.data;
146
156
  },
147
- removeDetail: (client, id) => client.removeQueries({ queryKey: KEYS.detail(id) })
157
+ removeDetail: (client, id) => client.removeQueries({ queryKey: KEYS.detail(id) }),
158
+ invalidateScopedDetail: (client, id, organizationId) => client.invalidateQueries({ queryKey: KEYS.scopedDetail(id, organizationId) }),
159
+ setScopedDetail: (client, id, organizationId, data) => client.setQueryData(KEYS.scopedDetail(id, organizationId), { data }),
160
+ getScopedDetail: (client, id, organizationId) => {
161
+ return client.getQueryData(KEYS.scopedDetail(id, organizationId))?.data;
162
+ },
163
+ removeScopedDetail: (client, id, organizationId) => client.removeQueries({ queryKey: KEYS.scopedDetail(id, organizationId) })
148
164
  };
149
165
  }
150
166
  function useListQuery({ queryKey, queryFn, enabled = true, options = {}, prefillDetailCache = true, detailKeyBuilder, itemIdResolver, select }) {
@@ -239,12 +255,6 @@ function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {},
239
255
  data: query.data
240
256
  };
241
257
  }
242
- /** @deprecated Use `useListQuery` */
243
- const createListQuery = useListQuery;
244
- /** @deprecated Use `useDetailQuery` */
245
- const createDetailQuery = useDetailQuery;
246
- /** @deprecated Use `useInfiniteListQuery` */
247
- const createInfiniteListQuery = useInfiniteListQuery;
248
258
 
249
259
  //#endregion
250
- export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createDetailQuery, createInfiniteListQuery, createListQuery, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
260
+ export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery };
package/dist/sse.d.ts CHANGED
@@ -9,12 +9,19 @@ interface ArcServerEvent {
9
9
  id?: string;
10
10
  }
11
11
  interface EventStreamOptions {
12
- /** SSE endpoint URL (absolute or relative to baseUrl). Default: `/{basePath}/{resource}/events/stream` */
12
+ /** Full SSE endpoint URL. When set, overrides `path` and `baseUrl`. Use for non-Arc backends or custom URLs. */
13
13
  url?: string;
14
- /** Resource name used for the default endpoint path and event filtering. */
14
+ /**
15
+ * Resource name for automatic pattern filtering.
16
+ * When set and `patterns` is empty, auto-generates `['resource.*']` filter.
17
+ * Does NOT affect the endpoint URL — use `path` for that.
18
+ */
15
19
  resource?: string;
16
- /** Base path for the events endpoint. Default: '/api/v1' */
17
- basePath?: string;
20
+ /**
21
+ * SSE endpoint path (appended to baseUrl). Default: '/events/stream'.
22
+ * Matches Arc's ssePlugin default. Override if your backend uses a custom path.
23
+ */
24
+ path?: string;
18
25
  /** Event patterns to listen for (e.g., ['agents.created', 'agents.updated']). When empty, all events are received. */
19
26
  patterns?: string[];
20
27
  /** Query keys to invalidate when any event is received. */
package/dist/sse.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { getAuthContext, getAuthMode } from "./client.js";
3
+ import { getAuthContext, getAuthMode, getBaseUrl } from "./client.js";
4
4
  import { useQueryClient } from "@tanstack/react-query";
5
5
  import { useCallback, useEffect, useRef, useState } from "react";
6
6
 
@@ -19,7 +19,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
19
19
  * });
20
20
  */
21
21
  function useEventStream(options) {
22
- const { url, resource, basePath = "/api/v1", enabled = true, reconnectDelay = 3e3, maxReconnectAttempts = Infinity, withCredentials } = options;
22
+ const { url, resource, path: ssePath = "/events/stream", enabled = true, reconnectDelay = 3e3, maxReconnectAttempts = Infinity, withCredentials } = options;
23
23
  const queryClient = useQueryClient();
24
24
  const [isConnected, setIsConnected] = useState(false);
25
25
  const [lastEvent, setLastEvent] = useState(null);
@@ -38,20 +38,20 @@ function useEventStream(options) {
38
38
  invalidateKeysRef.current = options.invalidateQueries ?? [];
39
39
  const buildUrl = useCallback(() => {
40
40
  if (url) return url;
41
- if (!resource) throw new Error("[arc-next] useEventStream requires either `url` or `resource`");
42
41
  const auth = getAuthContext();
43
42
  const params = new URLSearchParams();
44
43
  const patterns = patternsRef.current;
45
- if (patterns.length > 0) params.set("patterns", patterns.join(","));
44
+ const effectivePatterns = patterns.length > 0 ? patterns : resource ? [`${resource}.*`] : [];
45
+ if (effectivePatterns.length > 0) params.set("patterns", effectivePatterns.join(","));
46
46
  if (auth.organizationId) params.set("organizationId", auth.organizationId);
47
47
  if (auth.token) params.set("token", auth.token);
48
48
  const qs = params.toString();
49
- const base = `${basePath}/${resource}/events/stream`;
49
+ const base = `${getBaseUrl()}${ssePath}`;
50
50
  return qs ? `${base}?${qs}` : base;
51
51
  }, [
52
52
  url,
53
53
  resource,
54
- basePath
54
+ ssePath
55
55
  ]);
56
56
  const connect = useCallback(() => {
57
57
  if (esRef.current) esRef.current.close();
@@ -110,6 +110,7 @@ function useEventStream(options) {
110
110
  }, []);
111
111
  const reconnect = useCallback(() => {
112
112
  reconnectAttemptsRef.current = 0;
113
+ manualCloseRef.current = false;
113
114
  connect();
114
115
  }, [connect]);
115
116
  useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc-next",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "React + TanStack Query SDK for Arc resources",
5
5
  "type": "module",
6
6
  "sideEffects": false,