@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/dist/hooks.js CHANGED
@@ -4,8 +4,8 @@ import { getAuthContext, getAuthMode } from "./client.js";
4
4
  import { isKeysetPagination, isOffsetPagination } from "./api.js";
5
5
  import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, updateListCache, useDetailQuery, useInfiniteListQuery, useListQuery } from "./query.js";
6
6
  import { useMutationWithTransition, useOptimisticMutation } from "./mutation.js";
7
- import { useMutation, useQueryClient } from "@tanstack/react-query";
8
- import { useCallback, useMemo, useRef } from "react";
7
+ import { useQueryClient } from "@tanstack/react-query";
8
+ import { useCallback, useRef } from "react";
9
9
 
10
10
  //#region src/hooks.ts
11
11
  let useRouterHook = null;
@@ -25,7 +25,18 @@ function createEnabledRule(token, options, authMode = getAuthMode()) {
25
25
  if (authMode === "cookie" || options.public) return options.enabled ?? true;
26
26
  return options.enabled !== void 0 ? options.enabled && !!token : !!token;
27
27
  }
28
- function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks = {}, client }) {
28
+ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults = {}, callbacks = {}, client }) {
29
+ const pluralName = plural ?? `${singular}s`;
30
+ /** Extract ID from an item using configured idField, falling back to _id → id */
31
+ function resolveItemId(item) {
32
+ if (!item || typeof item !== "object") return null;
33
+ const obj = item;
34
+ if (idField) {
35
+ const val = obj[idField];
36
+ if (val != null) return String(val);
37
+ }
38
+ return getItemId(item);
39
+ }
29
40
  const KEYS = createQueryKeys(entityKey);
30
41
  const cache = createCacheUtils(KEYS);
31
42
  const instanceToast = client?.toast;
@@ -91,6 +102,7 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
91
102
  },
92
103
  prefillDetailCache: queryOpts.prefillDetailCache ?? true,
93
104
  detailKeyBuilder: (id) => KEYS.detail(id),
105
+ itemIdResolver: resolveItemId,
94
106
  select: queryOpts.select
95
107
  });
96
108
  }
@@ -151,7 +163,7 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
151
163
  const optimisticItem = {
152
164
  ...data,
153
165
  _optimistic: true,
154
- [getItemId(data) ? "id" : "_id"]: getItemId(data) ?? `temp-${Date.now()}`
166
+ [idField ?? (resolveItemId(data) ? "id" : "_id")]: resolveItemId(data) ?? `temp-${Date.now()}`
155
167
  };
156
168
  return updateListCache(oldData, (arr) => [optimisticItem, ...arr || []]);
157
169
  },
@@ -181,7 +193,7 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
181
193
  queryKeys: [KEYS.lists(), KEYS.details()],
182
194
  shouldToast,
183
195
  optimisticUpdate: (oldData, { id, data }) => {
184
- const updated = updateListCache(oldData, (arr) => (arr || []).map((item) => getItemId(item) === id ? {
196
+ const updated = updateListCache(oldData, (arr) => (arr || []).map((item) => resolveItemId(item) === id ? {
185
197
  ...item,
186
198
  ...data
187
199
  } : item));
@@ -229,7 +241,7 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
229
241
  queryKeys: [KEYS.lists()],
230
242
  shouldToast,
231
243
  optimisticUpdate: (oldData, { id }) => {
232
- return updateListCache(oldData, (arr) => (arr || []).filter((item) => getItemId(item) !== id));
244
+ return updateListCache(oldData, (arr) => (arr || []).filter((item) => resolveItemId(item) !== id));
233
245
  },
234
246
  onSuccess: (data, { id }) => {
235
247
  queryClient.removeQueries({ queryKey: KEYS.detail(id) });
@@ -247,6 +259,23 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
247
259
  },
248
260
  toastHandler: instanceToast
249
261
  });
262
+ const restoreMutation = useMutationWithTransition({
263
+ mutationFn: ({ token, organizationId, id }) => {
264
+ if (!api.restore) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a restore method`));
265
+ return api.restore({
266
+ token,
267
+ organizationId,
268
+ id
269
+ });
270
+ },
271
+ invalidateQueries: [KEYS.lists(), KEYS.custom("deleted")],
272
+ shouldToast,
273
+ messages: {
274
+ success: `${singular} restored successfully`,
275
+ error: `Failed to restore ${singular.toLowerCase()}`
276
+ },
277
+ toastHandler: instanceToast
278
+ });
250
279
  const resolveAuth = useCallback((params) => {
251
280
  const auth = getAuthContext();
252
281
  return {
@@ -301,10 +330,26 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
301
330
  silentRef.current = false;
302
331
  }
303
332
  }, [deleteMutation, resolveAuth]),
333
+ restore: useCallback(async (params, options) => {
334
+ silentRef.current = options?.silent ?? false;
335
+ try {
336
+ const entity = extractItem(await restoreMutation.mutateAsync(resolveAuth(params)));
337
+ options?.onSuccess?.(entity);
338
+ options?.onSettled?.(entity, null);
339
+ return entity;
340
+ } catch (error) {
341
+ options?.onError?.(error);
342
+ options?.onSettled?.(void 0, error);
343
+ throw error;
344
+ } finally {
345
+ silentRef.current = false;
346
+ }
347
+ }, [restoreMutation, resolveAuth]),
304
348
  isCreating: createMutation.isPending,
305
349
  isUpdating: updateMutation.isPending,
306
350
  isDeleting: deleteMutation.isPending,
307
- isMutating: createMutation.isPending || updateMutation.isPending || deleteMutation.isPending
351
+ isRestoring: restoreMutation.isPending,
352
+ isMutating: createMutation.isPending || updateMutation.isPending || deleteMutation.isPending || restoreMutation.isPending
308
353
  };
309
354
  }
310
355
  function useInfiniteList(tokenOrParams, paramsOrOptions, maybeOptions) {
@@ -357,6 +402,13 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
357
402
  const p = page;
358
403
  if (typeof p.hasNext === "boolean" && typeof p.page === "number") return p.hasNext ? p.page + 1 : void 0;
359
404
  },
405
+ getPreviousPageParam: queryOpts.maxPages != null ? (firstPage) => {
406
+ const page = firstPage;
407
+ if (isOffsetPagination(page)) return page.hasPrev ? page.page - 1 : void 0;
408
+ const p = page;
409
+ if (typeof p.hasPrev === "boolean" && typeof p.page === "number") return p.hasPrev ? p.page - 1 : void 0;
410
+ } : void 0,
411
+ maxPages: queryOpts.maxPages,
360
412
  options: {
361
413
  staleTime: queryOpts.staleTime ?? config.staleTime,
362
414
  gcTime: queryOpts.gcTime ?? config.gcTime,
@@ -392,8 +444,6 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
392
444
  });
393
445
  }
394
446
  function useSearch(query, params, options) {
395
- if (!api.search) throw new Error(`[arc-next] "${entityKey}" api does not define a search method`);
396
- const searchApi = api.search;
397
447
  const auth = getAuthContext();
398
448
  const token = params?.token ?? auth.token;
399
449
  const organizationId = params?.organizationId ?? auth.organizationId;
@@ -413,16 +463,19 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
413
463
  "search",
414
464
  searchKeyParams
415
465
  ],
416
- queryFn: ({ signal }) => searchApi({
417
- token,
418
- organizationId,
419
- params: searchParams,
420
- options: {
421
- signal,
422
- ...requestOpts
423
- }
424
- }),
425
- enabled: query.length > 0 && createEnabledRule(token, queryOpts, resolveAuthMode()),
466
+ queryFn: ({ signal }) => {
467
+ if (!api.search) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a search method`));
468
+ return api.search({
469
+ token,
470
+ organizationId,
471
+ params: searchParams,
472
+ options: {
473
+ signal,
474
+ ...requestOpts
475
+ }
476
+ });
477
+ },
478
+ enabled: !!api.search && query.length > 0 && createEnabledRule(token, queryOpts, resolveAuthMode()),
426
479
  options: {
427
480
  staleTime: queryOpts.staleTime ?? config.staleTime,
428
481
  gcTime: queryOpts.gcTime ?? config.gcTime
@@ -441,6 +494,243 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
441
494
  toastHandler: instanceToast
442
495
  });
443
496
  }
497
+ function useDeleted(params, options) {
498
+ const auth = getAuthContext();
499
+ const token = auth.token;
500
+ const mergedParams = params ?? {};
501
+ const organizationId = mergedParams.organizationId ?? auth.organizationId;
502
+ const { organizationId: _, ...restParams } = mergedParams;
503
+ const { request: requestOpts, ...queryOpts } = options ?? {};
504
+ return useListQuery({
505
+ queryKey: KEYS.custom("deleted", {
506
+ organizationId,
507
+ ...restParams
508
+ }),
509
+ queryFn: ({ signal }) => {
510
+ if (!api.getDeleted) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getDeleted method`));
511
+ return api.getDeleted({
512
+ token,
513
+ organizationId,
514
+ params: restParams,
515
+ options: {
516
+ signal,
517
+ ...requestOpts
518
+ }
519
+ });
520
+ },
521
+ enabled: !!api.getDeleted && createEnabledRule(token, queryOpts, resolveAuthMode()),
522
+ options: {
523
+ staleTime: queryOpts.staleTime ?? config.staleTime,
524
+ gcTime: queryOpts.gcTime ?? config.gcTime
525
+ },
526
+ select: queryOpts.select
527
+ });
528
+ }
529
+ function useDetailBySlug(slug, options) {
530
+ const auth = getAuthContext();
531
+ const token = auth.token;
532
+ const resolvedOptions = options ?? {};
533
+ const organizationId = resolvedOptions.organizationId ?? auth.organizationId;
534
+ const { params: queryParams, request: requestOpts, ...restOptions } = resolvedOptions;
535
+ return useDetailQuery({
536
+ queryKey: queryParams ? KEYS.custom("slug", slug, queryParams) : KEYS.custom("slug", slug),
537
+ queryFn: ({ signal }) => {
538
+ if (!api.getBySlug) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getBySlug method`));
539
+ return api.getBySlug({
540
+ slug,
541
+ token,
542
+ organizationId,
543
+ params: queryParams,
544
+ options: {
545
+ signal,
546
+ ...requestOpts
547
+ }
548
+ });
549
+ },
550
+ enabled: !!api.getBySlug && !!slug && createEnabledRule(token, restOptions, resolveAuthMode()),
551
+ options: {
552
+ staleTime: restOptions.staleTime ?? config.staleTime,
553
+ gcTime: restOptions.gcTime ?? config.gcTime,
554
+ refetchOnWindowFocus: restOptions.refetchOnWindowFocus ?? config.refetchOnWindowFocus,
555
+ structuralSharing: restOptions.structuralSharing ?? config.structuralSharing
556
+ },
557
+ select: restOptions.select
558
+ });
559
+ }
560
+ function useTree(params, options) {
561
+ const auth = getAuthContext();
562
+ const token = auth.token;
563
+ const mergedParams = params ?? {};
564
+ const organizationId = mergedParams.organizationId ?? auth.organizationId;
565
+ const { organizationId: _, ...restParams } = mergedParams;
566
+ const { request: requestOpts, ...queryOpts } = options ?? {};
567
+ return useListQuery({
568
+ queryKey: KEYS.custom("tree", {
569
+ organizationId,
570
+ ...restParams
571
+ }),
572
+ queryFn: ({ signal }) => {
573
+ if (!api.getTree) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getTree method`));
574
+ return api.getTree({
575
+ token,
576
+ organizationId,
577
+ params: restParams,
578
+ options: {
579
+ signal,
580
+ ...requestOpts
581
+ }
582
+ });
583
+ },
584
+ enabled: !!api.getTree && createEnabledRule(token, queryOpts, resolveAuthMode()),
585
+ options: {
586
+ staleTime: queryOpts.staleTime ?? config.staleTime,
587
+ gcTime: queryOpts.gcTime ?? config.gcTime
588
+ },
589
+ select: queryOpts.select
590
+ });
591
+ }
592
+ function useChildren(parentId, params, options) {
593
+ const auth = getAuthContext();
594
+ const token = auth.token;
595
+ const mergedParams = params ?? {};
596
+ const organizationId = mergedParams.organizationId ?? auth.organizationId;
597
+ const { organizationId: _, ...restParams } = mergedParams;
598
+ const { request: requestOpts, ...queryOpts } = options ?? {};
599
+ return useListQuery({
600
+ queryKey: KEYS.custom("children", parentId, {
601
+ organizationId,
602
+ ...restParams
603
+ }),
604
+ queryFn: ({ signal }) => {
605
+ if (!api.getChildren) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getChildren method`));
606
+ return api.getChildren({
607
+ token,
608
+ organizationId,
609
+ parentId,
610
+ params: restParams,
611
+ options: {
612
+ signal,
613
+ ...requestOpts
614
+ }
615
+ });
616
+ },
617
+ enabled: !!api.getChildren && !!parentId && createEnabledRule(token, queryOpts, resolveAuthMode()),
618
+ options: {
619
+ staleTime: queryOpts.staleTime ?? config.staleTime,
620
+ gcTime: queryOpts.gcTime ?? config.gcTime
621
+ },
622
+ prefillDetailCache: queryOpts.prefillDetailCache ?? true,
623
+ detailKeyBuilder: (id) => KEYS.detail(id),
624
+ itemIdResolver: resolveItemId,
625
+ select: queryOpts.select
626
+ });
627
+ }
628
+ function useFindBy(field, value, options) {
629
+ const auth = getAuthContext();
630
+ const token = auth.token;
631
+ const organizationId = auth.organizationId;
632
+ const { operator, request: requestOpts, ...queryOpts } = options ?? {};
633
+ return useListQuery({
634
+ queryKey: KEYS.custom("findBy", field, value, operator),
635
+ queryFn: ({ signal }) => {
636
+ if (!api.findBy) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a findBy method`));
637
+ return api.findBy({
638
+ token,
639
+ organizationId,
640
+ field,
641
+ value,
642
+ operator,
643
+ options: {
644
+ signal,
645
+ ...requestOpts
646
+ }
647
+ });
648
+ },
649
+ enabled: !!api.findBy && value !== void 0 && value !== null && createEnabledRule(token, queryOpts, resolveAuthMode()),
650
+ options: {
651
+ staleTime: queryOpts.staleTime ?? config.staleTime,
652
+ gcTime: queryOpts.gcTime ?? config.gcTime
653
+ },
654
+ prefillDetailCache: queryOpts.prefillDetailCache ?? true,
655
+ detailKeyBuilder: (id) => KEYS.detail(id),
656
+ itemIdResolver: resolveItemId,
657
+ select: queryOpts.select
658
+ });
659
+ }
660
+ function useBulkActions() {
661
+ const bulkCreateMutation = useMutationWithTransition({
662
+ mutationFn: (vars) => {
663
+ if (!api.bulkCreate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a bulkCreate method`));
664
+ const auth = getAuthContext();
665
+ return api.bulkCreate({
666
+ token: vars.token ?? auth.token,
667
+ organizationId: vars.organizationId ?? auth.organizationId,
668
+ data: vars.data
669
+ });
670
+ },
671
+ invalidateQueries: [KEYS.lists()],
672
+ messages: {
673
+ success: `${pluralName} created successfully`,
674
+ error: `Failed to create ${pluralName.toLowerCase()}`
675
+ },
676
+ toastHandler: instanceToast
677
+ });
678
+ const bulkUpdateMutation = useMutationWithTransition({
679
+ mutationFn: (vars) => {
680
+ if (!api.bulkUpdate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a bulkUpdate method`));
681
+ const auth = getAuthContext();
682
+ return api.bulkUpdate({
683
+ token: vars.token ?? auth.token,
684
+ organizationId: vars.organizationId ?? auth.organizationId,
685
+ filter: vars.filter,
686
+ data: vars.data
687
+ });
688
+ },
689
+ invalidateQueries: [KEYS.lists(), KEYS.details()],
690
+ messages: {
691
+ success: `${pluralName} updated successfully`,
692
+ error: `Failed to update ${pluralName.toLowerCase()}`
693
+ },
694
+ toastHandler: instanceToast
695
+ });
696
+ const bulkDeleteMutation = useMutationWithTransition({
697
+ mutationFn: (vars) => {
698
+ if (!api.bulkDelete) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a bulkDelete method`));
699
+ const auth = getAuthContext();
700
+ return api.bulkDelete({
701
+ token: vars.token ?? auth.token,
702
+ organizationId: vars.organizationId ?? auth.organizationId,
703
+ filter: vars.filter
704
+ });
705
+ },
706
+ invalidateQueries: [KEYS.lists()],
707
+ messages: {
708
+ success: `${pluralName} deleted successfully`,
709
+ error: `Failed to delete ${pluralName.toLowerCase()}`
710
+ },
711
+ toastHandler: instanceToast
712
+ });
713
+ return {
714
+ bulkCreate: async (params, options) => {
715
+ const items = (await bulkCreateMutation.mutateAsync(params))?.data ?? [];
716
+ options?.onSuccess?.(items);
717
+ return items;
718
+ },
719
+ bulkUpdate: async (params, options) => {
720
+ const result = await bulkUpdateMutation.mutateAsync(params);
721
+ options?.onSuccess?.(result);
722
+ return result;
723
+ },
724
+ bulkRemove: async (params, options) => {
725
+ const result = await bulkDeleteMutation.mutateAsync(params);
726
+ options?.onSuccess?.(result);
727
+ return result;
728
+ },
729
+ isBulkCreating: bulkCreateMutation.isPending,
730
+ isBulkUpdating: bulkUpdateMutation.isPending,
731
+ isBulkDeleting: bulkDeleteMutation.isPending
732
+ };
733
+ }
444
734
  const resolvedRouterHook = instanceNavigation ?? useRouterHook ?? (() => ({
445
735
  push: () => {},
446
736
  replace: () => {}
@@ -449,7 +739,7 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
449
739
  const queryClient = useQueryClient();
450
740
  const router = resolvedRouterHook();
451
741
  return useCallback((href, item, options = {}) => {
452
- const id = getItemId(item);
742
+ const id = resolveItemId(item);
453
743
  if (id) queryClient.setQueryData(KEYS.detail(id), { data: item });
454
744
  if (!router) return;
455
745
  const { scroll = true, replace = false } = options;
@@ -464,6 +754,12 @@ function createCrudHooks({ api, entityKey, singular, defaults = {}, callbacks =
464
754
  useDetail,
465
755
  useInfiniteList,
466
756
  useActions,
757
+ useBulkActions,
758
+ useDeleted,
759
+ useDetailBySlug,
760
+ useTree,
761
+ useChildren,
762
+ useFindBy,
467
763
  useUpload,
468
764
  useSearch,
469
765
  useCustomMutation,
@@ -1,5 +1,6 @@
1
1
  import { ToastHandler } from "./client.js";
2
- import * as _tanstack_react_query0 from "@tanstack/react-query";
2
+ import { QUERY_CONFIGS } from "./query.js";
3
+ import * as _$_tanstack_react_query0 from "@tanstack/react-query";
3
4
  import { QueryClient, QueryKey, UseMutateAsyncFunction, UseMutateFunction } from "@tanstack/react-query";
4
5
 
5
6
  //#region src/mutation.d.ts
@@ -47,8 +48,8 @@ interface TransitionMutationConfig<TData, TVariables> {
47
48
  toastHandler?: ToastHandler;
48
49
  }
49
50
  declare function useMutationWithTransition<TData, TVariables>(config: TransitionMutationConfig<TData, TVariables>): {
50
- mutate: UseMutateFunction<TData, Error, TVariables, unknown>;
51
- mutateAsync: UseMutateAsyncFunction<TData, Error, TVariables, unknown>;
51
+ mutate: UseMutateFunction<TData, Error, TVariables, void>;
52
+ mutateAsync: UseMutateAsyncFunction<TData, Error, TVariables, void>;
52
53
  isPending: boolean;
53
54
  isSuccess: boolean;
54
55
  isError: boolean;
@@ -100,27 +101,12 @@ interface CreateOptimisticMutationConfig<TData, TVariables> {
100
101
  shouldToast?: () => boolean;
101
102
  toastHandler?: ToastHandler;
102
103
  }
103
- declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimisticMutationConfig<TData, TVariables>): _tanstack_react_query0.UseMutationResult<TData, Error, TVariables, {
104
+ declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimisticMutationConfig<TData, TVariables>): _$_tanstack_react_query0.UseMutationResult<TData, Error, TVariables, {
104
105
  previous: {
105
106
  key: readonly unknown[];
106
107
  data: [readonly unknown[], unknown][];
107
108
  }[];
108
109
  }>;
109
- declare const QUERY_CONFIGS: {
110
- readonly realtime: {
111
- readonly staleTime: 20000;
112
- readonly refetchInterval: 30000;
113
- };
114
- readonly frequent: {
115
- readonly staleTime: 60000;
116
- };
117
- readonly stable: {
118
- readonly staleTime: 300000;
119
- };
120
- readonly static: {
121
- readonly staleTime: 600000;
122
- };
123
- };
124
110
  /** @deprecated Use `useOptimisticMutation` */
125
111
  declare const createOptimisticMutation: typeof useOptimisticMutation;
126
112
  //#endregion
package/dist/mutation.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
- import { isArcApiError } from "./client.js";
3
+ import { isArcApiError, isAutoIdempotency } from "./client.js";
4
+ import { QUERY_CONFIGS } from "./query.js";
4
5
  import { useMutation, useQueryClient } from "@tanstack/react-query";
5
- import { useTransition } from "react";
6
+ import { useCallback, useRef, useTransition } from "react";
6
7
 
7
8
  //#region src/mutation.ts
8
9
  let toastHandler = {
@@ -43,8 +44,18 @@ function useMutationWithTransition(config) {
43
44
  const { mutationFn, invalidateQueries = [], onSuccess, onError, onSettled, messages, useTransition: withTransition = true, showToast: toast = true, toastHandler: instanceToast } = config;
44
45
  const queryClient = useQueryClient();
45
46
  const [isTransitioning, startTransition] = useTransition();
47
+ const idempotencyKeyRef = useRef(null);
46
48
  const mutation = useMutation({
47
- mutationFn,
49
+ mutationFn: (variables) => {
50
+ if (idempotencyKeyRef.current && typeof variables === "object" && variables !== null) {
51
+ const vars = variables;
52
+ if (vars.options && typeof vars.options === "object") vars.options.idempotencyKey ??= idempotencyKeyRef.current;
53
+ }
54
+ return mutationFn(variables);
55
+ },
56
+ onMutate: () => {
57
+ idempotencyKeyRef.current = isAutoIdempotency() ? globalThis.crypto.randomUUID() : null;
58
+ },
48
59
  onSuccess: (data, variables) => {
49
60
  const invalidate = () => {
50
61
  invalidateQueries.forEach((key) => queryClient.invalidateQueries({ queryKey: key }));
@@ -151,15 +162,6 @@ function useOptimisticMutation(config) {
151
162
  }
152
163
  });
153
164
  }
154
- const QUERY_CONFIGS = {
155
- realtime: {
156
- staleTime: 2e4,
157
- refetchInterval: 3e4
158
- },
159
- frequent: { staleTime: 6e4 },
160
- stable: { staleTime: 3e5 },
161
- static: { staleTime: 6e5 }
162
- };
163
165
  /** @deprecated Use `useOptimisticMutation` */
164
166
  const createOptimisticMutation = useOptimisticMutation;
165
167
 
@@ -28,6 +28,21 @@ interface CrudPrefetcher {
28
28
  * await productsPrefetcher.prefetchDetail(queryClient, productId);
29
29
  */
30
30
  prefetchDetail: (queryClient: QueryClient, id: string, options?: PrefetchDetailOptions) => Promise<void>;
31
+ /**
32
+ * Prefetch a detail-by-slug query. Uses the same query keys as useDetailBySlug.
33
+ * Only available when the API has a `getBySlug` method (slugLookup preset).
34
+ */
35
+ prefetchBySlug: (queryClient: QueryClient, slug: string, options?: PrefetchDetailOptions) => Promise<void>;
36
+ /**
37
+ * Prefetch soft-deleted items. Uses the same query keys as useDeleted.
38
+ * Only available when the API has a `getDeleted` method (softDelete preset).
39
+ */
40
+ prefetchDeleted: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
41
+ /**
42
+ * Prefetch a tree query. Uses the same query keys as useTree.
43
+ * Only available when the API has a `getTree` method (tree preset).
44
+ */
45
+ prefetchTree: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
31
46
  }
32
47
  /**
33
48
  * Create server-safe prefetch helpers for CRUD queries.
@@ -65,6 +80,22 @@ declare function createCrudPrefetcher(api: {
65
80
  token?: string | null;
66
81
  organizationId?: string | null;
67
82
  }) => Promise<unknown>;
83
+ getBySlug?: (opts: {
84
+ slug: string;
85
+ token?: string | null;
86
+ organizationId?: string | null;
87
+ params?: Record<string, unknown>;
88
+ }) => Promise<unknown>;
89
+ getDeleted?: (opts: {
90
+ params?: Record<string, unknown>;
91
+ token?: string | null;
92
+ organizationId?: string | null;
93
+ }) => Promise<unknown>;
94
+ getTree?: (opts: {
95
+ params?: Record<string, unknown>;
96
+ token?: string | null;
97
+ organizationId?: string | null;
98
+ }) => Promise<unknown>;
68
99
  }, entityKey: string): CrudPrefetcher;
69
100
  //#endregion
70
101
  export { CrudPrefetcher, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
package/dist/prefetch.js CHANGED
@@ -1,23 +1,7 @@
1
+ import { createQueryKeys } from "./query.js";
1
2
  import { dehydrate } from "@tanstack/react-query";
2
3
 
3
4
  //#region src/prefetch.ts
4
- function scopedListKey(entityKey, scope, params) {
5
- return [
6
- entityKey,
7
- "list",
8
- {
9
- _scope: scope,
10
- ...params || {}
11
- }
12
- ];
13
- }
14
- function detailKey(entityKey, id) {
15
- return [
16
- entityKey,
17
- "detail",
18
- id
19
- ];
20
- }
21
5
  /**
22
6
  * Create server-safe prefetch helpers for CRUD queries.
23
7
  * Use in Next.js server components to pre-populate the query cache before rendering.
@@ -44,10 +28,12 @@ function detailKey(entityKey, id) {
44
28
  * }
45
29
  */
46
30
  function createCrudPrefetcher(api, entityKey) {
31
+ const KEYS = createQueryKeys(entityKey);
47
32
  return {
48
33
  async prefetchList(queryClient, params = {}, options = {}) {
49
34
  const { organizationId, ...restParams } = params;
50
- const queryKey = scopedListKey(entityKey, organizationId ? "tenant" : "super-admin", {
35
+ const scope = organizationId ? "tenant" : "super-admin";
36
+ const queryKey = KEYS.scopedList(scope, {
51
37
  organizationId,
52
38
  ...restParams
53
39
  });
@@ -62,7 +48,7 @@ function createCrudPrefetcher(api, entityKey) {
62
48
  },
63
49
  async prefetchDetail(queryClient, id, options = {}) {
64
50
  const { params, staleTime } = options;
65
- const baseKey = detailKey(entityKey, id);
51
+ const baseKey = KEYS.detail(id);
66
52
  const queryKey = params ? [...baseKey, params] : baseKey;
67
53
  await queryClient.prefetchQuery({
68
54
  queryKey,
@@ -72,6 +58,51 @@ function createCrudPrefetcher(api, entityKey) {
72
58
  }),
73
59
  staleTime
74
60
  });
61
+ },
62
+ async prefetchBySlug(queryClient, slug, options = {}) {
63
+ if (!api.getBySlug) throw new Error(`[arc-next] prefetchBySlug requires an api with getBySlug (slugLookup preset)`);
64
+ const { params, staleTime } = options;
65
+ const queryKey = params ? KEYS.custom("slug", slug, params) : KEYS.custom("slug", slug);
66
+ await queryClient.prefetchQuery({
67
+ queryKey,
68
+ queryFn: () => api.getBySlug({
69
+ slug,
70
+ ...params ? { params } : {}
71
+ }),
72
+ staleTime
73
+ });
74
+ },
75
+ async prefetchDeleted(queryClient, params = {}, options = {}) {
76
+ if (!api.getDeleted) throw new Error(`[arc-next] prefetchDeleted requires an api with getDeleted (softDelete preset)`);
77
+ const { organizationId, ...restParams } = params;
78
+ const queryKey = KEYS.custom("deleted", {
79
+ organizationId,
80
+ ...restParams
81
+ });
82
+ await queryClient.prefetchQuery({
83
+ queryKey,
84
+ queryFn: () => api.getDeleted({
85
+ params: restParams,
86
+ organizationId: organizationId ?? null
87
+ }),
88
+ staleTime: options.staleTime
89
+ });
90
+ },
91
+ async prefetchTree(queryClient, params = {}, options = {}) {
92
+ if (!api.getTree) throw new Error(`[arc-next] prefetchTree requires an api with getTree (tree preset)`);
93
+ const { organizationId, ...restParams } = params;
94
+ const queryKey = KEYS.custom("tree", {
95
+ organizationId,
96
+ ...restParams
97
+ });
98
+ await queryClient.prefetchQuery({
99
+ queryKey,
100
+ queryFn: () => api.getTree({
101
+ params: restParams,
102
+ organizationId: organizationId ?? null
103
+ }),
104
+ staleTime: options.staleTime
105
+ });
75
106
  }
76
107
  };
77
108
  }