@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/README.md +14 -0
- package/dist/api.d.ts +44 -0
- package/dist/api.js +66 -0
- package/dist/cache.d.ts +15 -1
- package/dist/cache.js +21 -1
- package/dist/client.d.ts +40 -1
- package/dist/client.js +30 -1
- package/dist/field-encryption.d.ts +86 -0
- package/dist/field-encryption.js +159 -0
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +38 -28
- package/dist/mutation.js +4 -2
- package/dist/prefetch.d.ts +23 -62
- package/dist/prefetch.js +25 -103
- package/dist/presets/history.d.ts +56 -0
- package/dist/presets/history.js +29 -0
- package/dist/query-client.js +2 -1
- package/dist/query-options.d.ts +165 -0
- package/dist/query-options.js +188 -0
- package/llms.txt +22 -0
- package/package.json +178 -165
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/prefetch.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
62
|
-
const baseKey = KEYS.detail(id);
|
|
63
|
-
const queryKey = params ? [...baseKey, params] : baseKey;
|
|
54
|
+
const { staleTime, ...detailOpts } = options;
|
|
64
55
|
await queryClient.prefetchQuery({
|
|
65
|
-
|
|
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 {
|
|
79
|
-
const queryKey = params ? KEYS.custom("slug", slug, params) : KEYS.custom("slug", slug);
|
|
62
|
+
const { staleTime, ...detailOpts } = options;
|
|
80
63
|
await queryClient.prefetchQuery({
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/query-client.js
CHANGED
|
@@ -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
|