@classytic/arc-next 0.11.1 → 0.13.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/LICENSE +1 -1
- package/README.md +57 -0
- package/dist/api.d.ts +71 -117
- package/dist/api.js +16 -2
- package/dist/cache.d.ts +28 -7
- package/dist/cache.js +52 -1
- package/dist/client.d.ts +126 -13
- package/dist/client.js +159 -24
- package/dist/encryption.d.ts +1 -2
- package/dist/hooks.d.ts +32 -29
- package/dist/hooks.js +99 -27
- package/dist/mutation.d.ts +30 -4
- package/dist/mutation.js +28 -11
- package/dist/prefetch.d.ts +0 -1
- package/dist/presets/bulk.d.ts +0 -1
- package/dist/presets/history.d.ts +1 -2
- package/dist/presets/search.d.ts +17 -8
- package/dist/presets/slug.d.ts +0 -1
- package/dist/presets/soft-delete.d.ts +0 -1
- package/dist/presets/tree.d.ts +0 -1
- package/dist/query-client.d.ts +0 -1
- package/dist/query-options.d.ts +32 -26
- package/dist/query.d.ts +7 -47
- package/dist/sse.d.ts +0 -1
- package/dist/sse.js +4 -3
- package/dist/upload.d.ts +9 -1
- package/dist/upload.js +8 -4
- package/dist/ws.d.ts +1 -2
- package/dist/ws.js +1 -1
- package/package.json +19 -8
package/dist/hooks.d.ts
CHANGED
|
@@ -2,15 +2,14 @@ import { ArcClient, UseRouterHook } from "./client.js";
|
|
|
2
2
|
import { AggResult, AggRow, BaseApi } from "./api.js";
|
|
3
3
|
import { CacheUtils, QueryKeys } from "./cache.js";
|
|
4
4
|
import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
|
|
5
|
-
import { DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, RequestPassthrough } from "./query.js";
|
|
6
|
-
import { SoftDeleteMethods } from "./presets/soft-delete.js";
|
|
7
5
|
import { BulkMethods } from "./presets/bulk.js";
|
|
6
|
+
import { SearchPresetMethods } from "./presets/search.js";
|
|
8
7
|
import { SlugLookupMethods } from "./presets/slug.js";
|
|
8
|
+
import { SoftDeleteMethods } from "./presets/soft-delete.js";
|
|
9
9
|
import { TreeMethods } from "./presets/tree.js";
|
|
10
|
-
import {
|
|
10
|
+
import { DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, RequestPassthrough } from "./query.js";
|
|
11
11
|
import { QueryKey, UseQueryResult } from "@tanstack/react-query";
|
|
12
12
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
13
|
-
|
|
14
13
|
//#region src/hooks.d.ts
|
|
15
14
|
/**
|
|
16
15
|
* CRUD API interface accepted by createCrudHooks.
|
|
@@ -22,11 +21,12 @@ import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
|
22
21
|
* any cast — yet a vanilla `createCrudApi('todos')` instance has none of them
|
|
23
22
|
* in autocomplete unless you opt in.
|
|
24
23
|
*/
|
|
25
|
-
type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>,
|
|
26
|
-
upload?: BaseApi<T, TCreate, TUpdate>[
|
|
27
|
-
dispatchAction?: BaseApi<T, TCreate, TUpdate>[
|
|
28
|
-
invokeRoute?: BaseApi<T, TCreate, TUpdate>[
|
|
29
|
-
|
|
24
|
+
type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>, "getAll" | "getById" | "create" | "update" | "delete" | "count"> & {
|
|
25
|
+
upload?: BaseApi<T, TCreate, TUpdate>["upload"];
|
|
26
|
+
dispatchAction?: BaseApi<T, TCreate, TUpdate>["dispatchAction"];
|
|
27
|
+
invokeRoute?: BaseApi<T, TCreate, TUpdate>["invokeRoute"];
|
|
28
|
+
/** Declared aggregations (arc 2.13+). Always available on BaseApi. */
|
|
29
|
+
aggregate?: BaseApi<T, TCreate, TUpdate>["aggregate"];
|
|
30
30
|
} & Partial<SoftDeleteMethods<T>> & Partial<BulkMethods<T, TCreate, TUpdate>> & Partial<SlugLookupMethods<T>> & Partial<TreeMethods<T>> & Partial<SearchPresetMethods<T>>;
|
|
31
31
|
/** Args + options for `useAggregation`. Mirrors `ListQueryOptions` for DX consistency. */
|
|
32
32
|
interface AggregationQueryOptions<TRow extends AggRow = AggRow, TData = AggResult<TRow>> {
|
|
@@ -182,11 +182,15 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
182
182
|
KEYS: QueryKeys;
|
|
183
183
|
cache: CacheUtils<T>;
|
|
184
184
|
useList: {
|
|
185
|
-
/** New signature — auto-injects token/orgId from configureAuth() context */
|
|
185
|
+
/** New signature — auto-injects token/orgId from configureAuth() context */
|
|
186
|
+
(params?: Record<string, unknown>, options?: ListQueryOptions<T>): ListQueryResult<T>;
|
|
187
|
+
/** Legacy signature — explicit token */
|
|
186
188
|
(token: string | null, params?: Record<string, unknown>, options?: ListQueryOptions<T>): ListQueryResult<T>;
|
|
187
189
|
};
|
|
188
190
|
useDetail: {
|
|
189
|
-
/** New signature — auto-injects token from configureAuth() context */
|
|
191
|
+
/** New signature — auto-injects token from configureAuth() context */
|
|
192
|
+
(id: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>;
|
|
193
|
+
/** Legacy signature — explicit token */
|
|
190
194
|
(id: string | null, token: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>;
|
|
191
195
|
};
|
|
192
196
|
/**
|
|
@@ -202,7 +206,9 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
202
206
|
*/
|
|
203
207
|
useSuspenseDetail: (id: string, options?: Omit<DetailQueryOptions<T>, "enabled">) => DetailQueryResult<T>;
|
|
204
208
|
useInfiniteList: {
|
|
205
|
-
/** New signature — auto-injects token/orgId from configureAuth() context */
|
|
209
|
+
/** New signature — auto-injects token/orgId from configureAuth() context */
|
|
210
|
+
(params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
|
|
211
|
+
/** Legacy signature — explicit token */
|
|
206
212
|
(token: string | null, params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
|
|
207
213
|
};
|
|
208
214
|
useActions: () => CrudActions<T, TCreate, TUpdate>;
|
|
@@ -223,7 +229,8 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
223
229
|
* (approve/cancel/dispatch) instead of bespoke routes.
|
|
224
230
|
*/
|
|
225
231
|
useAction: <TResult = T, TBody extends Record<string, unknown> = Record<string, unknown>>(options?: {
|
|
226
|
-
invalidateQueries?: QueryKey[];
|
|
232
|
+
invalidateQueries?: QueryKey[];
|
|
233
|
+
/** Default action name. Can be overridden per-call via `mutate({ action })`. */
|
|
227
234
|
action?: string;
|
|
228
235
|
messages?: MutationMessages<TResult, {
|
|
229
236
|
id: string;
|
|
@@ -335,15 +342,20 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
335
342
|
* `ssePlugin` (`/events/stream`). Pass `enabled: false` to opt out.
|
|
336
343
|
*/
|
|
337
344
|
useResourceSync: (options?: {
|
|
338
|
-
source?:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
345
|
+
source?: "ws" | "sse";
|
|
346
|
+
/** Override resource name. Defaults to the factory's `entityKey`. */
|
|
347
|
+
resource?: string;
|
|
348
|
+
/** Override path (default: `/ws` or `/events/stream`). */
|
|
349
|
+
path?: string;
|
|
350
|
+
/** Whether the connection is active. Default: true. */
|
|
351
|
+
enabled?: boolean;
|
|
352
|
+
/** Per-event hook fired AFTER cache invalidation. */
|
|
342
353
|
onEvent?: (event: {
|
|
343
|
-
operation:
|
|
354
|
+
operation: "created" | "updated" | "deleted";
|
|
344
355
|
id?: string;
|
|
345
356
|
data: unknown;
|
|
346
|
-
}) => void;
|
|
357
|
+
}) => void;
|
|
358
|
+
/** Connection-state listener. */
|
|
347
359
|
onConnectionChange?: (connected: boolean) => void;
|
|
348
360
|
}) => {
|
|
349
361
|
isConnected: boolean;
|
|
@@ -360,15 +372,6 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
360
372
|
* configureNavigation(useRouter);
|
|
361
373
|
*/
|
|
362
374
|
declare function configureNavigation(hook: UseRouterHook): void;
|
|
363
|
-
declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>({
|
|
364
|
-
api,
|
|
365
|
-
entityKey,
|
|
366
|
-
singular,
|
|
367
|
-
plural,
|
|
368
|
-
idField,
|
|
369
|
-
defaults,
|
|
370
|
-
callbacks,
|
|
371
|
-
client
|
|
372
|
-
}: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
|
|
375
|
+
declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>({ api, entityKey, singular, plural, idField, defaults, callbacks, client }: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
|
|
373
376
|
//#endregion
|
|
374
377
|
export { AggregationQueryOptions, BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };
|
package/dist/hooks.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { getAuthMode, getClientAuthContext, hasGlobalStaticAuth } from "./client.js";
|
|
4
4
|
import { isKeysetPagination, isOffsetPagination } from "./api.js";
|
|
5
|
-
import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, syncDetailToLists, updateListCache, withOrgParams } from "./cache.js";
|
|
5
|
+
import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, prependToListCache, replaceItemInListCache, syncDetailToLists, updateListCache, withOrgParams } from "./cache.js";
|
|
6
6
|
import { findItemInListCache, useDetailQuery, useInfiniteListQuery, useListQuery, useSuspenseDetailQuery, useSuspenseListQuery } from "./query.js";
|
|
7
7
|
import { useMutationWithTransition, useOptimisticMutation } from "./mutation.js";
|
|
8
8
|
import { subscribeToEvents } from "./sse.js";
|
|
@@ -58,6 +58,32 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
58
58
|
}
|
|
59
59
|
const KEYS = createQueryKeys(entityKey);
|
|
60
60
|
const cache = createCacheUtils(KEYS);
|
|
61
|
+
/**
|
|
62
|
+
* Shared identity for this resource's write mutations (create / update /
|
|
63
|
+
* delete). Powers last-standing invalidation in `useOptimisticMutation`:
|
|
64
|
+
* rapid sequential writes trigger ONE settled refetch instead of N racing
|
|
65
|
+
* ones, so an early write's refetch can never clobber a later write's
|
|
66
|
+
* optimistic state.
|
|
67
|
+
*/
|
|
68
|
+
const WRITE_MUTATION_KEY = [entityKey, "write"];
|
|
69
|
+
/**
|
|
70
|
+
* Per-record write ordering. `update`/`remove`/`restore` calls against the
|
|
71
|
+
* SAME record are chained (call order = server application order = cache
|
|
72
|
+
* application order); writes to different records stay fully parallel. A
|
|
73
|
+
* failed write does not block the next one — each link runs regardless of
|
|
74
|
+
* the previous outcome. Factory-scoped so every component using this
|
|
75
|
+
* resource's hooks shares one chain per record.
|
|
76
|
+
*/
|
|
77
|
+
const writeChains = /* @__PURE__ */ new Map();
|
|
78
|
+
function enqueueWrite(recordId, run) {
|
|
79
|
+
const result = (writeChains.get(recordId) ?? Promise.resolve()).then(run, run);
|
|
80
|
+
const tail = result.then(() => void 0, () => void 0);
|
|
81
|
+
writeChains.set(recordId, tail);
|
|
82
|
+
tail.then(() => {
|
|
83
|
+
if (writeChains.get(recordId) === tail) writeChains.delete(recordId);
|
|
84
|
+
});
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
61
87
|
const instanceToast = client?.toast;
|
|
62
88
|
const instanceNavigation = client?.navigation ?? null;
|
|
63
89
|
const resolveAuthMode = () => client?.config?.authMode ?? getAuthMode();
|
|
@@ -149,7 +175,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
149
175
|
const detailResult = useDetailQuery({
|
|
150
176
|
queryKey: fullDetailKey,
|
|
151
177
|
queryFn: ({ signal }) => api.getById({
|
|
152
|
-
id,
|
|
178
|
+
id: id ?? "",
|
|
153
179
|
token,
|
|
154
180
|
organizationId,
|
|
155
181
|
params: queryParams,
|
|
@@ -260,6 +286,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
260
286
|
const queryClient = useQueryClient();
|
|
261
287
|
const silentRef = useRef(false);
|
|
262
288
|
const shouldToast = useCallback(() => !silentRef.current, []);
|
|
289
|
+
const tempIdsRef = useRef(/* @__PURE__ */ new WeakMap());
|
|
263
290
|
const createMutation = useOptimisticMutation({
|
|
264
291
|
mutationFn: ({ token, organizationId, data }) => api.create({
|
|
265
292
|
token,
|
|
@@ -268,14 +295,32 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
268
295
|
}),
|
|
269
296
|
queryClient,
|
|
270
297
|
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
298
|
+
mutationKey: WRITE_MUTATION_KEY,
|
|
271
299
|
shouldToast,
|
|
272
|
-
optimisticUpdate: (oldData,
|
|
273
|
-
|
|
300
|
+
optimisticUpdate: (oldData, variables, qKey) => {
|
|
301
|
+
if (qKey[1] !== "list") return oldData;
|
|
302
|
+
const { data } = variables;
|
|
303
|
+
let tempId = tempIdsRef.current.get(variables);
|
|
304
|
+
if (!tempId) {
|
|
305
|
+
tempId = resolveItemId(data) ?? `temp-${globalThis.crypto?.randomUUID?.() ?? Date.now()}`;
|
|
306
|
+
tempIdsRef.current.set(variables, tempId);
|
|
307
|
+
}
|
|
308
|
+
return prependToListCache(oldData, {
|
|
274
309
|
...data,
|
|
275
310
|
_optimistic: true,
|
|
276
|
-
[idField ?? (resolveItemId(data) ? "id" : "_id")]:
|
|
277
|
-
};
|
|
278
|
-
|
|
311
|
+
[idField ?? (resolveItemId(data) ? "id" : "_id")]: tempId
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
reconcile: (raw, variables) => {
|
|
315
|
+
const serverDoc = extractItem(raw);
|
|
316
|
+
if (!serverDoc || typeof serverDoc !== "object") return;
|
|
317
|
+
const tempId = tempIdsRef.current.get(variables);
|
|
318
|
+
if (tempId) for (const [qKey, qData] of queryClient.getQueriesData({ queryKey: KEYS.lists() })) {
|
|
319
|
+
const next = replaceItemInListCache(qData, tempId, serverDoc, idField ? { idField } : {});
|
|
320
|
+
if (next !== qData) queryClient.setQueryData(qKey, next);
|
|
321
|
+
}
|
|
322
|
+
const realId = resolveItemId(serverDoc);
|
|
323
|
+
if (realId) queryClient.setQueryData(KEYS.detail(realId), serverDoc);
|
|
279
324
|
},
|
|
280
325
|
onSuccess: (raw, variables) => {
|
|
281
326
|
callbacks.onCreate?.onSuccess?.(extractItem(raw), { data: variables.data }, void 0);
|
|
@@ -305,26 +350,32 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
305
350
|
KEYS.details(),
|
|
306
351
|
KEYS.aggregations()
|
|
307
352
|
],
|
|
353
|
+
mutationKey: WRITE_MUTATION_KEY,
|
|
308
354
|
shouldToast,
|
|
309
|
-
optimisticUpdate: (oldData, { id, data }) => {
|
|
310
|
-
|
|
355
|
+
optimisticUpdate: (oldData, { id, data }, qKey) => {
|
|
356
|
+
if (qKey[1] === "detail") {
|
|
357
|
+
if (qKey[2] !== id || !oldData || typeof oldData !== "object") return oldData;
|
|
358
|
+
return {
|
|
359
|
+
...oldData,
|
|
360
|
+
...data
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (qKey[1] !== "list") return oldData;
|
|
364
|
+
return updateListCache(oldData, (arr) => (arr || []).map((item) => resolveItemId(item) === id ? {
|
|
311
365
|
...item,
|
|
312
366
|
...data
|
|
313
367
|
} : item));
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
});
|
|
324
|
-
return updated;
|
|
368
|
+
},
|
|
369
|
+
reconcile: (raw, { id }) => {
|
|
370
|
+
const serverDoc = extractItem(raw);
|
|
371
|
+
if (!serverDoc || typeof serverDoc !== "object") return;
|
|
372
|
+
for (const [qKey, qData] of queryClient.getQueriesData({ queryKey: KEYS.detail(id) })) if (qData) queryClient.setQueryData(qKey, serverDoc);
|
|
373
|
+
for (const [qKey, qData] of queryClient.getQueriesData({ queryKey: KEYS.lists() })) {
|
|
374
|
+
const next = replaceItemInListCache(qData, id, serverDoc, idField ? { idField } : {});
|
|
375
|
+
if (next !== qData) queryClient.setQueryData(qKey, next);
|
|
376
|
+
}
|
|
325
377
|
},
|
|
326
378
|
onSuccess: (raw, { id, data: updateData }) => {
|
|
327
|
-
queryClient.invalidateQueries({ queryKey: KEYS.detail(id) });
|
|
328
379
|
callbacks.onUpdate?.onSuccess?.(extractItem(raw), {
|
|
329
380
|
id,
|
|
330
381
|
data: updateData
|
|
@@ -356,8 +407,10 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
356
407
|
}),
|
|
357
408
|
queryClient,
|
|
358
409
|
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
410
|
+
mutationKey: WRITE_MUTATION_KEY,
|
|
359
411
|
shouldToast,
|
|
360
|
-
optimisticUpdate: (oldData, { id }) => {
|
|
412
|
+
optimisticUpdate: (oldData, { id }, qKey) => {
|
|
413
|
+
if (qKey[1] !== "list") return oldData;
|
|
361
414
|
return updateListCache(oldData, (arr) => (arr || []).filter((item) => resolveItemId(item) !== id));
|
|
362
415
|
},
|
|
363
416
|
onSuccess: (data, { id }) => {
|
|
@@ -424,7 +477,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
424
477
|
update: useCallback(async (params, options) => {
|
|
425
478
|
silentRef.current = options?.silent ?? false;
|
|
426
479
|
try {
|
|
427
|
-
const entity = extractItem(await updateMutation.mutateAsync(resolveActionAuth(params)));
|
|
480
|
+
const entity = extractItem(await enqueueWrite(params.id, () => updateMutation.mutateAsync(resolveActionAuth(params))));
|
|
428
481
|
options?.onSuccess?.(entity);
|
|
429
482
|
options?.onSettled?.(entity, null);
|
|
430
483
|
return entity;
|
|
@@ -439,7 +492,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
439
492
|
remove: useCallback(async (params, options) => {
|
|
440
493
|
silentRef.current = options?.silent ?? false;
|
|
441
494
|
try {
|
|
442
|
-
const result = await deleteMutation.mutateAsync(resolveActionAuth(params));
|
|
495
|
+
const result = await enqueueWrite(params.id, () => deleteMutation.mutateAsync(resolveActionAuth(params)));
|
|
443
496
|
options?.onSuccess?.(result);
|
|
444
497
|
options?.onSettled?.(result, null);
|
|
445
498
|
return result;
|
|
@@ -454,7 +507,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
454
507
|
restore: useCallback(async (params, options) => {
|
|
455
508
|
silentRef.current = options?.silent ?? false;
|
|
456
509
|
try {
|
|
457
|
-
const entity = extractItem(await restoreMutation.mutateAsync(resolveActionAuth(params)));
|
|
510
|
+
const entity = extractItem(await enqueueWrite(params.id, () => restoreMutation.mutateAsync(resolveActionAuth(params))));
|
|
458
511
|
options?.onSuccess?.(entity);
|
|
459
512
|
options?.onSettled?.(entity, null);
|
|
460
513
|
return entity;
|
|
@@ -631,7 +684,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
631
684
|
queryFn: ({ signal }) => {
|
|
632
685
|
if (!api.getBySlug) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getBySlug method`));
|
|
633
686
|
return api.getBySlug({
|
|
634
|
-
slug,
|
|
687
|
+
slug: slug ?? "",
|
|
635
688
|
token,
|
|
636
689
|
organizationId,
|
|
637
690
|
params: queryParams,
|
|
@@ -704,7 +757,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
704
757
|
return api.getChildren({
|
|
705
758
|
token,
|
|
706
759
|
organizationId,
|
|
707
|
-
parentId,
|
|
760
|
+
parentId: parentId ?? "",
|
|
708
761
|
params: restParams,
|
|
709
762
|
options: {
|
|
710
763
|
signal,
|
|
@@ -721,6 +774,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
721
774
|
});
|
|
722
775
|
}
|
|
723
776
|
function useBulkActions() {
|
|
777
|
+
const queryClient = useQueryClient();
|
|
724
778
|
const bulkCreateMutation = useMutationWithTransition({
|
|
725
779
|
mutationFn: (vars) => {
|
|
726
780
|
if (!api.bulkCreate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a bulkCreate method`));
|
|
@@ -732,6 +786,14 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
732
786
|
});
|
|
733
787
|
},
|
|
734
788
|
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
789
|
+
onSuccess: (raw) => {
|
|
790
|
+
const created = raw?.data;
|
|
791
|
+
if (!Array.isArray(created)) return;
|
|
792
|
+
for (const doc of created) {
|
|
793
|
+
const id = resolveItemId(doc);
|
|
794
|
+
if (id) queryClient.setQueryData(KEYS.detail(id), doc);
|
|
795
|
+
}
|
|
796
|
+
},
|
|
735
797
|
messages: {
|
|
736
798
|
success: `${pluralName} created successfully`,
|
|
737
799
|
error: `Failed to create ${pluralName.toLowerCase()}`
|
|
@@ -754,6 +816,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
754
816
|
KEYS.details(),
|
|
755
817
|
KEYS.aggregations()
|
|
756
818
|
],
|
|
819
|
+
shouldInvalidate: (raw) => {
|
|
820
|
+
const r = raw;
|
|
821
|
+
if (!r || typeof r.modifiedCount !== "number") return true;
|
|
822
|
+
return r.modifiedCount > 0 || (r.upsertedCount ?? 0) > 0;
|
|
823
|
+
},
|
|
757
824
|
messages: {
|
|
758
825
|
success: `${pluralName} updated successfully`,
|
|
759
826
|
error: `Failed to update ${pluralName.toLowerCase()}`
|
|
@@ -771,6 +838,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
771
838
|
});
|
|
772
839
|
},
|
|
773
840
|
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
841
|
+
shouldInvalidate: (raw) => {
|
|
842
|
+
const r = raw;
|
|
843
|
+
if (!r || typeof r.deletedCount !== "number") return true;
|
|
844
|
+
return r.deletedCount > 0;
|
|
845
|
+
},
|
|
774
846
|
messages: {
|
|
775
847
|
success: `${pluralName} deleted successfully`,
|
|
776
848
|
error: `Failed to delete ${pluralName.toLowerCase()}`
|
package/dist/mutation.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { ToastHandler } from "./client.js";
|
|
2
|
-
import * as _$_tanstack_react_query0 from "@tanstack/react-query";
|
|
3
2
|
import { QueryClient, QueryKey, UseMutateAsyncFunction, UseMutateFunction } from "@tanstack/react-query";
|
|
4
|
-
|
|
5
3
|
//#region src/mutation.d.ts
|
|
6
4
|
/**
|
|
7
5
|
* Toast copy for a mutation. Generic over the mutation's result/variables so
|
|
@@ -60,6 +58,13 @@ declare function getToastHandler(): ToastHandler;
|
|
|
60
58
|
interface TransitionMutationConfig<TData, TVariables> {
|
|
61
59
|
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
62
60
|
invalidateQueries?: QueryKey[];
|
|
61
|
+
/**
|
|
62
|
+
* Result-aware invalidation gate. When provided, `invalidateQueries` only
|
|
63
|
+
* fire if this returns true for the mutation result — lets bulk operations
|
|
64
|
+
* skip refetching everything when the server reports nothing changed
|
|
65
|
+
* (`modifiedCount: 0`, `deletedCount: 0`).
|
|
66
|
+
*/
|
|
67
|
+
shouldInvalidate?: (data: TData) => boolean;
|
|
63
68
|
onSuccess?: (data: TData, variables: TVariables) => void;
|
|
64
69
|
onError?: (error: Error, variables: TVariables) => void;
|
|
65
70
|
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
|
|
@@ -115,7 +120,28 @@ interface CreateOptimisticMutationConfig<TData, TVariables> {
|
|
|
115
120
|
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
116
121
|
queryClient: QueryClient;
|
|
117
122
|
queryKeys: QueryKey[];
|
|
118
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Per-cache-entry optimistic updater. Receives the concrete query key of
|
|
125
|
+
* the entry being updated so a single updater can treat list, detail, and
|
|
126
|
+
* aggregation caches differently (merge the doc into `[e,'detail',id]`,
|
|
127
|
+
* map items inside `[e,'list',…]`, leave `[e,'aggregation',…]` untouched).
|
|
128
|
+
* Return the input unchanged to skip the write for that entry.
|
|
129
|
+
*/
|
|
130
|
+
optimisticUpdate?: (oldData: unknown, variables: TVariables, queryKey: QueryKey) => unknown;
|
|
131
|
+
/**
|
|
132
|
+
* Shared identity for this resource's write mutations. When set, settled
|
|
133
|
+
* invalidation only fires from the LAST pending mutation carrying the same
|
|
134
|
+
* key (`isMutating === 1`) — rapid sequential writes produce one refetch
|
|
135
|
+
* at the end instead of N racing refetches, and a refetch triggered by
|
|
136
|
+
* write #1 can never clobber write #2's optimistic state.
|
|
137
|
+
*/
|
|
138
|
+
mutationKey?: readonly unknown[];
|
|
139
|
+
/**
|
|
140
|
+
* Post-success cache reconciliation, run BEFORE any invalidation while the
|
|
141
|
+
* optimistic state is still in place. Use to swap temp IDs for server
|
|
142
|
+
* documents or seed detail caches from the response.
|
|
143
|
+
*/
|
|
144
|
+
reconcile?: (data: TData, variables: TVariables) => void;
|
|
119
145
|
onSuccess?: (data: TData, variables: TVariables) => void;
|
|
120
146
|
onError?: (error: Error, variables: TVariables) => void;
|
|
121
147
|
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
|
|
@@ -124,7 +150,7 @@ interface CreateOptimisticMutationConfig<TData, TVariables> {
|
|
|
124
150
|
shouldToast?: () => boolean;
|
|
125
151
|
toastHandler?: ToastHandler;
|
|
126
152
|
}
|
|
127
|
-
declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimisticMutationConfig<TData, TVariables>):
|
|
153
|
+
declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimisticMutationConfig<TData, TVariables>): import("@tanstack/react-query").UseMutationResult<TData, Error, TVariables, {
|
|
128
154
|
previous: {
|
|
129
155
|
key: readonly unknown[];
|
|
130
156
|
data: [readonly unknown[], unknown][];
|
package/dist/mutation.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { getQuotaDetails, isArcApiError, isAutoIdempotency } from "./client.js";
|
|
4
4
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
|
-
import {
|
|
5
|
+
import { useRef, useTransition } from "react";
|
|
6
6
|
|
|
7
7
|
//#region src/mutation.ts
|
|
8
8
|
let toastHandler = {
|
|
9
|
-
success: (
|
|
10
|
-
error: (
|
|
9
|
+
success: () => {},
|
|
10
|
+
error: () => {}
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
13
|
* Configure toast handler. Call once at app init.
|
|
@@ -78,9 +78,11 @@ function useMutationWithTransition(config) {
|
|
|
78
78
|
},
|
|
79
79
|
onSuccess: (data, variables) => {
|
|
80
80
|
const invalidate = () => {
|
|
81
|
-
invalidateQueries.forEach((key) =>
|
|
81
|
+
invalidateQueries.forEach((key) => {
|
|
82
|
+
queryClient.invalidateQueries({ queryKey: key });
|
|
83
|
+
});
|
|
82
84
|
};
|
|
83
|
-
if (
|
|
85
|
+
if (invalidateQueries.length > 0 && (config.shouldInvalidate?.(data) ?? true)) if (withTransition) startTransition(invalidate);
|
|
84
86
|
else invalidate();
|
|
85
87
|
if (toast && (config.shouldToast?.() ?? true)) showToast("success", messages, data, variables, void 0, instanceToast);
|
|
86
88
|
onSuccess?.(data, variables);
|
|
@@ -121,12 +123,16 @@ function useMutationWithOptimistic(config) {
|
|
|
121
123
|
return { previous };
|
|
122
124
|
},
|
|
123
125
|
onSuccess: (data, variables) => {
|
|
124
|
-
queryKeys.forEach((key) =>
|
|
126
|
+
queryKeys.forEach((key) => {
|
|
127
|
+
queryClient.invalidateQueries({ queryKey: key });
|
|
128
|
+
});
|
|
125
129
|
if (toast) showToast("success", messages, data, variables, void 0, instanceToast);
|
|
126
130
|
onSuccess?.(data, variables);
|
|
127
131
|
},
|
|
128
132
|
onError: (error, variables, context) => {
|
|
129
|
-
context?.previous?.forEach(({ key, data }) =>
|
|
133
|
+
context?.previous?.forEach(({ key, data }) => {
|
|
134
|
+
queryClient.setQueryData(key, data);
|
|
135
|
+
});
|
|
130
136
|
if (toast) showToast("error", messages, null, variables, error, instanceToast);
|
|
131
137
|
onError?.(error, variables);
|
|
132
138
|
},
|
|
@@ -146,9 +152,15 @@ function useMutationWithOptimistic(config) {
|
|
|
146
152
|
};
|
|
147
153
|
}
|
|
148
154
|
function useOptimisticMutation(config) {
|
|
149
|
-
const { mutationFn, queryClient, queryKeys, optimisticUpdate, onSuccess, onError, onSettled, messages, toastHandler: instanceToast } = config;
|
|
155
|
+
const { mutationFn, queryClient, queryKeys, optimisticUpdate, mutationKey, reconcile, onSuccess, onError, onSettled, messages, toastHandler: instanceToast } = config;
|
|
156
|
+
const invalidateAll = () => {
|
|
157
|
+
queryKeys.forEach((key) => {
|
|
158
|
+
queryClient.invalidateQueries({ queryKey: key });
|
|
159
|
+
});
|
|
160
|
+
};
|
|
150
161
|
return useMutation({
|
|
151
162
|
mutationFn,
|
|
163
|
+
...mutationKey ? { mutationKey } : {},
|
|
152
164
|
onMutate: async (variables) => {
|
|
153
165
|
await Promise.all(queryKeys.map((key) => queryClient.cancelQueries({
|
|
154
166
|
queryKey: key,
|
|
@@ -160,24 +172,29 @@ function useOptimisticMutation(config) {
|
|
|
160
172
|
}));
|
|
161
173
|
if (optimisticUpdate) queryKeys.forEach((key) => {
|
|
162
174
|
queryClient.getQueriesData({ queryKey: key }).forEach(([qKey, qData]) => {
|
|
163
|
-
|
|
175
|
+
const next = optimisticUpdate(qData, variables, qKey);
|
|
176
|
+
if (next !== qData) queryClient.setQueryData(qKey, next);
|
|
164
177
|
});
|
|
165
178
|
});
|
|
166
179
|
return { previous };
|
|
167
180
|
},
|
|
168
181
|
onSuccess: (data, variables) => {
|
|
169
182
|
if ((config.shouldToast?.() ?? true) && messages?.success) showToast("success", messages, data, variables, void 0, instanceToast);
|
|
170
|
-
|
|
183
|
+
reconcile?.(data, variables);
|
|
184
|
+
if (!mutationKey) invalidateAll();
|
|
171
185
|
onSuccess?.(data, variables);
|
|
172
186
|
},
|
|
173
187
|
onError: (error, variables, context) => {
|
|
174
188
|
context?.previous?.forEach(({ data }) => {
|
|
175
|
-
data.forEach(([qKey, qData]) =>
|
|
189
|
+
data.forEach(([qKey, qData]) => {
|
|
190
|
+
queryClient.setQueryData(qKey, qData);
|
|
191
|
+
});
|
|
176
192
|
});
|
|
177
193
|
if (config.shouldToast?.() ?? true) showToast("error", messages, null, variables, error, instanceToast);
|
|
178
194
|
onError?.(error, variables);
|
|
179
195
|
},
|
|
180
196
|
onSettled: (data, error, variables) => {
|
|
197
|
+
if (mutationKey && queryClient.isMutating({ mutationKey }) === 1) invalidateAll();
|
|
181
198
|
onSettled?.(data, error, variables);
|
|
182
199
|
}
|
|
183
200
|
});
|
package/dist/prefetch.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { EntityReadApi } from "./query-options.js";
|
|
2
2
|
import { HydrationBoundary, InfiniteData, QueryClient, dehydrate } from "@tanstack/react-query";
|
|
3
|
-
|
|
4
3
|
//#region src/prefetch.d.ts
|
|
5
4
|
interface PrefetchAuthContext {
|
|
6
5
|
/** Auth token for protected endpoints. Required for bearer/header auth on server. */
|
package/dist/presets/bulk.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AnyBaseApi, CreateOf, DocOf, ScopedArgs, UpdateOf } from "../api.js";
|
|
2
2
|
import { BulkCreateResult, DeleteManyResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
3
|
-
|
|
4
3
|
//#region src/presets/bulk.d.ts
|
|
5
4
|
interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
|
|
6
5
|
/** Insert many docs in one round-trip. Backend mounts `POST /:resource/bulk`. */
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { AnyBaseApi, ScopedArgs } from "../api.js";
|
|
2
|
-
|
|
3
2
|
//#region src/presets/history.d.ts
|
|
4
3
|
/** One audit-trail entry — arc's `AuditEntry` wire shape for a single record. */
|
|
5
4
|
interface HistoryEntry {
|
|
6
5
|
id: string;
|
|
7
6
|
resource: string;
|
|
8
7
|
documentId: string;
|
|
9
|
-
action:
|
|
8
|
+
action: "create" | "update" | "delete" | "restore" | "custom";
|
|
10
9
|
userId?: string;
|
|
11
10
|
organizationId?: string;
|
|
12
11
|
before?: Record<string, unknown>;
|
package/dist/presets/search.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AnyBaseApi, DocOf, ScopedArgs } from "../api.js";
|
|
2
2
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
|
-
|
|
4
3
|
//#region src/presets/search.d.ts
|
|
5
4
|
interface SearchPresetMethods<TDoc> {
|
|
6
5
|
/**
|
|
@@ -8,8 +7,11 @@ interface SearchPresetMethods<TDoc> {
|
|
|
8
7
|
* Backend mounts `POST /:resource/search` via `searchPreset()`.
|
|
9
8
|
*/
|
|
10
9
|
searchEngine<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
|
|
11
|
-
/** Free-text query forwarded as `body.query`. */
|
|
12
|
-
|
|
10
|
+
/** Free-text query forwarded as `body.query`. */
|
|
11
|
+
query?: string;
|
|
12
|
+
/** Engine-specific options merged into the request body. */
|
|
13
|
+
body?: TBody;
|
|
14
|
+
/** Override path (default `/search`). */
|
|
13
15
|
path?: string;
|
|
14
16
|
}): Promise<TResult[] | PaginatedResult<TResult>>;
|
|
15
17
|
/**
|
|
@@ -17,9 +19,13 @@ interface SearchPresetMethods<TDoc> {
|
|
|
17
19
|
* Backend mounts `POST /:resource/search-similar`.
|
|
18
20
|
*/
|
|
19
21
|
searchSimilar<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
|
|
20
|
-
/** Text query — backend embeds and searches for nearest neighbors. */
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/** Text query — backend embeds and searches for nearest neighbors. */
|
|
23
|
+
query?: string;
|
|
24
|
+
/** Pre-computed embedding vector — used directly for similarity search. */
|
|
25
|
+
vector?: number[];
|
|
26
|
+
/** Vector-engine options (`topK`, `filter`, `index`, ...). */
|
|
27
|
+
body?: TBody;
|
|
28
|
+
/** Override path (default `/search-similar`). */
|
|
23
29
|
path?: string;
|
|
24
30
|
}): Promise<TResult[]>;
|
|
25
31
|
/**
|
|
@@ -27,8 +33,11 @@ interface SearchPresetMethods<TDoc> {
|
|
|
27
33
|
* Backend mounts `POST /:resource/embed`.
|
|
28
34
|
*/
|
|
29
35
|
embed(args: ScopedArgs & {
|
|
30
|
-
/** Text or array of texts to embed. */
|
|
31
|
-
|
|
36
|
+
/** Text or array of texts to embed. */
|
|
37
|
+
input: string | string[];
|
|
38
|
+
/** Embed-engine options (`model`, `dimensions`, ...). */
|
|
39
|
+
body?: Record<string, unknown>;
|
|
40
|
+
/** Override path (default `/embed`). */
|
|
32
41
|
path?: string;
|
|
33
42
|
}): Promise<number[] | number[][]>;
|
|
34
43
|
}
|
package/dist/presets/slug.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AnyBaseApi, DocOf, QueryParams, ScopedArgs } from "../api.js";
|
|
2
2
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
|
-
|
|
4
3
|
//#region src/presets/soft-delete.d.ts
|
|
5
4
|
interface SoftDeleteMethods<TDoc> {
|
|
6
5
|
/** List soft-deleted docs. Backend mounts `GET /:resource/deleted`. */
|
package/dist/presets/tree.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AnyBaseApi, DocOf, QueryParams, ScopedArgs } from "../api.js";
|
|
2
2
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
|
-
|
|
4
3
|
//#region src/presets/tree.d.ts
|
|
5
4
|
interface TreeMethods<TDoc> {
|
|
6
5
|
/** Fetch the full hierarchy. Backend mounts `GET /:resource/tree`. */
|