@classytic/arc-next 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,8 +30,9 @@ import { useRouter } from "next/navigation";
30
30
  // Required — sets the API base URL and auth mode
31
31
  configureClient({
32
32
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
33
- authMode: "cookie", // 'cookie' for Better Auth, 'bearer' for token auth (default)
34
- // credentials: 'omit', // override if you don't want cookies sent cross-origin
33
+ authMode: "cookie", // 'cookie' | 'bearer' (default) | 'header'
34
+ // apiVersion: '2', // sends Accept-Version header
35
+ // autoIdempotency: true, // auto Idempotency-Key on mutations (retry-safe)
35
36
  });
36
37
 
37
38
  // Optional — auto-inject tenant context into queries/mutations
@@ -59,6 +60,7 @@ configureNavigation(useRouter);
59
60
  | `@classytic/arc-next/hooks` | `createCrudHooks`, `configureNavigation` | Yes |
60
61
  | `@classytic/arc-next/query-client` | `getQueryClient` (SSR-safe singleton) | No |
61
62
  | `@classytic/arc-next/prefetch` | `createCrudPrefetcher`, `dehydrate` (SSR prefetch) | No |
63
+ | `@classytic/arc-next/sse` | `useEventStream` (Server-Sent Events for real-time cache invalidation) | Yes |
62
64
 
63
65
  No barrel index — every file is its own entry point. Tree-shakeable (`sideEffects: false`).
64
66
 
@@ -148,25 +150,28 @@ export function ProductsPage() {
148
150
  ```ts
149
151
  configureClient({
150
152
  baseUrl: string; // Required — API base URL
151
- authMode?: 'cookie' | 'bearer'; // Default: 'bearer'
153
+ authMode?: 'bearer' | 'cookie' | 'header'; // Default: 'bearer'
152
154
  credentials?: RequestCredentials; // Default: derived from authMode
153
- internalApiKey?: string; // Optional — sent as x-internal-api-key header
154
- defaultHeaders?: Record<string, string>; // Optional — merged into every request
155
+ internalApiKey?: string; // Sent as x-internal-api-key header
156
+ defaultHeaders?: Record<string, string>; // Merged into every request
157
+ apiVersion?: string; // Sent as Accept-Version header
158
+ autoIdempotency?: boolean; // Auto Idempotency-Key on mutations (retry-safe)
155
159
  });
156
160
  ```
157
161
 
158
- - `authMode: 'bearer'` (default) — requires a token; queries disabled until token provided; `credentials: 'same-origin'`
159
- - `authMode: 'cookie'` — HTTP-only cookies (e.g. Better Auth); queries always enabled; `credentials: 'include'`
160
- - `credentials` — explicit override: `'include'` (send cookies cross-origin), `'same-origin'` (same-origin only), `'omit'` (never send cookies)
162
+ - `authMode: 'bearer'` (default) — requires token; queries disabled until provided
163
+ - `authMode: 'cookie'` — HTTP-only cookies (e.g. Better Auth); queries always enabled
164
+ - `authMode: 'header'` — custom header auth (e.g. `x-api-key`); uses `headerName` from `configureAuth`
161
165
 
162
- Must be called before any API requests. Throws if not configured.
166
+ Must be called before any API requests. Warns if called on the server (SSR safety).
163
167
 
164
168
  ### `configureAuth(config)`
165
169
 
166
170
  ```ts
167
171
  configureAuth({
168
- getToken?: () => string | null; // For bearer auth — return access token
172
+ getToken?: () => string | null; // For bearer/header auth — return access token or API key
169
173
  getOrgId?: () => string | null; // Return active organization ID
174
+ headerName?: string; // Custom header name for authMode: 'header' (default: 'x-api-key')
170
175
  });
171
176
  ```
172
177
 
@@ -235,12 +240,15 @@ Factory that returns everything you need. The `api` parameter accepts any `creat
235
240
  const {
236
241
  KEYS, cache,
237
242
  useList, useDetail, useInfiniteList,
238
- useActions, useUpload, useSearch, useCustomMutation,
243
+ useActions, useBulkActions,
244
+ useDeleted, useDetailBySlug, useTree, useChildren, useFindBy,
245
+ useUpload, useSearch, useCustomMutation,
239
246
  useNavigation,
240
247
  } = createCrudHooks<Product, CreateProduct>({
241
248
  api: productsApi, // from createCrudApi() — types inferred, no cast
242
249
  entityKey: "products", // TanStack Query key prefix
243
250
  singular: "Product", // for toast messages
251
+ idField: "sku", // optional — custom ID field for cache keys (default: _id → id)
244
252
  defaults: { // optional
245
253
  staleTime: 60_000,
246
254
  messages: { createSuccess: "Product added!" },
@@ -393,6 +401,52 @@ const { mutateAsync: publish, isPending } = useCustomMutation({
393
401
  });
394
402
  ```
395
403
 
404
+ #### `useDeleted(params?, options?)`
405
+
406
+ List soft-deleted items. Requires `softDelete` preset on the Arc resource.
407
+
408
+ #### `useDetailBySlug(slug, options?)`
409
+
410
+ Fetch a single item by slug (`GET /slug/:slug`). Requires `slugLookup` preset.
411
+
412
+ #### `useTree(params?, options?)`
413
+
414
+ Fetch hierarchical tree data (`GET /tree`). Requires `tree` preset.
415
+
416
+ #### `useChildren(parentId, params?, options?)`
417
+
418
+ Fetch children of a parent node (`GET /:parentId/children`). Requires `tree` preset.
419
+
420
+ #### `useFindBy(field, value, options?)`
421
+
422
+ Query by a single field with optional operator:
423
+
424
+ ```ts
425
+ const { items } = useFindBy("status", "active");
426
+ const { items } = useFindBy("price", 50, { operator: "gte" });
427
+ ```
428
+
429
+ #### `useBulkActions()`
430
+
431
+ ```ts
432
+ const { bulkCreate, bulkUpdate, bulkRemove } = useBulkActions();
433
+ await bulkCreate({ data: [{ name: "A" }, { name: "B" }] });
434
+ ```
435
+
436
+ #### `useEventStream(options)` (from `./sse`)
437
+
438
+ Subscribe to Arc SSE events with auto-reconnect and query invalidation:
439
+
440
+ ```ts
441
+ import { useEventStream } from "@classytic/arc-next/sse";
442
+
443
+ const { isConnected, lastEvent } = useEventStream({
444
+ resource: "agents",
445
+ patterns: ["agents.created", "agents.updated"],
446
+ invalidateQueries: [agentKeys.lists()],
447
+ });
448
+ ```
449
+
396
450
  ### Query Keys (`KEYS`)
397
451
 
398
452
  ```ts
@@ -713,8 +767,15 @@ const adminApi = createCrudApi("users", {
713
767
  - **Pagination Normalization** — Handles `docs`/`data`/`items`/`results` + any custom key, offset/keyset/aggregate pagination
714
768
  - **Detail Cache Prefilling** — List results auto-populate detail query cache
715
769
  - **React 19 Transitions** — `useMutationWithTransition` wraps invalidation in `startTransition`
716
- - **Cookie & Bearer Auth** — `authMode: 'cookie'` for Better Auth, `'bearer'` for token auth, configurable `credentials` policy
717
- - **SSR Prefetch** — `createCrudPrefetcher` + `dehydrate` for server component data loading
770
+ - **Cookie, Bearer & Header Auth** — `authMode: 'cookie'` / `'bearer'` / `'header'` (custom header like `x-api-key`)
771
+ - **Custom ID Fields** — `idField` on `createCrudHooks` for resources keyed by `sku`, `slug`, `code`, etc.
772
+ - **Preset Hooks** — `useDeleted`, `useBulkActions`, `useDetailBySlug`, `useTree`, `useChildren`, `useFindBy`
773
+ - **SSE Real-Time** — `useEventStream` with auto-reconnect, pattern filtering, query invalidation
774
+ - **Infinite Scroll** — `maxPages` for memory management with automatic scroll-back support
775
+ - **Idempotency** — `autoIdempotency` generates retry-safe keys at mutation level
776
+ - **API Versioning** — `apiVersion` sends `Accept-Version` header
777
+ - **SSR Prefetch** — `createCrudPrefetcher` + `prefetchBySlug` / `prefetchDeleted` / `prefetchTree`
778
+ - **SSR Safety** — warns when `configureClient`/`configureAuth` called on the server
718
779
  - **Multi-Client** — `createClient()` for multiple API backends side by side
719
780
  - **Pluggable Toast** — `configureToast()` — use sonner, react-hot-toast, or anything
720
781
  - **Pluggable Navigation** — `configureNavigation()` — use Next.js, React Router, or any router
package/dist/api.d.ts CHANGED
@@ -52,9 +52,22 @@ interface DeleteResponse {
52
52
  soft?: boolean;
53
53
  };
54
54
  }
55
+ interface BulkCreateResponse<T = unknown> {
56
+ success: boolean;
57
+ data?: T[];
58
+ count?: number;
59
+ }
60
+ interface BulkUpdateResponse {
61
+ success: boolean;
62
+ modifiedCount?: number;
63
+ }
64
+ interface BulkDeleteResponse {
65
+ success: boolean;
66
+ deletedCount?: number;
67
+ }
55
68
  type SortDirection = 1 | -1 | 'asc' | 'desc';
56
69
  type SortSpec = Record<string, SortDirection> | string;
57
- type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith' | 'regex';
70
+ type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith' | 'regex' | 'like' | 'exists' | 'size' | 'type';
58
71
  interface QueryParams {
59
72
  page?: number;
60
73
  limit?: number;
@@ -65,6 +78,13 @@ interface QueryParams {
65
78
  populate?: string | string[];
66
79
  populateOptions?: PopulateOption[];
67
80
  lean?: boolean | 'true' | 'false';
81
+ /** Database-agnostic joins. Maps alias → collection or full lookup config. */
82
+ lookup?: Record<string, string | {
83
+ from: string;
84
+ localField: string;
85
+ foreignField: string;
86
+ select?: string;
87
+ }>;
68
88
  [key: string]: unknown;
69
89
  }
70
90
  interface RequestOptions {
@@ -218,6 +238,103 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
218
238
  params?: QueryParams;
219
239
  options?: Omit<RequestOptions, 'token' | 'organizationId'>;
220
240
  }): Promise<TResponse>;
241
+ getDeleted({
242
+ token,
243
+ organizationId,
244
+ params,
245
+ options
246
+ }?: {
247
+ token?: string | null;
248
+ organizationId?: string | null;
249
+ params?: QueryParams;
250
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
251
+ }): Promise<PaginatedResponse<TDoc>>;
252
+ restore({
253
+ token,
254
+ organizationId,
255
+ id,
256
+ options
257
+ }: {
258
+ token?: string | null;
259
+ organizationId?: string | null;
260
+ id: string;
261
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
262
+ }): Promise<ApiResponse<TDoc>>;
263
+ bulkCreate({
264
+ token,
265
+ organizationId,
266
+ data,
267
+ options
268
+ }: {
269
+ token?: string | null;
270
+ organizationId?: string | null;
271
+ data: TCreate[];
272
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
273
+ }): Promise<BulkCreateResponse<TDoc>>;
274
+ bulkUpdate({
275
+ token,
276
+ organizationId,
277
+ filter,
278
+ data,
279
+ options
280
+ }: {
281
+ token?: string | null;
282
+ organizationId?: string | null;
283
+ filter: Record<string, unknown>;
284
+ data: TUpdate;
285
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
286
+ }): Promise<BulkUpdateResponse>;
287
+ bulkDelete({
288
+ token,
289
+ organizationId,
290
+ filter,
291
+ options
292
+ }: {
293
+ token?: string | null;
294
+ organizationId?: string | null;
295
+ filter: Record<string, unknown>;
296
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
297
+ }): Promise<BulkDeleteResponse>;
298
+ getBySlug({
299
+ token,
300
+ organizationId,
301
+ slug,
302
+ params,
303
+ options
304
+ }: {
305
+ token?: string | null;
306
+ organizationId?: string | null;
307
+ slug: string;
308
+ params?: {
309
+ select?: string;
310
+ populate?: string | string[];
311
+ };
312
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
313
+ }): Promise<ApiResponse<TDoc>>;
314
+ getTree({
315
+ token,
316
+ organizationId,
317
+ params,
318
+ options
319
+ }?: {
320
+ token?: string | null;
321
+ organizationId?: string | null;
322
+ params?: QueryParams;
323
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
324
+ }): Promise<ApiResponse<TDoc[]>>;
325
+ getChildren({
326
+ token,
327
+ organizationId,
328
+ parentId,
329
+ params,
330
+ options
331
+ }: {
332
+ token?: string | null;
333
+ organizationId?: string | null;
334
+ parentId: string;
335
+ params?: QueryParams;
336
+ options?: Omit<RequestOptions, 'token' | 'organizationId'>;
337
+ }): Promise<PaginatedResponse<TDoc>>;
221
338
  }
222
339
  declare function createCrudApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>>(entity: string, config?: BaseApiConfig): BaseApi<TDoc, TCreate, TUpdate>;
223
340
  type ExtractDoc<T> = T extends PaginatedResponse<infer D> ? D : never;
@@ -225,4 +342,4 @@ declare function isOffsetPagination<T>(response: PaginatedResponse<T>): response
225
342
  declare function isKeysetPagination<T>(response: PaginatedResponse<T>): response is KeysetPaginationResponse<T>;
226
343
  declare function isAggregatePagination<T>(response: PaginatedResponse<T>): response is AggregatePaginationResponse<T>;
227
344
  //#endregion
228
- export { AggregatePaginationResponse, ApiResponse, BaseApi, BaseApiConfig, DeleteResponse, ExtractDoc, FilterOperator, KeysetPaginationResponse, OffsetPaginationResponse, PaginatedResponse, PopulateOption, QueryParams, RequestOptions, SortDirection, SortSpec, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
345
+ export { AggregatePaginationResponse, ApiResponse, BaseApi, BaseApiConfig, BulkCreateResponse, BulkDeleteResponse, BulkUpdateResponse, DeleteResponse, ExtractDoc, FilterOperator, KeysetPaginationResponse, OffsetPaginationResponse, PaginatedResponse, PopulateOption, QueryParams, RequestOptions, SortDirection, SortSpec, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
package/dist/api.js CHANGED
@@ -48,6 +48,19 @@ var BaseApi = class {
48
48
  if (Array.isArray(value) && value.length > 0) result[key] = value;
49
49
  return;
50
50
  }
51
+ if (key === "lookup") {
52
+ if (typeof value === "object" && value !== null) Object.entries(value).forEach(([alias, lv]) => {
53
+ if (typeof lv === "string") result[`lookup[${alias}]`] = lv;
54
+ else if (typeof lv === "object" && lv !== null) {
55
+ const cfg = lv;
56
+ result[`lookup[${alias}][from]`] = cfg.from;
57
+ result[`lookup[${alias}][localField]`] = cfg.localField;
58
+ result[`lookup[${alias}][foreignField]`] = cfg.foreignField;
59
+ if (cfg.select) result[`lookup[${alias}][select]`] = cfg.select;
60
+ }
61
+ });
62
+ return;
63
+ }
51
64
  if (value !== void 0 && value !== "") if (["page", "limit"].includes(key)) result[key] = parseInt(String(value)) || (key === "page" ? 1 : 10);
52
65
  else if (Array.isArray(value)) {
53
66
  if (value.length > 1) result[`${key}[in]`] = value.join(",");
@@ -169,6 +182,97 @@ var BaseApi = class {
169
182
  if (organizationId) requestOptions.organizationId = organizationId;
170
183
  return this.requestFn(method, url, this.withHeaders(requestOptions));
171
184
  }
185
+ async getDeleted({ token = null, organizationId = null, params = {}, options = {} } = {}) {
186
+ const mergedParams = {
187
+ ...this.config.defaultParams,
188
+ ...params
189
+ };
190
+ const processedParams = this.prepareParams(mergedParams);
191
+ const queryString = this.createQueryString(processedParams);
192
+ const requestOptions = {
193
+ cache: this.config.cache,
194
+ ...options
195
+ };
196
+ if (token) requestOptions.token = token;
197
+ if (organizationId) requestOptions.organizationId = organizationId;
198
+ return this.requestFn("GET", `${this.baseUrl}/deleted?${queryString}`, this.withHeaders(requestOptions));
199
+ }
200
+ async restore({ token, organizationId = null, id, options = {} }) {
201
+ if (!id) throw new Error("ID is required");
202
+ const requestOptions = { ...options };
203
+ if (token) requestOptions.token = token;
204
+ if (organizationId) requestOptions.organizationId = organizationId;
205
+ return this.requestFn("POST", `${this.baseUrl}/${id}/restore`, this.withHeaders(requestOptions));
206
+ }
207
+ async bulkCreate({ token, organizationId = null, data, options = {} }) {
208
+ const requestOptions = {
209
+ body: data,
210
+ ...options
211
+ };
212
+ if (token) requestOptions.token = token;
213
+ if (organizationId) requestOptions.organizationId = organizationId;
214
+ return this.requestFn("POST", `${this.baseUrl}/bulk`, this.withHeaders(requestOptions));
215
+ }
216
+ async bulkUpdate({ token, organizationId = null, filter, data, options = {} }) {
217
+ const requestOptions = {
218
+ body: {
219
+ filter,
220
+ data
221
+ },
222
+ ...options
223
+ };
224
+ if (token) requestOptions.token = token;
225
+ if (organizationId) requestOptions.organizationId = organizationId;
226
+ return this.requestFn("PATCH", `${this.baseUrl}/bulk`, this.withHeaders(requestOptions));
227
+ }
228
+ async bulkDelete({ token, organizationId = null, filter, options = {} }) {
229
+ const requestOptions = {
230
+ body: { filter },
231
+ ...options
232
+ };
233
+ if (token) requestOptions.token = token;
234
+ if (organizationId) requestOptions.organizationId = organizationId;
235
+ return this.requestFn("DELETE", `${this.baseUrl}/bulk`, this.withHeaders(requestOptions));
236
+ }
237
+ async getBySlug({ token = null, organizationId = null, slug, params = {}, options = {} }) {
238
+ if (!slug) throw new Error("Slug is required");
239
+ const queryString = this.createQueryString(params);
240
+ const url = queryString ? `${this.baseUrl}/slug/${slug}?${queryString}` : `${this.baseUrl}/slug/${slug}`;
241
+ const requestOptions = {
242
+ cache: this.config.cache,
243
+ ...options
244
+ };
245
+ if (token) requestOptions.token = token;
246
+ if (organizationId) requestOptions.organizationId = organizationId;
247
+ return this.requestFn("GET", url, this.withHeaders(requestOptions));
248
+ }
249
+ async getTree({ token = null, organizationId = null, params = {}, options = {} } = {}) {
250
+ const processedParams = this.prepareParams(params);
251
+ const queryString = this.createQueryString(processedParams);
252
+ const requestOptions = {
253
+ cache: this.config.cache,
254
+ ...options
255
+ };
256
+ if (token) requestOptions.token = token;
257
+ if (organizationId) requestOptions.organizationId = organizationId;
258
+ return this.requestFn("GET", `${this.baseUrl}/tree?${queryString}`, this.withHeaders(requestOptions));
259
+ }
260
+ async getChildren({ token = null, organizationId = null, parentId, params = {}, options = {} }) {
261
+ if (!parentId) throw new Error("Parent ID is required");
262
+ const mergedParams = {
263
+ ...this.config.defaultParams,
264
+ ...params
265
+ };
266
+ const processedParams = this.prepareParams(mergedParams);
267
+ const queryString = this.createQueryString(processedParams);
268
+ const requestOptions = {
269
+ cache: this.config.cache,
270
+ ...options
271
+ };
272
+ if (token) requestOptions.token = token;
273
+ if (organizationId) requestOptions.organizationId = organizationId;
274
+ return this.requestFn("GET", `${this.baseUrl}/${parentId}/children?${queryString}`, this.withHeaders(requestOptions));
275
+ }
172
276
  };
173
277
  function createCrudApi(entity, config = {}) {
174
278
  return new BaseApi(entity, config);
package/dist/client.d.ts CHANGED
@@ -55,7 +55,7 @@ interface ClientConfig {
55
55
  * - 'bearer' (default): Requires a token for authenticated requests. Queries are disabled until a token is provided.
56
56
  * - 'cookie': Auth is handled via HTTP-only cookies (e.g. Better Auth). Queries are always enabled — no token needed.
57
57
  */
58
- authMode?: 'bearer' | 'cookie';
58
+ authMode?: 'bearer' | 'cookie' | 'header';
59
59
  /**
60
60
  * Fetch credentials policy.
61
61
  * - 'include': Always send cookies cross-origin (required for cookie-based auth).
@@ -67,6 +67,18 @@ interface ClientConfig {
67
67
  * - `authMode: 'bearer'` (default) → `'same-origin'`
68
68
  */
69
69
  credentials?: RequestCredentials;
70
+ /**
71
+ * API version sent as `Accept-Version` header.
72
+ * Use when the Arc backend has versioning enabled.
73
+ * @example '2' // sends Accept-Version: 2
74
+ */
75
+ apiVersion?: string;
76
+ /**
77
+ * Auto-generate `Idempotency-Key` header for POST/PUT/PATCH requests.
78
+ * Prevents duplicate mutations on network retries.
79
+ * Default: false — opt-in per-request via `idempotencyKey` option.
80
+ */
81
+ autoIdempotency?: boolean;
70
82
  }
71
83
  /**
72
84
  * Configure the API client. Call once at app init before any API requests.
@@ -86,10 +98,14 @@ declare function configureClient(config: ClientConfig): void;
86
98
  /**
87
99
  * Get the configured auth mode. Returns 'bearer' if not configured.
88
100
  */
89
- declare function getAuthMode(): 'bearer' | 'cookie';
101
+ declare function getAuthMode(): 'bearer' | 'cookie' | 'header';
102
+ /** Whether auto-idempotency is enabled on the global client. */
103
+ declare function isAutoIdempotency(): boolean;
90
104
  interface AuthConfig {
91
105
  getToken?: () => string | null;
92
106
  getOrgId?: () => string | null;
107
+ /** Custom auth header name. Used when authMode is 'header'. Default: 'x-api-key' */
108
+ headerName?: string;
93
109
  }
94
110
  /**
95
111
  * Configure auth context for automatic token/orgId injection.
@@ -125,14 +141,8 @@ interface ApiRequestOptions {
125
141
  tags?: string[];
126
142
  cache?: RequestCache;
127
143
  signal?: AbortSignal;
128
- }
129
- interface BlobResponse {
130
- data: Blob;
131
- response: Response;
132
- }
133
- interface TextResponse {
134
- data: string;
135
- response: Response;
144
+ /** Explicit idempotency key for this request. Sent as `Idempotency-Key` header. */
145
+ idempotencyKey?: string;
136
146
  }
137
147
  interface ArcClientConfig extends ClientConfig {
138
148
  toast?: ToastHandler;
@@ -185,4 +195,4 @@ declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: str
185
195
  */
186
196
  declare function createQueryString<T extends Record<string, unknown>>(params?: T): string;
187
197
  //#endregion
188
- export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, BlobResponse, ClientConfig, HttpMethod, TextResponse, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError };
198
+ export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, ClientConfig, HttpMethod, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
package/dist/client.js CHANGED
@@ -55,6 +55,7 @@ let clientConfig = null;
55
55
  * configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL!, authMode: 'cookie' });
56
56
  */
57
57
  function configureClient(config) {
58
+ if (typeof window === "undefined") console.warn("[arc-next] configureClient() 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).");
58
59
  clientConfig = config;
59
60
  }
60
61
  /**
@@ -63,6 +64,10 @@ function configureClient(config) {
63
64
  function getAuthMode() {
64
65
  return clientConfig?.authMode ?? "bearer";
65
66
  }
67
+ /** Whether auto-idempotency is enabled on the global client. */
68
+ function isAutoIdempotency() {
69
+ return clientConfig?.autoIdempotency ?? false;
70
+ }
66
71
  let authConfig = null;
67
72
  /**
68
73
  * Configure auth context for automatic token/orgId injection.
@@ -81,6 +86,7 @@ let authConfig = null;
81
86
  * });
82
87
  */
83
88
  function configureAuth(config) {
89
+ 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).");
84
90
  authConfig = config;
85
91
  }
86
92
  /**
@@ -115,14 +121,19 @@ function createClient(config) {
115
121
  };
116
122
  }
117
123
  async function executeRequest(config, method, endpoint, options = {}) {
118
- const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal } = options;
124
+ const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey } = options;
119
125
  try {
120
126
  let headers = {
121
127
  ...organizationId ? { "x-organization-id": organizationId } : {},
122
128
  ...config.defaultHeaders ?? {}
123
129
  };
124
130
  if (config.internalApiKey) headers["x-internal-api-key"] = config.internalApiKey;
125
- if (token) headers["Authorization"] = `Bearer ${token}`;
131
+ if (token) if (config.authMode === "header") {
132
+ const headerName = authConfig?.headerName ?? "x-api-key";
133
+ headers[headerName] = token;
134
+ } else headers["Authorization"] = `Bearer ${token}`;
135
+ if (config.apiVersion) headers["Accept-Version"] = config.apiVersion;
136
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
126
137
  if (body !== void 0 && body !== null && !(body instanceof FormData)) headers["Content-Type"] = "application/json";
127
138
  if (headerOptions) headers = {
128
139
  ...headers,
@@ -245,4 +256,4 @@ function createQueryString(params = {}) {
245
256
  }
246
257
 
247
258
  //#endregion
248
- export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError };
259
+ export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
package/dist/hooks.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ArcClient, UseRouterHook } from "./client.js";
2
- import { BaseApi } from "./api.js";
3
- import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
2
+ import { BaseApi, FilterOperator } from "./api.js";
4
3
  import { CacheUtils, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, QueryKeys } from "./query.js";
4
+ import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
5
5
  import { QueryKey } from "@tanstack/react-query";
6
6
 
7
7
  //#region src/hooks.d.ts
@@ -15,12 +15,32 @@ import { QueryKey } from "@tanstack/react-query";
15
15
  type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>, 'getAll' | 'getById' | 'create' | 'update' | 'delete'> & {
16
16
  upload?: BaseApi<T, TCreate, TUpdate>['upload'];
17
17
  search?: BaseApi<T, TCreate, TUpdate>['search'];
18
+ getDeleted?: BaseApi<T, TCreate, TUpdate>['getDeleted'];
19
+ restore?: BaseApi<T, TCreate, TUpdate>['restore'];
20
+ bulkCreate?: BaseApi<T, TCreate, TUpdate>['bulkCreate'];
21
+ bulkUpdate?: BaseApi<T, TCreate, TUpdate>['bulkUpdate'];
22
+ bulkDelete?: BaseApi<T, TCreate, TUpdate>['bulkDelete'];
23
+ getBySlug?: BaseApi<T, TCreate, TUpdate>['getBySlug'];
24
+ getTree?: BaseApi<T, TCreate, TUpdate>['getTree'];
25
+ getChildren?: BaseApi<T, TCreate, TUpdate>['getChildren'];
26
+ findBy?: BaseApi<T, TCreate, TUpdate>['findBy'];
18
27
  };
19
28
  interface CrudHooksConfig<T, TCreate = Partial<T>, TUpdate = Partial<T>> {
20
29
  api: CrudApi<T, TCreate, TUpdate>;
21
30
  entityKey: string;
22
31
  singular: string;
23
32
  plural?: string;
33
+ /**
34
+ * Primary key field used to extract item IDs from response data.
35
+ * Used for cache key resolution, optimistic updates, and detail cache prefill.
36
+ *
37
+ * Default lookup order: `_id` → `id`.
38
+ * Set this when your resource uses a custom ID field (e.g., `'sku'`, `'slug'`, `'code'`).
39
+ *
40
+ * @example
41
+ * createCrudHooks({ idField: 'sku', ... }) // GET /products/:sku
42
+ */
43
+ idField?: string;
24
44
  defaults?: {
25
45
  staleTime?: number;
26
46
  gcTime?: number;
@@ -72,11 +92,35 @@ interface CrudActions<T, TCreate, TUpdate> {
72
92
  create: (params: MutationParams<TCreate>, options?: CallOptions<T>) => Promise<T>;
73
93
  update: (params: UpdateParams<TUpdate>, options?: CallOptions<T>) => Promise<T>;
74
94
  remove: (params: DeleteParams, options?: CallOptions) => Promise<unknown>;
95
+ /** Restore a soft-deleted item. Only available when backend has softDelete preset. */
96
+ restore: (params: DeleteParams, options?: CallOptions<T>) => Promise<T>;
75
97
  isCreating: boolean;
76
98
  isUpdating: boolean;
77
99
  isDeleting: boolean;
100
+ isRestoring: boolean;
78
101
  isMutating: boolean;
79
102
  }
103
+ interface BulkActions<T, TCreate> {
104
+ bulkCreate: (params: {
105
+ data: TCreate[];
106
+ token?: string | null;
107
+ organizationId?: string | null;
108
+ }, options?: CallOptions<T[]>) => Promise<T[]>;
109
+ bulkUpdate: (params: {
110
+ filter: Record<string, unknown>;
111
+ data: Partial<T>;
112
+ token?: string | null;
113
+ organizationId?: string | null;
114
+ }, options?: CallOptions) => Promise<unknown>;
115
+ bulkRemove: (params: {
116
+ filter: Record<string, unknown>;
117
+ token?: string | null;
118
+ organizationId?: string | null;
119
+ }, options?: CallOptions) => Promise<unknown>;
120
+ isBulkCreating: boolean;
121
+ isBulkUpdating: boolean;
122
+ isBulkDeleting: boolean;
123
+ }
80
124
  interface NavigationOptions {
81
125
  scroll?: boolean;
82
126
  replace?: boolean;
@@ -98,6 +142,14 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
98
142
  (token: string | null, params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
99
143
  };
100
144
  useActions: () => CrudActions<T, TCreate, TUpdate>;
145
+ useBulkActions: () => BulkActions<T, TCreate>;
146
+ useDeleted: (params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
147
+ useDetailBySlug: (slug: string | null, options?: DetailQueryOptions<T>) => DetailQueryResult<T>;
148
+ useTree: (params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
149
+ useChildren: (parentId: string | null, params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
150
+ useFindBy: (field: string, value: unknown, options?: ListQueryOptions<T> & {
151
+ operator?: FilterOperator;
152
+ }) => ListQueryResult<T>;
101
153
  useUpload: (options?: {
102
154
  invalidateQueries?: QueryKey[];
103
155
  messages?: MutationMessages;
@@ -134,9 +186,11 @@ declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>(
134
186
  api,
135
187
  entityKey,
136
188
  singular,
189
+ plural,
190
+ idField,
137
191
  defaults,
138
192
  callbacks,
139
193
  client
140
194
  }: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
141
195
  //#endregion
142
- export { CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };
196
+ export { BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };