@classytic/arc-next 0.5.0 → 0.7.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 +148 -9
- package/dist/api.d.ts +59 -75
- package/dist/api.js +25 -6
- package/dist/cache.d.ts +48 -4
- package/dist/cache.js +132 -26
- package/dist/client.d.ts +340 -72
- package/dist/client.js +500 -91
- package/dist/hooks.d.ts +72 -8
- package/dist/hooks.js +109 -33
- package/dist/prefetch.d.ts +23 -0
- package/dist/prefetch.js +20 -0
- package/dist/presets/bulk.d.ts +5 -4
- package/dist/presets/search.d.ts +5 -4
- package/dist/presets/slug.d.ts +2 -2
- package/dist/presets/soft-delete.d.ts +4 -3
- package/dist/presets/tree.d.ts +4 -3
- package/dist/query.d.ts +87 -32
- package/dist/query.js +72 -46
- package/dist/sse.js +71 -5
- package/dist/upload.js +33 -2
- package/dist/ws.js +42 -5
- package/package.json +9 -2
package/dist/hooks.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { ArcClient, UseRouterHook } from "./client.js";
|
|
2
|
-
import {
|
|
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 } from "./query.js";
|
|
5
|
+
import { DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, RequestPassthrough } from "./query.js";
|
|
6
6
|
import { SoftDeleteMethods } from "./presets/soft-delete.js";
|
|
7
7
|
import { BulkMethods } from "./presets/bulk.js";
|
|
8
8
|
import { SlugLookupMethods } from "./presets/slug.js";
|
|
9
9
|
import { TreeMethods } from "./presets/tree.js";
|
|
10
10
|
import { SearchPresetMethods } from "./presets/search.js";
|
|
11
|
-
import { QueryKey } from "@tanstack/react-query";
|
|
11
|
+
import { QueryKey, UseQueryResult } from "@tanstack/react-query";
|
|
12
12
|
|
|
13
13
|
//#region src/hooks.d.ts
|
|
14
14
|
/**
|
|
@@ -24,8 +24,50 @@ import { QueryKey } from "@tanstack/react-query";
|
|
|
24
24
|
type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>, 'getAll' | 'getById' | 'create' | 'update' | 'delete'> & {
|
|
25
25
|
upload?: BaseApi<T, TCreate, TUpdate>['upload'];
|
|
26
26
|
dispatchAction?: BaseApi<T, TCreate, TUpdate>['dispatchAction'];
|
|
27
|
-
invokeRoute?: BaseApi<T, TCreate, TUpdate>['invokeRoute'];
|
|
27
|
+
invokeRoute?: BaseApi<T, TCreate, TUpdate>['invokeRoute']; /** Declared aggregations (arc 2.13+). Always available on BaseApi. */
|
|
28
|
+
aggregate?: BaseApi<T, TCreate, TUpdate>['aggregate'];
|
|
28
29
|
} & Partial<SoftDeleteMethods<T>> & Partial<BulkMethods<T, TCreate, TUpdate>> & Partial<SlugLookupMethods<T>> & Partial<TreeMethods<T>> & Partial<SearchPresetMethods<T>>;
|
|
30
|
+
/** Args + options for `useAggregation`. Mirrors `ListQueryOptions` for DX consistency. */
|
|
31
|
+
interface AggregationQueryOptions<TRow extends AggRow = AggRow, TData = AggResult<TRow>> {
|
|
32
|
+
/** Bypass auth gate (for public dashboards behind `allowPublic` permissions). */
|
|
33
|
+
public?: boolean;
|
|
34
|
+
/** Skip the query (e.g. while a parent filter is being assembled). */
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
/** Per-call staleTime override. Defaults to factory's `defaults.staleTime`. */
|
|
37
|
+
staleTime?: number;
|
|
38
|
+
/** Per-call gcTime override. */
|
|
39
|
+
gcTime?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Refetch on window focus. Aggregations / dashboards usually want this OFF —
|
|
42
|
+
* long-running compute. Defaults to `false` regardless of factory config.
|
|
43
|
+
*/
|
|
44
|
+
refetchOnWindowFocus?: boolean;
|
|
45
|
+
/** Periodic refetch (ms or `false`). Set for live dashboards. */
|
|
46
|
+
refetchInterval?: number | false;
|
|
47
|
+
/** Continue polling while the tab is in the background. */
|
|
48
|
+
refetchIntervalInBackground?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Transform `AggResult<TRow>` before exposing it. Common pattern is to
|
|
51
|
+
* pluck the rows array directly:
|
|
52
|
+
* `select: (r) => r.rows`
|
|
53
|
+
* Or compute a derived value:
|
|
54
|
+
* `select: (r) => r.rows.reduce((acc, x) => acc + x.total, 0)`
|
|
55
|
+
* Runs on each render after structural-sharing dedupe.
|
|
56
|
+
*/
|
|
57
|
+
select?: (data: AggResult<TRow>) => TData;
|
|
58
|
+
/**
|
|
59
|
+
* Pass-through to the underlying fetch call. Use for Next.js ISR — pass
|
|
60
|
+
* `request: { revalidate: 60, tags: ['orders'] }` so the server-side fetch
|
|
61
|
+
* cache participates in `revalidateTag('orders')` invalidations alongside
|
|
62
|
+
* TanStack's client cache.
|
|
63
|
+
*/
|
|
64
|
+
request?: RequestPassthrough;
|
|
65
|
+
/**
|
|
66
|
+
* Show this data while the real query is loading / re-fetching with
|
|
67
|
+
* different filters. Smooths fast filter switches on dashboards.
|
|
68
|
+
*/
|
|
69
|
+
placeholderData?: AggResult<TRow> | ((prev: AggResult<TRow> | undefined) => AggResult<TRow> | undefined);
|
|
70
|
+
}
|
|
29
71
|
interface CrudHooksConfig<T, TCreate = Partial<T>, TUpdate = Partial<T>> {
|
|
30
72
|
api: CrudApi<T, TCreate, TUpdate>;
|
|
31
73
|
entityKey: string;
|
|
@@ -157,7 +199,7 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
157
199
|
invalidateQueries?: QueryKey[]; /** Default action name. Can be overridden per-call via `mutate({ action })`. */
|
|
158
200
|
action?: string;
|
|
159
201
|
messages?: MutationMessages;
|
|
160
|
-
onSuccess?: (data:
|
|
202
|
+
onSuccess?: (data: TResult, variables: {
|
|
161
203
|
id: string;
|
|
162
204
|
action: string;
|
|
163
205
|
data?: TBody;
|
|
@@ -167,12 +209,12 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
167
209
|
action: string;
|
|
168
210
|
data?: TBody;
|
|
169
211
|
}) => void;
|
|
170
|
-
onSettled?: (data:
|
|
212
|
+
onSettled?: (data: TResult | undefined, error: Error | null, variables: {
|
|
171
213
|
id: string;
|
|
172
214
|
action: string;
|
|
173
215
|
data?: TBody;
|
|
174
216
|
}) => void;
|
|
175
|
-
}) => TransitionMutationReturn<
|
|
217
|
+
}) => TransitionMutationReturn<TResult, {
|
|
176
218
|
id: string;
|
|
177
219
|
action?: string;
|
|
178
220
|
data?: TBody;
|
|
@@ -205,6 +247,28 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
205
247
|
input: string | string[];
|
|
206
248
|
body?: Record<string, unknown>;
|
|
207
249
|
}>;
|
|
250
|
+
/**
|
|
251
|
+
* Query against arc's declarative aggregations (arc v2.13+).
|
|
252
|
+
* `GET /:resource/aggregations/:name` — wire shape `{ rows: TRow[] }`.
|
|
253
|
+
*
|
|
254
|
+
* Cached under `KEYS.aggregation(name, filter)` so multiple call sites with
|
|
255
|
+
* the same args share one cache entry. Mutation hooks
|
|
256
|
+
* (`useActions`/`useBulkActions`) auto-invalidate `KEYS.aggregations()` so
|
|
257
|
+
* dashboards refresh after CRUD writes — opt out via the mutation's
|
|
258
|
+
* `invalidateQueries` override if you want stale-on-write.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const { data } = useAggregation<{ day: string; total: number }>({
|
|
262
|
+
* name: 'salesByDay',
|
|
263
|
+
* filter: { from: '2025-01-01' },
|
|
264
|
+
* refetchOnWindowFocus: false,
|
|
265
|
+
* });
|
|
266
|
+
* // data.rows: Array<{ day: string; total: number }>
|
|
267
|
+
*/
|
|
268
|
+
useAggregation: <TRow extends AggRow = AggRow, TData = AggResult<TRow>>(args: {
|
|
269
|
+
name: string;
|
|
270
|
+
filter?: Record<string, unknown>;
|
|
271
|
+
} & AggregationQueryOptions<TRow, TData>) => UseQueryResult<TData>;
|
|
208
272
|
useUpload: (options?: {
|
|
209
273
|
invalidateQueries?: QueryKey[];
|
|
210
274
|
messages?: MutationMessages;
|
|
@@ -269,4 +333,4 @@ declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>(
|
|
|
269
333
|
client
|
|
270
334
|
}: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
|
|
271
335
|
//#endregion
|
|
272
|
-
export { BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };
|
|
336
|
+
export { AggregationQueryOptions, BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };
|
package/dist/hooks.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { getAuthMode, getClientAuthContext } from "./client.js";
|
|
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, updateListCache } from "./cache.js";
|
|
6
|
-
import { useDetailQuery, useInfiniteListQuery, useListQuery } from "./query.js";
|
|
5
|
+
import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, syncDetailToLists, updateListCache } from "./cache.js";
|
|
6
|
+
import { findItemInListCache, useDetailQuery, useInfiniteListQuery, useListQuery } from "./query.js";
|
|
7
7
|
import { useMutationWithTransition, useOptimisticMutation } from "./mutation.js";
|
|
8
8
|
import { subscribeToEvents } from "./sse.js";
|
|
9
9
|
import { connectWs } from "./ws.js";
|
|
10
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
10
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
11
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
12
|
|
|
13
13
|
//#region src/hooks.ts
|
|
@@ -33,8 +33,19 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
33
33
|
const pluralName = plural ?? `${singular}s`;
|
|
34
34
|
/** Resolve auth context — per-client auth takes priority over global */
|
|
35
35
|
const resolveAuth = () => getClientAuthContext(client);
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Whether auth is provided via static config — no per-request token needed.
|
|
38
|
+
* Sources (in priority order):
|
|
39
|
+
* 1. Per-client `defaultHeaders` / `internalApiKey` (when `client` is passed in).
|
|
40
|
+
* 2. Per-client custom auth (e.g. `createClient({ getToken })`).
|
|
41
|
+
* 3. Global `configureClient({ internalApiKey | defaultHeaders | authMode: 'cookie' })`.
|
|
42
|
+
*
|
|
43
|
+
* Resolved lazily on every render so that an app calling `configureClient`
|
|
44
|
+
* inside a "use client" provider after factory creation still picks up the
|
|
45
|
+
* global static-auth signal — previously a global `internalApiKey` was
|
|
46
|
+
* ignored, leaving every protected query stuck in a permanently-disabled state.
|
|
47
|
+
*/
|
|
48
|
+
const resolveHasStaticAuth = () => !!(client?.config?.defaultHeaders || client?.config?.internalApiKey || client?.auth) || hasGlobalStaticAuth();
|
|
38
49
|
/** Extract ID from an item using configured idField, falling back to _id → id */
|
|
39
50
|
function resolveItemId(item) {
|
|
40
51
|
if (!item || typeof item !== "object") return null;
|
|
@@ -99,7 +110,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
99
110
|
...requestOpts
|
|
100
111
|
}
|
|
101
112
|
}),
|
|
102
|
-
enabled: createEnabledRule(token, queryOpts, resolveAuthMode(),
|
|
113
|
+
enabled: createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
|
|
103
114
|
options: {
|
|
104
115
|
staleTime: queryOpts.staleTime ?? config.staleTime,
|
|
105
116
|
gcTime: queryOpts.gcTime ?? config.gcTime,
|
|
@@ -108,9 +119,6 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
108
119
|
refetchInterval: queryOpts.refetchInterval,
|
|
109
120
|
refetchIntervalInBackground: queryOpts.refetchIntervalInBackground
|
|
110
121
|
},
|
|
111
|
-
prefillDetailCache: queryOpts.prefillDetailCache ?? true,
|
|
112
|
-
detailKeyBuilder: (id) => KEYS.scopedDetail(id, organizationId ?? null),
|
|
113
|
-
itemIdResolver: resolveItemId,
|
|
114
122
|
select: queryOpts.select
|
|
115
123
|
});
|
|
116
124
|
}
|
|
@@ -131,8 +139,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
131
139
|
}
|
|
132
140
|
const { organizationId, params: queryParams, request: requestOpts, ...restOptions } = options;
|
|
133
141
|
const detailKey = KEYS.scopedDetail(id || "", organizationId ?? null);
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
const fullDetailKey = queryParams ? [...detailKey, queryParams] : detailKey;
|
|
143
|
+
const queryClient = useQueryClient();
|
|
144
|
+
const listPlaceholder = useCallback(() => id ? findItemInListCache(queryClient, KEYS.lists(), id, idField) : void 0, [queryClient, id]);
|
|
145
|
+
const detailResult = useDetailQuery({
|
|
146
|
+
queryKey: fullDetailKey,
|
|
136
147
|
queryFn: ({ signal }) => api.getById({
|
|
137
148
|
id,
|
|
138
149
|
token,
|
|
@@ -143,7 +154,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
143
154
|
...requestOpts
|
|
144
155
|
}
|
|
145
156
|
}),
|
|
146
|
-
enabled: !!id && createEnabledRule(token, restOptions, resolveAuthMode(),
|
|
157
|
+
enabled: !!id && createEnabledRule(token, restOptions, resolveAuthMode(), resolveHasStaticAuth()),
|
|
147
158
|
options: {
|
|
148
159
|
staleTime: restOptions.staleTime ?? config.staleTime,
|
|
149
160
|
gcTime: restOptions.gcTime ?? config.gcTime,
|
|
@@ -152,8 +163,18 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
152
163
|
refetchInterval: restOptions.refetchInterval,
|
|
153
164
|
refetchIntervalInBackground: restOptions.refetchIntervalInBackground
|
|
154
165
|
},
|
|
155
|
-
select: restOptions.select
|
|
166
|
+
select: restOptions.select,
|
|
167
|
+
placeholderData: listPlaceholder
|
|
156
168
|
});
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!detailResult.item || detailResult.isPlaceholderData) return;
|
|
171
|
+
syncDetailToLists(queryClient, KEYS.lists(), detailResult.item, idField ? { idField } : {});
|
|
172
|
+
}, [
|
|
173
|
+
detailResult.item,
|
|
174
|
+
detailResult.isPlaceholderData,
|
|
175
|
+
queryClient
|
|
176
|
+
]);
|
|
177
|
+
return detailResult;
|
|
157
178
|
}
|
|
158
179
|
function useActions() {
|
|
159
180
|
const queryClient = useQueryClient();
|
|
@@ -166,7 +187,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
166
187
|
data
|
|
167
188
|
}),
|
|
168
189
|
queryClient,
|
|
169
|
-
queryKeys: [KEYS.lists()],
|
|
190
|
+
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
170
191
|
shouldToast,
|
|
171
192
|
optimisticUpdate: (oldData, { data }) => {
|
|
172
193
|
const optimisticItem = {
|
|
@@ -199,7 +220,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
199
220
|
data
|
|
200
221
|
}),
|
|
201
222
|
queryClient,
|
|
202
|
-
queryKeys: [
|
|
223
|
+
queryKeys: [
|
|
224
|
+
KEYS.lists(),
|
|
225
|
+
KEYS.details(),
|
|
226
|
+
KEYS.aggregations()
|
|
227
|
+
],
|
|
203
228
|
shouldToast,
|
|
204
229
|
optimisticUpdate: (oldData, { id, data }) => {
|
|
205
230
|
const updated = updateListCache(oldData, (arr) => (arr || []).map((item) => resolveItemId(item) === id ? {
|
|
@@ -250,7 +275,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
250
275
|
id
|
|
251
276
|
}),
|
|
252
277
|
queryClient,
|
|
253
|
-
queryKeys: [KEYS.lists()],
|
|
278
|
+
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
254
279
|
shouldToast,
|
|
255
280
|
optimisticUpdate: (oldData, { id }) => {
|
|
256
281
|
return updateListCache(oldData, (arr) => (arr || []).filter((item) => resolveItemId(item) !== id));
|
|
@@ -280,7 +305,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
280
305
|
id
|
|
281
306
|
});
|
|
282
307
|
},
|
|
283
|
-
invalidateQueries: [
|
|
308
|
+
invalidateQueries: [
|
|
309
|
+
KEYS.lists(),
|
|
310
|
+
KEYS.custom("deleted"),
|
|
311
|
+
KEYS.aggregations()
|
|
312
|
+
],
|
|
284
313
|
shouldToast,
|
|
285
314
|
messages: {
|
|
286
315
|
success: `${singular} restored successfully`,
|
|
@@ -405,7 +434,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
405
434
|
}
|
|
406
435
|
});
|
|
407
436
|
},
|
|
408
|
-
enabled: createEnabledRule(token, queryOpts, resolveAuthMode(),
|
|
437
|
+
enabled: createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
|
|
409
438
|
initialPageParam: restParams.after ? restParams.after : 1,
|
|
410
439
|
getNextPageParam: (lastPage) => {
|
|
411
440
|
const page = lastPage;
|
|
@@ -490,7 +519,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
490
519
|
}
|
|
491
520
|
});
|
|
492
521
|
},
|
|
493
|
-
enabled: !!api.getDeleted && createEnabledRule(token, queryOpts, resolveAuthMode(),
|
|
522
|
+
enabled: !!api.getDeleted && createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
|
|
494
523
|
options: {
|
|
495
524
|
staleTime: queryOpts.staleTime ?? config.staleTime,
|
|
496
525
|
gcTime: queryOpts.gcTime ?? config.gcTime
|
|
@@ -504,7 +533,9 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
504
533
|
const resolvedOptions = options ?? {};
|
|
505
534
|
const organizationId = resolvedOptions.organizationId ?? auth.organizationId;
|
|
506
535
|
const { params: queryParams, request: requestOpts, ...restOptions } = resolvedOptions;
|
|
507
|
-
|
|
536
|
+
const queryClient = useQueryClient();
|
|
537
|
+
const listPlaceholder = useCallback(() => slug ? findItemInListCache(queryClient, KEYS.lists(), slug, "slug") : void 0, [queryClient, slug]);
|
|
538
|
+
const slugResult = useDetailQuery({
|
|
508
539
|
queryKey: queryParams ? KEYS.custom("slug", slug, queryParams) : KEYS.custom("slug", slug),
|
|
509
540
|
queryFn: ({ signal }) => {
|
|
510
541
|
if (!api.getBySlug) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a getBySlug method`));
|
|
@@ -519,15 +550,25 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
519
550
|
}
|
|
520
551
|
});
|
|
521
552
|
},
|
|
522
|
-
enabled: !!api.getBySlug && !!slug && createEnabledRule(token, restOptions, resolveAuthMode(),
|
|
553
|
+
enabled: !!api.getBySlug && !!slug && createEnabledRule(token, restOptions, resolveAuthMode(), resolveHasStaticAuth()),
|
|
523
554
|
options: {
|
|
524
555
|
staleTime: restOptions.staleTime ?? config.staleTime,
|
|
525
556
|
gcTime: restOptions.gcTime ?? config.gcTime,
|
|
526
557
|
refetchOnWindowFocus: restOptions.refetchOnWindowFocus ?? config.refetchOnWindowFocus,
|
|
527
558
|
structuralSharing: restOptions.structuralSharing ?? config.structuralSharing
|
|
528
559
|
},
|
|
529
|
-
select: restOptions.select
|
|
560
|
+
select: restOptions.select,
|
|
561
|
+
placeholderData: listPlaceholder
|
|
530
562
|
});
|
|
563
|
+
useEffect(() => {
|
|
564
|
+
if (!slugResult.item || slugResult.isPlaceholderData) return;
|
|
565
|
+
syncDetailToLists(queryClient, KEYS.lists(), slugResult.item, idField ? { idField } : {});
|
|
566
|
+
}, [
|
|
567
|
+
slugResult.item,
|
|
568
|
+
slugResult.isPlaceholderData,
|
|
569
|
+
queryClient
|
|
570
|
+
]);
|
|
571
|
+
return slugResult;
|
|
531
572
|
}
|
|
532
573
|
function useTree(params, options) {
|
|
533
574
|
const auth = resolveAuth();
|
|
@@ -553,7 +594,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
553
594
|
}
|
|
554
595
|
});
|
|
555
596
|
},
|
|
556
|
-
enabled: !!api.getTree && createEnabledRule(token, queryOpts, resolveAuthMode(),
|
|
597
|
+
enabled: !!api.getTree && createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
|
|
557
598
|
options: {
|
|
558
599
|
staleTime: queryOpts.staleTime ?? config.staleTime,
|
|
559
600
|
gcTime: queryOpts.gcTime ?? config.gcTime
|
|
@@ -586,14 +627,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
586
627
|
}
|
|
587
628
|
});
|
|
588
629
|
},
|
|
589
|
-
enabled: !!api.getChildren && !!parentId && createEnabledRule(token, queryOpts, resolveAuthMode(),
|
|
630
|
+
enabled: !!api.getChildren && !!parentId && createEnabledRule(token, queryOpts, resolveAuthMode(), resolveHasStaticAuth()),
|
|
590
631
|
options: {
|
|
591
632
|
staleTime: queryOpts.staleTime ?? config.staleTime,
|
|
592
633
|
gcTime: queryOpts.gcTime ?? config.gcTime
|
|
593
634
|
},
|
|
594
|
-
prefillDetailCache: queryOpts.prefillDetailCache ?? true,
|
|
595
|
-
detailKeyBuilder: (id) => KEYS.scopedDetail(id, organizationId ?? null),
|
|
596
|
-
itemIdResolver: resolveItemId,
|
|
597
635
|
select: queryOpts.select
|
|
598
636
|
});
|
|
599
637
|
}
|
|
@@ -608,7 +646,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
608
646
|
data: vars.data
|
|
609
647
|
});
|
|
610
648
|
},
|
|
611
|
-
invalidateQueries: [KEYS.lists()],
|
|
649
|
+
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
612
650
|
messages: {
|
|
613
651
|
success: `${pluralName} created successfully`,
|
|
614
652
|
error: `Failed to create ${pluralName.toLowerCase()}`
|
|
@@ -626,7 +664,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
626
664
|
data: vars.data
|
|
627
665
|
});
|
|
628
666
|
},
|
|
629
|
-
invalidateQueries: [
|
|
667
|
+
invalidateQueries: [
|
|
668
|
+
KEYS.lists(),
|
|
669
|
+
KEYS.details(),
|
|
670
|
+
KEYS.aggregations()
|
|
671
|
+
],
|
|
630
672
|
messages: {
|
|
631
673
|
success: `${pluralName} updated successfully`,
|
|
632
674
|
error: `Failed to update ${pluralName.toLowerCase()}`
|
|
@@ -643,7 +685,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
643
685
|
filter: vars.filter
|
|
644
686
|
});
|
|
645
687
|
},
|
|
646
|
-
invalidateQueries: [KEYS.lists()],
|
|
688
|
+
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
647
689
|
messages: {
|
|
648
690
|
success: `${pluralName} deleted successfully`,
|
|
649
691
|
error: `Failed to delete ${pluralName.toLowerCase()}`
|
|
@@ -772,6 +814,38 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
772
814
|
toastHandler: instanceToast
|
|
773
815
|
});
|
|
774
816
|
}
|
|
817
|
+
function useAggregation(args) {
|
|
818
|
+
const { name, filter, public: isPublic, enabled = true, staleTime = config.staleTime, gcTime = config.gcTime, refetchOnWindowFocus = false, refetchInterval, refetchIntervalInBackground, select, request, placeholderData } = args;
|
|
819
|
+
const auth = resolveAuth();
|
|
820
|
+
const filterKey = auth.organizationId ? {
|
|
821
|
+
_org: auth.organizationId,
|
|
822
|
+
...filter ?? {}
|
|
823
|
+
} : filter ?? {};
|
|
824
|
+
return useQuery({
|
|
825
|
+
queryKey: KEYS.aggregation(name, filterKey),
|
|
826
|
+
queryFn: () => {
|
|
827
|
+
if (!api.aggregate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define an aggregate method (requires arc 2.13+)`));
|
|
828
|
+
return api.aggregate({
|
|
829
|
+
name,
|
|
830
|
+
filter,
|
|
831
|
+
token: auth.token,
|
|
832
|
+
organizationId: auth.organizationId,
|
|
833
|
+
...request ? { options: request } : {}
|
|
834
|
+
});
|
|
835
|
+
},
|
|
836
|
+
enabled: !!name && !!api.aggregate && createEnabledRule(auth.token, {
|
|
837
|
+
public: isPublic,
|
|
838
|
+
enabled
|
|
839
|
+
}, resolveAuthMode(), resolveHasStaticAuth()),
|
|
840
|
+
staleTime,
|
|
841
|
+
gcTime,
|
|
842
|
+
refetchOnWindowFocus,
|
|
843
|
+
...select ? { select } : {},
|
|
844
|
+
...refetchInterval !== void 0 ? { refetchInterval } : {},
|
|
845
|
+
...refetchIntervalInBackground !== void 0 ? { refetchIntervalInBackground } : {},
|
|
846
|
+
...placeholderData !== void 0 ? { placeholderData } : {}
|
|
847
|
+
});
|
|
848
|
+
}
|
|
775
849
|
/**
|
|
776
850
|
* Subscribe to live `<resource>.<operation>` broadcasts from arc and
|
|
777
851
|
* auto-invalidate this entity's TanStack Query cache.
|
|
@@ -813,6 +887,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
813
887
|
return raw != null ? String(raw) : void 0;
|
|
814
888
|
})() : void 0;
|
|
815
889
|
queryClient.invalidateQueries({ queryKey: KEYS.lists() });
|
|
890
|
+
queryClient.invalidateQueries({ queryKey: KEYS.aggregations() });
|
|
816
891
|
if (id && (operation === "updated" || operation === "deleted")) queryClient.invalidateQueries({ queryKey: KEYS.detail(id) });
|
|
817
892
|
onEventRef.current?.({
|
|
818
893
|
operation,
|
|
@@ -867,8 +942,8 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
867
942
|
const id = resolveItemId(item);
|
|
868
943
|
if (id) {
|
|
869
944
|
const orgId = resolveAuth().organizationId;
|
|
870
|
-
queryClient.setQueryData(KEYS.scopedDetail(id, orgId),
|
|
871
|
-
if (orgId) queryClient.setQueryData(KEYS.detail(id),
|
|
945
|
+
queryClient.setQueryData(KEYS.scopedDetail(id, orgId), item);
|
|
946
|
+
if (orgId) queryClient.setQueryData(KEYS.detail(id), item);
|
|
872
947
|
}
|
|
873
948
|
if (!router) return;
|
|
874
949
|
const { scroll = true, replace = false } = options;
|
|
@@ -894,6 +969,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
894
969
|
useSearchEngine,
|
|
895
970
|
useSearchSimilar,
|
|
896
971
|
useEmbed,
|
|
972
|
+
useAggregation,
|
|
897
973
|
useResourceSync,
|
|
898
974
|
useNavigation
|
|
899
975
|
};
|
package/dist/prefetch.d.ts
CHANGED
|
@@ -61,6 +61,20 @@ interface CrudPrefetcher {
|
|
|
61
61
|
* @example
|
|
62
62
|
* await productsPrefetcher.prefetchInfiniteList(queryClient, { limit: 20 });
|
|
63
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* Prefetch a declared aggregation (arc 2.13+). Uses the same query key as
|
|
66
|
+
* `useAggregation` so RSC-pre-rendered dashboard rows hydrate without a
|
|
67
|
+
* client refetch.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* await ordersPrefetcher.prefetchAggregation(
|
|
71
|
+
* queryClient,
|
|
72
|
+
* 'salesByDay',
|
|
73
|
+
* { from: '2025-01-01', to: '2025-12-31' },
|
|
74
|
+
* { token: jwt, organizationId: orgId, staleTime: 60_000 },
|
|
75
|
+
* );
|
|
76
|
+
*/
|
|
77
|
+
prefetchAggregation: (queryClient: QueryClient, name: string, filter?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
64
78
|
prefetchInfiniteList: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
65
79
|
}
|
|
66
80
|
/**
|
|
@@ -122,6 +136,15 @@ declare function createCrudPrefetcher(api: {
|
|
|
122
136
|
headerOptions?: Record<string, string>;
|
|
123
137
|
};
|
|
124
138
|
}) => Promise<unknown>;
|
|
139
|
+
aggregate?: (opts: {
|
|
140
|
+
name: string;
|
|
141
|
+
filter?: Record<string, unknown>;
|
|
142
|
+
token?: string | null;
|
|
143
|
+
organizationId?: string | null;
|
|
144
|
+
options?: {
|
|
145
|
+
headerOptions?: Record<string, string>;
|
|
146
|
+
};
|
|
147
|
+
}) => Promise<unknown>;
|
|
125
148
|
getTree?: (opts: {
|
|
126
149
|
params?: Record<string, unknown>;
|
|
127
150
|
token?: string | null;
|
package/dist/prefetch.js
CHANGED
|
@@ -100,6 +100,26 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
100
100
|
staleTime: options.staleTime
|
|
101
101
|
});
|
|
102
102
|
},
|
|
103
|
+
async prefetchAggregation(queryClient, name, filter, options = {}) {
|
|
104
|
+
if (!api.aggregate) throw new Error(`[arc-next] prefetchAggregation requires an api with aggregate (arc 2.13+)`);
|
|
105
|
+
if (!name) throw new Error("[arc-next] prefetchAggregation: aggregation name is required");
|
|
106
|
+
const orgId = options.organizationId ?? null;
|
|
107
|
+
const filterKey = orgId ? {
|
|
108
|
+
_org: orgId,
|
|
109
|
+
...filter ?? {}
|
|
110
|
+
} : filter ?? {};
|
|
111
|
+
await queryClient.prefetchQuery({
|
|
112
|
+
queryKey: KEYS.aggregation(name, filterKey),
|
|
113
|
+
queryFn: () => api.aggregate({
|
|
114
|
+
name,
|
|
115
|
+
filter,
|
|
116
|
+
token: options.token ?? null,
|
|
117
|
+
organizationId: orgId,
|
|
118
|
+
...options.headers ? { options: { headerOptions: options.headers } } : {}
|
|
119
|
+
}),
|
|
120
|
+
staleTime: options.staleTime
|
|
121
|
+
});
|
|
122
|
+
},
|
|
103
123
|
async prefetchTree(queryClient, params = {}, options = {}) {
|
|
104
124
|
if (!api.getTree) throw new Error(`[arc-next] prefetchTree requires an api with getTree (tree preset)`);
|
|
105
125
|
const { organizationId: paramOrgId, ...restParams } = params;
|
package/dist/presets/bulk.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { BaseApi,
|
|
1
|
+
import { BaseApi, ScopedArgs } from "../api.js";
|
|
2
|
+
import { BulkCreateResult, DeleteManyResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/bulk.d.ts
|
|
4
5
|
interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
|
|
5
6
|
/** Insert many docs in one round-trip. Backend mounts `POST /:resource/bulk`. */
|
|
6
7
|
bulkCreate(args: ScopedArgs & {
|
|
7
8
|
data: TCreate[];
|
|
8
|
-
}): Promise<
|
|
9
|
+
}): Promise<BulkCreateResult<TDoc>>;
|
|
9
10
|
/**
|
|
10
11
|
* Update all docs matching `filter` with `data`.
|
|
11
12
|
* Backend mounts `PATCH /:resource/bulk`.
|
|
@@ -13,14 +14,14 @@ interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
|
|
|
13
14
|
bulkUpdate(args: ScopedArgs & {
|
|
14
15
|
filter: Record<string, unknown>;
|
|
15
16
|
data: TUpdate;
|
|
16
|
-
}): Promise<
|
|
17
|
+
}): Promise<UpdateManyResult>;
|
|
17
18
|
/**
|
|
18
19
|
* Delete all docs matching `filter`.
|
|
19
20
|
* Backend mounts `DELETE /:resource/bulk`.
|
|
20
21
|
*/
|
|
21
22
|
bulkDelete(args: ScopedArgs & {
|
|
22
23
|
filter: Record<string, unknown>;
|
|
23
|
-
}): Promise<
|
|
24
|
+
}): Promise<DeleteManyResult>;
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Adds bulk preset methods to a BaseApi.
|
package/dist/presets/search.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, ScopedArgs } from "../api.js";
|
|
2
|
+
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/search.d.ts
|
|
4
5
|
interface SearchPresetMethods<TDoc> {
|
|
@@ -10,7 +11,7 @@ interface SearchPresetMethods<TDoc> {
|
|
|
10
11
|
/** Free-text query forwarded as `body.query`. */query?: string; /** Engine-specific options merged into the request body. */
|
|
11
12
|
body?: TBody; /** Override path (default `/search`). */
|
|
12
13
|
path?: string;
|
|
13
|
-
}): Promise<
|
|
14
|
+
}): Promise<TResult[] | PaginatedResult<TResult>>;
|
|
14
15
|
/**
|
|
15
16
|
* Vector / semantic similarity (Atlas, Pinecone, Qdrant...).
|
|
16
17
|
* Backend mounts `POST /:resource/search-similar`.
|
|
@@ -20,7 +21,7 @@ interface SearchPresetMethods<TDoc> {
|
|
|
20
21
|
vector?: number[]; /** Vector-engine options (`topK`, `filter`, `index`, ...). */
|
|
21
22
|
body?: TBody; /** Override path (default `/search-similar`). */
|
|
22
23
|
path?: string;
|
|
23
|
-
}): Promise<
|
|
24
|
+
}): Promise<TResult[]>;
|
|
24
25
|
/**
|
|
25
26
|
* Convert text/media to a vector embedding via the engine the resource is wired to.
|
|
26
27
|
* Backend mounts `POST /:resource/embed`.
|
|
@@ -29,7 +30,7 @@ interface SearchPresetMethods<TDoc> {
|
|
|
29
30
|
/** Text or array of texts to embed. */input: string | string[]; /** Embed-engine options (`model`, `dimensions`, ...). */
|
|
30
31
|
body?: Record<string, unknown>; /** Override path (default `/embed`). */
|
|
31
32
|
path?: string;
|
|
32
|
-
}): Promise<
|
|
33
|
+
}): Promise<number[] | number[][]>;
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* Adds search preset methods to a BaseApi.
|
package/dist/presets/slug.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, ScopedArgs } from "../api.js";
|
|
2
2
|
|
|
3
3
|
//#region src/presets/slug.d.ts
|
|
4
4
|
interface SlugLookupMethods<TDoc> {
|
|
@@ -9,7 +9,7 @@ interface SlugLookupMethods<TDoc> {
|
|
|
9
9
|
select?: string;
|
|
10
10
|
populate?: string | string[];
|
|
11
11
|
};
|
|
12
|
-
}): Promise<
|
|
12
|
+
}): Promise<TDoc>;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Adds slug-lookup preset methods to a BaseApi.
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, QueryParams, ScopedArgs } from "../api.js";
|
|
2
|
+
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/soft-delete.d.ts
|
|
4
5
|
interface SoftDeleteMethods<TDoc> {
|
|
5
6
|
/** List soft-deleted docs. Backend mounts `GET /:resource/deleted`. */
|
|
6
7
|
getDeleted(args?: ScopedArgs & {
|
|
7
8
|
params?: QueryParams;
|
|
8
|
-
}): Promise<
|
|
9
|
+
}): Promise<PaginatedResult<TDoc>>;
|
|
9
10
|
/** Undo a soft-delete. Backend mounts `POST /:resource/:id/restore`. */
|
|
10
11
|
restore(args: ScopedArgs & {
|
|
11
12
|
id: string;
|
|
12
|
-
}): Promise<
|
|
13
|
+
}): Promise<TDoc>;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Adds soft-delete preset methods to a BaseApi.
|
package/dist/presets/tree.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, QueryParams, ScopedArgs } from "../api.js";
|
|
2
|
+
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/tree.d.ts
|
|
4
5
|
interface TreeMethods<TDoc> {
|
|
5
6
|
/** Fetch the full hierarchy. Backend mounts `GET /:resource/tree`. */
|
|
6
7
|
getTree(args?: ScopedArgs & {
|
|
7
8
|
params?: QueryParams;
|
|
8
|
-
}): Promise<
|
|
9
|
+
}): Promise<TDoc[]>;
|
|
9
10
|
/** Fetch direct children of a node. Backend mounts `GET /:resource/:id/children`. */
|
|
10
11
|
getChildren(args: ScopedArgs & {
|
|
11
12
|
parentId: string;
|
|
12
13
|
params?: QueryParams;
|
|
13
|
-
}): Promise<
|
|
14
|
+
}): Promise<PaginatedResult<TDoc>>;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Adds tree preset methods to a BaseApi.
|