@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 +74 -13
- package/dist/api.d.ts +119 -2
- package/dist/api.js +104 -0
- package/dist/client.d.ts +21 -11
- package/dist/client.js +14 -3
- package/dist/hooks.d.ts +57 -3
- package/dist/hooks.js +316 -20
- package/dist/mutation.d.ts +5 -19
- package/dist/mutation.js +14 -12
- package/dist/prefetch.d.ts +31 -0
- package/dist/prefetch.js +50 -19
- package/dist/query.d.ts +30 -2
- package/dist/query.js +16 -4
- package/dist/sse.d.ts +62 -0
- package/dist/sse.js +144 -0
- package/package.json +22 -13
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",
|
|
34
|
-
//
|
|
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' | '
|
|
153
|
+
authMode?: 'bearer' | 'cookie' | 'header'; // Default: 'bearer'
|
|
152
154
|
credentials?: RequestCredentials; // Default: derived from authMode
|
|
153
|
-
internalApiKey?: string; //
|
|
154
|
-
defaultHeaders?: Record<string, string>; //
|
|
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
|
|
159
|
-
- `authMode: 'cookie'` — HTTP-only cookies (e.g. Better Auth); queries always enabled
|
|
160
|
-
- `
|
|
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.
|
|
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,
|
|
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 &
|
|
717
|
-
- **
|
|
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
|
-
|
|
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,
|
|
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)
|
|
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 };
|