@classytic/arc-next 0.9.1 → 0.11.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
@@ -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 } from "./cache.js";
5
+ import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, 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";
@@ -75,6 +75,13 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
75
75
  ...defaults.messages
76
76
  }
77
77
  };
78
+ const computeEnabled = (token, options, extraGate = true) => {
79
+ const merged = options.public === void 0 && config.defaultPublic ? {
80
+ ...options,
81
+ public: true
82
+ } : options;
83
+ return extraGate && createEnabledRule(token, merged, resolveAuthMode(), resolveHasStaticAuth());
84
+ };
78
85
  function useList(tokenOrParams, paramsOrOptions, maybeOptions) {
79
86
  let token;
80
87
  let params;
@@ -97,10 +104,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
97
104
  const scope = options._scope || (organizationId ? "tenant" : "super-admin");
98
105
  const { request: requestOpts, ...queryOpts } = options;
99
106
  return useListQuery({
100
- queryKey: KEYS.scopedList(scope, {
101
- organizationId,
102
- ...restParams
103
- }),
107
+ queryKey: KEYS.scopedList(scope, withOrgParams(organizationId, restParams)),
104
108
  queryFn: ({ signal }) => api.getAll({
105
109
  token,
106
110
  organizationId,
@@ -110,7 +114,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
110
114
  ...requestOpts
111
115
  }
112
116
  }),
113
- enabled: createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
117
+ enabled: computeEnabled(token, queryOpts),
114
118
  options: {
115
119
  staleTime: queryOpts.staleTime ?? config.staleTime,
116
120
  gcTime: queryOpts.gcTime ?? config.gcTime,
@@ -154,7 +158,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
154
158
  ...requestOpts
155
159
  }
156
160
  }),
157
- enabled: !!id && createEnabledRule(token, restOptions, resolveAuthMode(), resolveHasStaticAuth()),
161
+ enabled: computeEnabled(token, restOptions, !!id),
158
162
  options: {
159
163
  staleTime: restOptions.staleTime ?? config.staleTime,
160
164
  gcTime: restOptions.gcTime ?? config.gcTime,
@@ -491,10 +495,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
491
495
  const scope = options._scope || (organizationId ? "tenant" : "super-admin");
492
496
  const { request: requestOpts, ...queryOpts } = options;
493
497
  return useInfiniteListQuery({
494
- queryKey: [...KEYS.scopedList(scope, {
495
- organizationId,
496
- ...restParams
497
- }), "infinite"],
498
+ queryKey: [...KEYS.scopedList(scope, withOrgParams(organizationId, restParams)), "infinite"],
498
499
  queryFn: ({ pageParam, signal }) => {
499
500
  const paginationParams = typeof pageParam === "string" ? { after: pageParam } : { page: pageParam };
500
501
  return api.getAll({
@@ -510,7 +511,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
510
511
  }
511
512
  });
512
513
  },
513
- enabled: createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
514
+ enabled: computeEnabled(token, queryOpts),
514
515
  initialPageParam: restParams.after ? restParams.after : 1,
515
516
  getNextPageParam: (lastPage) => {
516
517
  const page = lastPage;
@@ -579,10 +580,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
579
580
  const { organizationId: _, ...restParams } = mergedParams;
580
581
  const { request: requestOpts, ...queryOpts } = options ?? {};
581
582
  return useListQuery({
582
- queryKey: KEYS.custom("deleted", {
583
- organizationId,
584
- ...restParams
585
- }),
583
+ queryKey: KEYS.custom("deleted", withOrgParams(organizationId, restParams)),
586
584
  queryFn: ({ signal }) => {
587
585
  if (!api.getDeleted) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getDeleted method`));
588
586
  return api.getDeleted({
@@ -595,7 +593,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
595
593
  }
596
594
  });
597
595
  },
598
- enabled: !!api.getDeleted && createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
596
+ enabled: computeEnabled(token, queryOpts, !!api.getDeleted),
599
597
  options: {
600
598
  staleTime: queryOpts.staleTime ?? config.staleTime,
601
599
  gcTime: queryOpts.gcTime ?? config.gcTime
@@ -603,6 +601,23 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
603
601
  select: queryOpts.select
604
602
  });
605
603
  }
604
+ function useCount(params, options) {
605
+ const auth = resolveAuth();
606
+ const mergedParams = params ?? {};
607
+ const organizationId = mergedParams.organizationId ?? auth.organizationId;
608
+ const { organizationId: _, ...restParams } = mergedParams;
609
+ return useQuery({
610
+ queryKey: KEYS.custom("count", withOrgParams(organizationId, restParams)),
611
+ queryFn: () => api.count({
612
+ token: auth.token,
613
+ organizationId,
614
+ params: restParams
615
+ }),
616
+ enabled: options?.enabled ?? true,
617
+ staleTime: options?.staleTime ?? config.staleTime,
618
+ gcTime: options?.gcTime ?? config.gcTime
619
+ });
620
+ }
606
621
  function useDetailBySlug(slug, options) {
607
622
  const auth = resolveAuth();
608
623
  const token = auth.token;
@@ -626,7 +641,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
626
641
  }
627
642
  });
628
643
  },
629
- enabled: !!api.getBySlug && !!slug && createEnabledRule(token, restOptions, resolveAuthMode(), resolveHasStaticAuth()),
644
+ enabled: computeEnabled(token, restOptions, !!api.getBySlug && !!slug),
630
645
  options: {
631
646
  staleTime: restOptions.staleTime ?? config.staleTime,
632
647
  gcTime: restOptions.gcTime ?? config.gcTime,
@@ -654,10 +669,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
654
669
  const { organizationId: _, ...restParams } = mergedParams;
655
670
  const { request: requestOpts, ...queryOpts } = options ?? {};
656
671
  return useListQuery({
657
- queryKey: KEYS.custom("tree", {
658
- organizationId,
659
- ...restParams
660
- }),
672
+ queryKey: KEYS.custom("tree", withOrgParams(organizationId, restParams)),
661
673
  queryFn: ({ signal }) => {
662
674
  if (!api.getTree) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getTree method`));
663
675
  return api.getTree({
@@ -670,7 +682,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
670
682
  }
671
683
  });
672
684
  },
673
- enabled: !!api.getTree && createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
685
+ enabled: computeEnabled(token, queryOpts, !!api.getTree),
674
686
  options: {
675
687
  staleTime: queryOpts.staleTime ?? config.staleTime,
676
688
  gcTime: queryOpts.gcTime ?? config.gcTime
@@ -686,10 +698,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
686
698
  const { organizationId: _, ...restParams } = mergedParams;
687
699
  const { request: requestOpts, ...queryOpts } = options ?? {};
688
700
  return useListQuery({
689
- queryKey: KEYS.custom("children", parentId, {
690
- organizationId,
691
- ...restParams
692
- }),
701
+ queryKey: KEYS.custom("children", parentId, withOrgParams(organizationId, restParams)),
693
702
  queryFn: ({ signal }) => {
694
703
  if (!api.getChildren) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getChildren method`));
695
704
  return api.getChildren({
@@ -703,7 +712,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
703
712
  }
704
713
  });
705
714
  },
706
- enabled: !!api.getChildren && !!parentId && createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
715
+ enabled: computeEnabled(token, queryOpts, !!api.getChildren && !!parentId),
707
716
  options: {
708
717
  staleTime: queryOpts.staleTime ?? config.staleTime,
709
718
  gcTime: queryOpts.gcTime ?? config.gcTime
@@ -1037,6 +1046,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
1037
1046
  useInfiniteList,
1038
1047
  useActions,
1039
1048
  useBulkActions,
1049
+ useCount,
1040
1050
  useDeleted,
1041
1051
  useDetailBySlug,
1042
1052
  useTree,
package/dist/mutation.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { isArcApiError, isAutoIdempotency } from "./client.js";
3
+ import { getQuotaDetails, isArcApiError, isAutoIdempotency } from "./client.js";
4
4
  import { useMutation, useQueryClient } from "@tanstack/react-query";
5
5
  import { useCallback, useRef, useTransition } from "react";
6
6
 
@@ -50,7 +50,9 @@ function showToast(type, messages, data, variables, error, handler) {
50
50
  } else {
51
51
  const msg = messages?.error;
52
52
  let defaultMsg = error?.message || "An error occurred";
53
- if (isArcApiError(error) && error.fieldErrors) {
53
+ const quota = getQuotaDetails(error);
54
+ if (quota) defaultMsg = `${quota.used.toLocaleString()} of ${quota.limit.toLocaleString()} ${quota.kind} used this period — resets ${new Date(quota.resetsAt).toLocaleDateString()}`;
55
+ else if (isArcApiError(error) && error.fieldErrors) {
54
56
  const fields = Object.entries(error.fieldErrors);
55
57
  if (fields.length > 0) defaultMsg = fields.map(([k, v]) => `${k}: ${v}`).join(", ");
56
58
  }
@@ -1,3 +1,4 @@
1
+ import { EntityReadApi } from "./query-options.js";
1
2
  import { HydrationBoundary, InfiniteData, QueryClient, dehydrate } from "@tanstack/react-query";
2
3
 
3
4
  //#region src/prefetch.d.ts
@@ -22,13 +23,6 @@ interface PrefetchOptions extends PrefetchAuthContext {
22
23
  revalidate?: number | false;
23
24
  tags?: string[];
24
25
  }
25
- /** Per-call API `options` the prefetcher forwards (caching + custom headers). */
26
- type ForwardedApiOptions = {
27
- headerOptions?: Record<string, string>;
28
- cache?: RequestCache;
29
- revalidate?: number | false;
30
- tags?: string[];
31
- };
32
26
  interface PrefetchDetailOptions extends PrefetchOptions {
33
27
  /** Query params (select, populate) — key must match useDetail's params to share cache */
34
28
  params?: {
@@ -46,11 +40,8 @@ interface CrudPrefetcher {
46
40
  */
47
41
  prefetchList: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
48
42
  /**
49
- * Prefetch a detail query on the server. Uses the same query keys as useDetail.
50
- *
51
- * @example
52
- * const queryClient = getQueryClient();
53
- * await productsPrefetcher.prefetchDetail(queryClient, productId);
43
+ * Prefetch a detail query on the server. Uses the same query keys as useDetail
44
+ * (tenant-scoped when `organizationId` is provided).
54
45
  */
55
46
  prefetchDetail: (queryClient: QueryClient, id: string, options?: PrefetchDetailOptions) => Promise<void>;
56
47
  /**
@@ -68,16 +59,6 @@ interface CrudPrefetcher {
68
59
  * Only available when the API has a `getTree` method (tree preset).
69
60
  */
70
61
  prefetchTree: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
71
- /**
72
- * Prefetch an infinite list query (cursor / page-based pagination). Uses the
73
- * same query keys as `useInfiniteList` and seeds the `{ pages, pageParams }`
74
- * shape TanStack Query expects for `useInfiniteQuery` — a flat
75
- * `prefetchQuery` would NOT match the cache shape and the client hook would
76
- * re-fetch from scratch, defeating the prefetch.
77
- *
78
- * @example
79
- * await productsPrefetcher.prefetchInfiniteList(queryClient, { limit: 20 });
80
- */
81
62
  /**
82
63
  * Prefetch a declared aggregation (arc 2.13+). Uses the same query key as
83
64
  * `useAggregation` so RSC-pre-rendered dashboard rows hydrate without a
@@ -92,12 +73,31 @@ interface CrudPrefetcher {
92
73
  * );
93
74
  */
94
75
  prefetchAggregation: (queryClient: QueryClient, name: string, filter?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
76
+ /**
77
+ * Prefetch an infinite list query (cursor / page-based pagination). Uses the
78
+ * same query keys as `useInfiniteList` and seeds the `{ pages, pageParams }`
79
+ * shape TanStack Query expects for `useInfiniteQuery` — a flat
80
+ * `prefetchQuery` would NOT match the cache shape and the client hook would
81
+ * re-fetch from scratch, defeating the prefetch.
82
+ *
83
+ * @example
84
+ * await productsPrefetcher.prefetchInfiniteList(queryClient, { limit: 20 });
85
+ */
95
86
  prefetchInfiniteList: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
96
87
  }
97
88
  /**
98
89
  * Create server-safe prefetch helpers for CRUD queries.
99
90
  * Use in Next.js server components to pre-populate the query cache before rendering.
100
91
  *
92
+ * Since 0.10 this is a thin layer over `createEntityQueries`
93
+ * (@classytic/arc-next/query-options) — the queryOptions factories are the
94
+ * single source of key + queryFn, shared with the client CRUD hooks, so
95
+ * prefetch keys can never drift from hook keys. Prefer the factories directly
96
+ * for new code that also needs `ensureQueryData` / router-loader integration:
97
+ *
98
+ * const products = createEntityQueries(productApi, 'products');
99
+ * await queryClient.prefetchQuery({ ...products.list({ limit: 20 }, { token }), staleTime: 60_000 });
100
+ *
101
101
  * @example
102
102
  * // products-prefetch.ts
103
103
  * import { productsApi } from '@/api/products-api';
@@ -119,45 +119,6 @@ interface CrudPrefetcher {
119
119
  * );
120
120
  * }
121
121
  */
122
- declare function createCrudPrefetcher(api: {
123
- getAll: (opts: {
124
- params?: Record<string, unknown>;
125
- token?: string | null;
126
- organizationId?: string | null;
127
- options?: ForwardedApiOptions;
128
- }) => Promise<unknown>;
129
- getById: (opts: {
130
- id: string;
131
- token?: string | null;
132
- organizationId?: string | null;
133
- options?: ForwardedApiOptions;
134
- }) => Promise<unknown>;
135
- getBySlug?: (opts: {
136
- slug: string;
137
- token?: string | null;
138
- organizationId?: string | null;
139
- params?: Record<string, unknown>;
140
- options?: ForwardedApiOptions;
141
- }) => Promise<unknown>;
142
- getDeleted?: (opts: {
143
- params?: Record<string, unknown>;
144
- token?: string | null;
145
- organizationId?: string | null;
146
- options?: ForwardedApiOptions;
147
- }) => Promise<unknown>;
148
- aggregate?: (opts: {
149
- name: string;
150
- filter?: Record<string, unknown>;
151
- token?: string | null;
152
- organizationId?: string | null;
153
- options?: ForwardedApiOptions;
154
- }) => Promise<unknown>;
155
- getTree?: (opts: {
156
- params?: Record<string, unknown>;
157
- token?: string | null;
158
- organizationId?: string | null;
159
- options?: ForwardedApiOptions;
160
- }) => Promise<unknown>;
161
- }, entityKey: string): CrudPrefetcher;
122
+ declare function createCrudPrefetcher(api: EntityReadApi, entityKey: string): CrudPrefetcher;
162
123
  //#endregion
163
124
  export { CrudPrefetcher, HydrationBoundary, type InfiniteData, PrefetchAuthContext, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
package/dist/prefetch.js CHANGED
@@ -1,11 +1,25 @@
1
- import { createQueryKeys } from "./cache.js";
1
+ import { createEntityQueries } from "./query-options.js";
2
2
  import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
3
3
 
4
4
  //#region src/prefetch.ts
5
+ /** Extract the queryFn context (auth + fetch caching) from prefetch options. */
6
+ function toCtx(options) {
7
+ const { staleTime: _staleTime, ...ctx } = options;
8
+ return ctx;
9
+ }
5
10
  /**
6
11
  * Create server-safe prefetch helpers for CRUD queries.
7
12
  * Use in Next.js server components to pre-populate the query cache before rendering.
8
13
  *
14
+ * Since 0.10 this is a thin layer over `createEntityQueries`
15
+ * (@classytic/arc-next/query-options) — the queryOptions factories are the
16
+ * single source of key + queryFn, shared with the client CRUD hooks, so
17
+ * prefetch keys can never drift from hook keys. Prefer the factories directly
18
+ * for new code that also needs `ensureQueryData` / router-loader integration:
19
+ *
20
+ * const products = createEntityQueries(productApi, 'products');
21
+ * await queryClient.prefetchQuery({ ...products.list({ limit: 20 }, { token }), staleTime: 60_000 });
22
+ *
9
23
  * @example
10
24
  * // products-prefetch.ts
11
25
  * import { productsApi } from '@/api/products-api';
@@ -28,146 +42,54 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
28
42
  * }
29
43
  */
30
44
  function createCrudPrefetcher(api, entityKey) {
31
- const KEYS = createQueryKeys(entityKey);
32
- const apiOptions = (o) => {
33
- const opt = {};
34
- if (o.headers) opt.headerOptions = o.headers;
35
- if (o.cache !== void 0) opt.cache = o.cache;
36
- if (o.revalidate !== void 0) opt.revalidate = o.revalidate;
37
- if (o.tags !== void 0) opt.tags = o.tags;
38
- return Object.keys(opt).length ? { options: opt } : {};
39
- };
45
+ const queries = createEntityQueries(api, entityKey);
40
46
  return {
41
47
  async prefetchList(queryClient, params = {}, options = {}) {
42
- const { organizationId: paramOrgId, ...restParams } = params;
43
- const orgId = paramOrgId ?? options.organizationId ?? null;
44
- const scope = orgId ? "tenant" : "super-admin";
45
- const queryKey = KEYS.scopedList(scope, {
46
- ...orgId ? { organizationId: orgId } : {},
47
- ...restParams
48
- });
49
48
  await queryClient.prefetchQuery({
50
- queryKey,
51
- queryFn: () => api.getAll({
52
- params: restParams,
53
- token: options.token ?? null,
54
- organizationId: orgId,
55
- ...apiOptions(options)
56
- }),
49
+ ...queries.list(params, toCtx(options)),
57
50
  staleTime: options.staleTime
58
51
  });
59
52
  },
60
53
  async prefetchDetail(queryClient, id, options = {}) {
61
- const { params, staleTime, token, organizationId } = options;
62
- const baseKey = KEYS.detail(id);
63
- const queryKey = params ? [...baseKey, params] : baseKey;
54
+ const { staleTime, ...detailOpts } = options;
64
55
  await queryClient.prefetchQuery({
65
- queryKey,
66
- queryFn: () => api.getById({
67
- id,
68
- token: token ?? null,
69
- organizationId: organizationId ?? null,
70
- ...params ? { params } : {},
71
- ...apiOptions(options)
72
- }),
56
+ ...queries.detail(id, detailOpts),
73
57
  staleTime
74
58
  });
75
59
  },
76
60
  async prefetchBySlug(queryClient, slug, options = {}) {
77
61
  if (!api.getBySlug) throw new Error(`[arc-next] prefetchBySlug requires an api with getBySlug (slugLookup preset)`);
78
- const { params, staleTime, token, organizationId } = options;
79
- const queryKey = params ? KEYS.custom("slug", slug, params) : KEYS.custom("slug", slug);
62
+ const { staleTime, ...detailOpts } = options;
80
63
  await queryClient.prefetchQuery({
81
- queryKey,
82
- queryFn: () => api.getBySlug({
83
- slug,
84
- token: token ?? null,
85
- organizationId: organizationId ?? null,
86
- ...params ? { params } : {},
87
- ...apiOptions(options)
88
- }),
64
+ ...queries.bySlug(slug, detailOpts),
89
65
  staleTime
90
66
  });
91
67
  },
92
68
  async prefetchDeleted(queryClient, params = {}, options = {}) {
93
69
  if (!api.getDeleted) throw new Error(`[arc-next] prefetchDeleted requires an api with getDeleted (softDelete preset)`);
94
- const { organizationId: paramOrgId, ...restParams } = params;
95
- const orgId = paramOrgId ?? options.organizationId ?? null;
96
- const queryKey = KEYS.custom("deleted", {
97
- ...orgId ? { organizationId: orgId } : {},
98
- ...restParams
99
- });
100
70
  await queryClient.prefetchQuery({
101
- queryKey,
102
- queryFn: () => api.getDeleted({
103
- params: restParams,
104
- token: options.token ?? null,
105
- organizationId: orgId,
106
- ...apiOptions(options)
107
- }),
71
+ ...queries.deleted(params, toCtx(options)),
108
72
  staleTime: options.staleTime
109
73
  });
110
74
  },
111
75
  async prefetchAggregation(queryClient, name, filter, options = {}) {
112
76
  if (!api.aggregate) throw new Error(`[arc-next] prefetchAggregation requires an api with aggregate (arc 2.13+)`);
113
77
  if (!name) throw new Error("[arc-next] prefetchAggregation: aggregation name is required");
114
- const orgId = options.organizationId ?? null;
115
- const filterKey = orgId ? {
116
- _org: orgId,
117
- ...filter ?? {}
118
- } : filter ?? {};
119
78
  await queryClient.prefetchQuery({
120
- queryKey: KEYS.aggregation(name, filterKey),
121
- queryFn: () => api.aggregate({
122
- name,
123
- filter,
124
- token: options.token ?? null,
125
- organizationId: orgId,
126
- ...apiOptions(options)
127
- }),
79
+ ...queries.aggregation(name, filter, toCtx(options)),
128
80
  staleTime: options.staleTime
129
81
  });
130
82
  },
131
83
  async prefetchTree(queryClient, params = {}, options = {}) {
132
84
  if (!api.getTree) throw new Error(`[arc-next] prefetchTree requires an api with getTree (tree preset)`);
133
- const { organizationId: paramOrgId, ...restParams } = params;
134
- const orgId = paramOrgId ?? options.organizationId ?? null;
135
- const queryKey = KEYS.custom("tree", {
136
- ...orgId ? { organizationId: orgId } : {},
137
- ...restParams
138
- });
139
85
  await queryClient.prefetchQuery({
140
- queryKey,
141
- queryFn: () => api.getTree({
142
- params: restParams,
143
- token: options.token ?? null,
144
- organizationId: orgId,
145
- ...apiOptions(options)
146
- }),
86
+ ...queries.tree(params, toCtx(options)),
147
87
  staleTime: options.staleTime
148
88
  });
149
89
  },
150
90
  async prefetchInfiniteList(queryClient, params = {}, options = {}) {
151
- const { organizationId: paramOrgId, ...restParams } = params;
152
- const orgId = paramOrgId ?? options.organizationId ?? null;
153
- const scope = orgId ? "tenant" : "super-admin";
154
- const queryKey = [...KEYS.scopedList(scope, {
155
- ...orgId ? { organizationId: orgId } : {},
156
- ...restParams
157
- }), "infinite"];
158
91
  await queryClient.prefetchInfiniteQuery({
159
- queryKey,
160
- queryFn: ({ pageParam }) => api.getAll({
161
- params: {
162
- ...restParams,
163
- ...pageParam ? { page: pageParam } : {}
164
- },
165
- token: options.token ?? null,
166
- organizationId: orgId,
167
- ...apiOptions(options)
168
- }),
169
- initialPageParam: 1,
170
- getNextPageParam: () => void 0,
92
+ ...queries.infiniteList(params, toCtx(options)),
171
93
  staleTime: options.staleTime
172
94
  });
173
95
  }
@@ -0,0 +1,56 @@
1
+ import { AnyBaseApi, ScopedArgs } from "../api.js";
2
+
3
+ //#region src/presets/history.d.ts
4
+ /** One audit-trail entry — arc's `AuditEntry` wire shape for a single record. */
5
+ interface HistoryEntry {
6
+ id: string;
7
+ resource: string;
8
+ documentId: string;
9
+ action: 'create' | 'update' | 'delete' | 'restore' | 'custom';
10
+ userId?: string;
11
+ organizationId?: string;
12
+ before?: Record<string, unknown>;
13
+ after?: Record<string, unknown>;
14
+ /** Field names that changed (updates). */
15
+ changes?: string[];
16
+ requestId?: string;
17
+ timestamp: string;
18
+ metadata?: Record<string, unknown>;
19
+ }
20
+ /** Wire shape of `GET /:resource/:id/history`. */
21
+ interface HistoryPage {
22
+ data: HistoryEntry[];
23
+ limit: number;
24
+ offset: number;
25
+ }
26
+ interface HistoryMethods {
27
+ /**
28
+ * Per-record change timeline. Backend mounts `GET /:resource/:id/history`
29
+ * when the resource declares `history: true` (arc 2.22) — audit-backed,
30
+ * newest first, gated stricter than reads (update → get → auth).
31
+ */
32
+ history(args: ScopedArgs & {
33
+ id: string;
34
+ params?: {
35
+ limit?: number;
36
+ offset?: number;
37
+ };
38
+ }): Promise<HistoryPage>;
39
+ }
40
+ /**
41
+ * Adds the per-record history method to a BaseApi.
42
+ *
43
+ * Mirrors arc's server-side `history: true` flag (2.22). Compose like every
44
+ * other preset wrapper:
45
+ *
46
+ * @example
47
+ * import { createCrudApi } from '@classytic/arc-next/api';
48
+ * import { withHistory } from '@classytic/arc-next/presets/history';
49
+ *
50
+ * const orders = withHistory(createCrudApi<Order>('orders'));
51
+ * const page = await orders.history({ id, params: { limit: 25 } });
52
+ * // page.data[0] → { action: 'update', changes: ['status'], before, after, ... }
53
+ */
54
+ declare function withHistory<TApi extends AnyBaseApi>(api: TApi): TApi & HistoryMethods;
55
+ //#endregion
56
+ export { HistoryEntry, HistoryMethods, HistoryPage, withHistory };
@@ -0,0 +1,29 @@
1
+ //#region src/presets/history.ts
2
+ /**
3
+ * Adds the per-record history method to a BaseApi.
4
+ *
5
+ * Mirrors arc's server-side `history: true` flag (2.22). Compose like every
6
+ * other preset wrapper:
7
+ *
8
+ * @example
9
+ * import { createCrudApi } from '@classytic/arc-next/api';
10
+ * import { withHistory } from '@classytic/arc-next/presets/history';
11
+ *
12
+ * const orders = withHistory(createCrudApi<Order>('orders'));
13
+ * const page = await orders.history({ id, params: { limit: 25 } });
14
+ * // page.data[0] → { action: 'update', changes: ['status'], before, after, ... }
15
+ */
16
+ function withHistory(api) {
17
+ return Object.assign(api, { async history({ token = null, organizationId = null, id, params = {}, options = {} }) {
18
+ if (!id) throw new Error("ID is required");
19
+ return api.request("GET", `${api.baseUrl}/${id}/history`, {
20
+ token,
21
+ organizationId,
22
+ params,
23
+ options
24
+ });
25
+ } });
26
+ }
27
+
28
+ //#endregion
29
+ export { withHistory };
@@ -1,3 +1,4 @@
1
+ import { isQuotaExceeded } from "./client.js";
1
2
  import { QueryClient, defaultShouldDehydrateQuery, isServer } from "@tanstack/react-query";
2
3
 
3
4
  //#region src/query-client.ts
@@ -14,7 +15,7 @@ function makeQueryClient(overrides) {
14
15
  };
15
16
  return new QueryClient({ defaultOptions: {
16
17
  queries: {
17
- retry: opts.retry,
18
+ retry: (failureCount, error) => !isQuotaExceeded(error) && failureCount < (opts.retry || 0),
18
19
  staleTime: opts.staleTime,
19
20
  gcTime: opts.gcTime,
20
21
  refetchOnWindowFocus: opts.refetchOnWindowFocus