@classytic/arc-next 0.4.1 → 0.6.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 +272 -660
- package/dist/api.d.ts +115 -194
- package/dist/api.js +69 -116
- package/dist/cache.d.ts +125 -0
- package/dist/cache.js +211 -0
- package/dist/client.d.ts +295 -5
- package/dist/client.js +392 -12
- package/dist/hooks.d.ts +162 -22
- package/dist/hooks.js +249 -89
- package/dist/mutation.d.ts +18 -1
- package/dist/mutation.js +20 -1
- package/dist/prefetch.d.ts +36 -2
- package/dist/prefetch.js +47 -3
- package/dist/presets/bulk.d.ts +43 -0
- package/dist/presets/bulk.js +50 -0
- package/dist/presets/search.d.ts +56 -0
- package/dist/presets/search.js +60 -0
- package/dist/presets/slug.d.ts +28 -0
- package/dist/presets/slug.js +27 -0
- package/dist/presets/soft-delete.d.ts +34 -0
- package/dist/presets/soft-delete.js +45 -0
- package/dist/presets/tree.d.ts +32 -0
- package/dist/presets/tree.js +47 -0
- package/dist/query.d.ts +86 -69
- package/dist/query.js +59 -161
- package/dist/sse.d.ts +115 -36
- package/dist/sse.js +179 -103
- package/dist/upload.d.ts +181 -0
- package/dist/upload.js +346 -0
- package/dist/ws.d.ts +167 -0
- package/dist/ws.js +274 -0
- package/package.json +39 -1
package/dist/hooks.d.ts
CHANGED
|
@@ -1,30 +1,73 @@
|
|
|
1
1
|
import { ArcClient, UseRouterHook } from "./client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { AggResult, AggRow, BaseApi } from "./api.js";
|
|
3
|
+
import { CacheUtils, QueryKeys } from "./cache.js";
|
|
3
4
|
import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, RequestPassthrough } from "./query.js";
|
|
6
|
+
import { SoftDeleteMethods } from "./presets/soft-delete.js";
|
|
7
|
+
import { BulkMethods } from "./presets/bulk.js";
|
|
8
|
+
import { SlugLookupMethods } from "./presets/slug.js";
|
|
9
|
+
import { TreeMethods } from "./presets/tree.js";
|
|
10
|
+
import { SearchPresetMethods } from "./presets/search.js";
|
|
11
|
+
import { QueryKey, UseQueryResult } from "@tanstack/react-query";
|
|
6
12
|
|
|
7
13
|
//#region src/hooks.d.ts
|
|
8
14
|
/**
|
|
9
15
|
* CRUD API interface accepted by createCrudHooks.
|
|
10
16
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
17
|
+
* Always-on surface (CRUD + universal helpers) is derived from BaseApi via Pick
|
|
18
|
+
* so types stay in sync. Preset surfaces are optional intersections of the
|
|
19
|
+
* corresponding `XxxMethods` interfaces, so a `withSearchPreset(api)` result
|
|
20
|
+
* satisfies CrudApi & gets `searchEngine`/`searchSimilar`/`embed` typed without
|
|
21
|
+
* any cast — yet a vanilla `createCrudApi('todos')` instance has none of them
|
|
22
|
+
* in autocomplete unless you opt in.
|
|
14
23
|
*/
|
|
15
24
|
type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>, 'getAll' | 'getById' | 'create' | 'update' | 'delete'> & {
|
|
16
25
|
upload?: BaseApi<T, TCreate, TUpdate>['upload'];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
dispatchAction?: BaseApi<T, TCreate, TUpdate>['dispatchAction'];
|
|
27
|
+
invokeRoute?: BaseApi<T, TCreate, TUpdate>['invokeRoute']; /** Declared aggregations (arc 2.13+). Always available on BaseApi. */
|
|
28
|
+
aggregate?: BaseApi<T, TCreate, TUpdate>['aggregate'];
|
|
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
|
+
}
|
|
28
71
|
interface CrudHooksConfig<T, TCreate = Partial<T>, TUpdate = Partial<T>> {
|
|
29
72
|
api: CrudApi<T, TCreate, TUpdate>;
|
|
30
73
|
entityKey: string;
|
|
@@ -147,9 +190,85 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
147
190
|
useDetailBySlug: (slug: string | null, options?: DetailQueryOptions<T>) => DetailQueryResult<T>;
|
|
148
191
|
useTree: (params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
|
|
149
192
|
useChildren: (parentId: string | null, params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Mutation against arc's unified action router (`POST /:id/action`).
|
|
195
|
+
* Server discriminates on `body.action`. Use for state transitions
|
|
196
|
+
* (approve/cancel/dispatch) instead of bespoke routes.
|
|
197
|
+
*/
|
|
198
|
+
useAction: <TResult = T, TBody extends Record<string, unknown> = Record<string, unknown>>(options?: {
|
|
199
|
+
invalidateQueries?: QueryKey[]; /** Default action name. Can be overridden per-call via `mutate({ action })`. */
|
|
200
|
+
action?: string;
|
|
201
|
+
messages?: MutationMessages;
|
|
202
|
+
onSuccess?: (data: TResult, variables: {
|
|
203
|
+
id: string;
|
|
204
|
+
action: string;
|
|
205
|
+
data?: TBody;
|
|
206
|
+
}) => void;
|
|
207
|
+
onError?: (error: Error, variables: {
|
|
208
|
+
id: string;
|
|
209
|
+
action: string;
|
|
210
|
+
data?: TBody;
|
|
211
|
+
}) => void;
|
|
212
|
+
onSettled?: (data: TResult | undefined, error: Error | null, variables: {
|
|
213
|
+
id: string;
|
|
214
|
+
action: string;
|
|
215
|
+
data?: TBody;
|
|
216
|
+
}) => void;
|
|
217
|
+
}) => TransitionMutationReturn<TResult, {
|
|
218
|
+
id: string;
|
|
219
|
+
action?: string;
|
|
220
|
+
data?: TBody;
|
|
221
|
+
}>;
|
|
222
|
+
/** Mutation against the search-preset POST `/search` route. */
|
|
223
|
+
useSearchEngine: <TResult = T, TBody extends Record<string, unknown> = Record<string, unknown>>(options?: {
|
|
224
|
+
path?: string;
|
|
225
|
+
messages?: MutationMessages;
|
|
226
|
+
invalidateQueries?: QueryKey[];
|
|
227
|
+
}) => TransitionMutationReturn<unknown, {
|
|
228
|
+
query?: string;
|
|
229
|
+
body?: TBody;
|
|
230
|
+
}>;
|
|
231
|
+
/** Mutation against the search-preset POST `/search-similar` route. */
|
|
232
|
+
useSearchSimilar: <TResult = T, TBody extends Record<string, unknown> = Record<string, unknown>>(options?: {
|
|
233
|
+
path?: string;
|
|
234
|
+
messages?: MutationMessages;
|
|
235
|
+
invalidateQueries?: QueryKey[];
|
|
236
|
+
}) => TransitionMutationReturn<unknown, {
|
|
237
|
+
query?: string;
|
|
238
|
+
vector?: number[];
|
|
239
|
+
body?: TBody;
|
|
240
|
+
}>;
|
|
241
|
+
/** Mutation against the search-preset POST `/embed` route. */
|
|
242
|
+
useEmbed: (options?: {
|
|
243
|
+
path?: string;
|
|
244
|
+
messages?: MutationMessages;
|
|
245
|
+
invalidateQueries?: QueryKey[];
|
|
246
|
+
}) => TransitionMutationReturn<unknown, {
|
|
247
|
+
input: string | string[];
|
|
248
|
+
body?: Record<string, unknown>;
|
|
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>;
|
|
153
272
|
useUpload: (options?: {
|
|
154
273
|
invalidateQueries?: QueryKey[];
|
|
155
274
|
messages?: MutationMessages;
|
|
@@ -161,7 +280,6 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
161
280
|
id?: string;
|
|
162
281
|
path?: string;
|
|
163
282
|
}>;
|
|
164
|
-
useSearch: (query: string, params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
|
|
165
283
|
useCustomMutation: <TData = unknown, TVariables = unknown>(config: {
|
|
166
284
|
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
167
285
|
invalidateQueries?: QueryKey[];
|
|
@@ -170,6 +288,28 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
170
288
|
onError?: (error: Error, variables: TVariables) => void;
|
|
171
289
|
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
|
|
172
290
|
}) => TransitionMutationReturn<TData, TVariables>;
|
|
291
|
+
/**
|
|
292
|
+
* Subscribe to live CRUD broadcasts and auto-invalidate this resource's
|
|
293
|
+
* cache. Drop in alongside `createCrudHooks` and every `useList` /
|
|
294
|
+
* `useDetail` rerenders when the backend emits `<resource>.<op>`.
|
|
295
|
+
*
|
|
296
|
+
* `source: 'ws'` uses arc's `websocketPlugin` (`/ws`); `source: 'sse'` uses
|
|
297
|
+
* `ssePlugin` (`/events/stream`). Pass `enabled: false` to opt out.
|
|
298
|
+
*/
|
|
299
|
+
useResourceSync: (options?: {
|
|
300
|
+
source?: 'ws' | 'sse'; /** Override resource name. Defaults to the factory's `entityKey`. */
|
|
301
|
+
resource?: string; /** Override path (default: `/ws` or `/events/stream`). */
|
|
302
|
+
path?: string; /** Whether the connection is active. Default: true. */
|
|
303
|
+
enabled?: boolean; /** Per-event hook fired AFTER cache invalidation. */
|
|
304
|
+
onEvent?: (event: {
|
|
305
|
+
operation: 'created' | 'updated' | 'deleted';
|
|
306
|
+
id?: string;
|
|
307
|
+
data: unknown;
|
|
308
|
+
}) => void; /** Connection-state listener. */
|
|
309
|
+
onConnectionChange?: (connected: boolean) => void;
|
|
310
|
+
}) => {
|
|
311
|
+
isConnected: boolean;
|
|
312
|
+
};
|
|
173
313
|
useNavigation: () => NavigateFn<T>;
|
|
174
314
|
}
|
|
175
315
|
/**
|
|
@@ -193,4 +333,4 @@ declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>(
|
|
|
193
333
|
client
|
|
194
334
|
}: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
|
|
195
335
|
//#endregion
|
|
196
|
-
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
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { getAuthMode, getClientAuthContext } from "./client.js";
|
|
4
4
|
import { isKeysetPagination, isOffsetPagination } from "./api.js";
|
|
5
|
-
import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, updateListCache
|
|
5
|
+
import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, updateListCache } from "./cache.js";
|
|
6
|
+
import { useDetailQuery, useInfiniteListQuery, useListQuery } from "./query.js";
|
|
6
7
|
import { useMutationWithTransition, useOptimisticMutation } from "./mutation.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { subscribeToEvents } from "./sse.js";
|
|
9
|
+
import { connectWs } from "./ws.js";
|
|
10
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
12
|
|
|
10
13
|
//#region src/hooks.ts
|
|
11
14
|
let useRouterHook = null;
|
|
@@ -163,7 +166,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
163
166
|
data
|
|
164
167
|
}),
|
|
165
168
|
queryClient,
|
|
166
|
-
queryKeys: [KEYS.lists()],
|
|
169
|
+
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
167
170
|
shouldToast,
|
|
168
171
|
optimisticUpdate: (oldData, { data }) => {
|
|
169
172
|
const optimisticItem = {
|
|
@@ -196,7 +199,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
196
199
|
data
|
|
197
200
|
}),
|
|
198
201
|
queryClient,
|
|
199
|
-
queryKeys: [
|
|
202
|
+
queryKeys: [
|
|
203
|
+
KEYS.lists(),
|
|
204
|
+
KEYS.details(),
|
|
205
|
+
KEYS.aggregations()
|
|
206
|
+
],
|
|
200
207
|
shouldToast,
|
|
201
208
|
optimisticUpdate: (oldData, { id, data }) => {
|
|
202
209
|
const updated = updateListCache(oldData, (arr) => (arr || []).map((item) => resolveItemId(item) === id ? {
|
|
@@ -247,7 +254,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
247
254
|
id
|
|
248
255
|
}),
|
|
249
256
|
queryClient,
|
|
250
|
-
queryKeys: [KEYS.lists()],
|
|
257
|
+
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
251
258
|
shouldToast,
|
|
252
259
|
optimisticUpdate: (oldData, { id }) => {
|
|
253
260
|
return updateListCache(oldData, (arr) => (arr || []).filter((item) => resolveItemId(item) !== id));
|
|
@@ -277,7 +284,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
277
284
|
id
|
|
278
285
|
});
|
|
279
286
|
},
|
|
280
|
-
invalidateQueries: [
|
|
287
|
+
invalidateQueries: [
|
|
288
|
+
KEYS.lists(),
|
|
289
|
+
KEYS.custom("deleted"),
|
|
290
|
+
KEYS.aggregations()
|
|
291
|
+
],
|
|
281
292
|
shouldToast,
|
|
282
293
|
messages: {
|
|
283
294
|
success: `${singular} restored successfully`,
|
|
@@ -452,46 +463,6 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
452
463
|
toastHandler: instanceToast
|
|
453
464
|
});
|
|
454
465
|
}
|
|
455
|
-
function useSearch(query, params, options) {
|
|
456
|
-
const auth = resolveAuth();
|
|
457
|
-
const token = params?.token ?? auth.token;
|
|
458
|
-
const organizationId = params?.organizationId ?? auth.organizationId;
|
|
459
|
-
const { organizationId: _, token: _t, ...restParams } = params ?? {};
|
|
460
|
-
const searchParams = {
|
|
461
|
-
q: query,
|
|
462
|
-
...restParams
|
|
463
|
-
};
|
|
464
|
-
const { request: requestOpts, ...queryOpts } = options ?? {};
|
|
465
|
-
const searchKeyParams = organizationId ? {
|
|
466
|
-
organizationId,
|
|
467
|
-
...searchParams
|
|
468
|
-
} : searchParams;
|
|
469
|
-
return useListQuery({
|
|
470
|
-
queryKey: [
|
|
471
|
-
...KEYS.lists(),
|
|
472
|
-
"search",
|
|
473
|
-
searchKeyParams
|
|
474
|
-
],
|
|
475
|
-
queryFn: ({ signal }) => {
|
|
476
|
-
if (!api.search) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a search method`));
|
|
477
|
-
return api.search({
|
|
478
|
-
token,
|
|
479
|
-
organizationId,
|
|
480
|
-
params: searchParams,
|
|
481
|
-
options: {
|
|
482
|
-
signal,
|
|
483
|
-
...requestOpts
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
},
|
|
487
|
-
enabled: !!api.search && query.length > 0 && createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
|
|
488
|
-
options: {
|
|
489
|
-
staleTime: queryOpts.staleTime ?? config.staleTime,
|
|
490
|
-
gcTime: queryOpts.gcTime ?? config.gcTime
|
|
491
|
-
},
|
|
492
|
-
select: queryOpts.select
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
466
|
function useCustomMutation(mutationConfig) {
|
|
496
467
|
return useMutationWithTransition({
|
|
497
468
|
mutationFn: mutationConfig.mutationFn,
|
|
@@ -634,43 +605,6 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
634
605
|
select: queryOpts.select
|
|
635
606
|
});
|
|
636
607
|
}
|
|
637
|
-
function useFindBy(field, value, options) {
|
|
638
|
-
const auth = resolveAuth();
|
|
639
|
-
const token = auth.token;
|
|
640
|
-
const organizationId = auth.organizationId;
|
|
641
|
-
const { operator, request: requestOpts, ...queryOpts } = options ?? {};
|
|
642
|
-
return useListQuery({
|
|
643
|
-
queryKey: KEYS.custom("findBy", {
|
|
644
|
-
field,
|
|
645
|
-
value,
|
|
646
|
-
operator,
|
|
647
|
-
organizationId
|
|
648
|
-
}),
|
|
649
|
-
queryFn: ({ signal }) => {
|
|
650
|
-
if (!api.findBy) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a findBy method`));
|
|
651
|
-
return api.findBy({
|
|
652
|
-
token,
|
|
653
|
-
organizationId,
|
|
654
|
-
field,
|
|
655
|
-
value,
|
|
656
|
-
operator,
|
|
657
|
-
options: {
|
|
658
|
-
signal,
|
|
659
|
-
...requestOpts
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
},
|
|
663
|
-
enabled: !!api.findBy && value !== void 0 && value !== null && createEnabledRule(token, queryOpts, resolveAuthMode(), hasStaticAuth),
|
|
664
|
-
options: {
|
|
665
|
-
staleTime: queryOpts.staleTime ?? config.staleTime,
|
|
666
|
-
gcTime: queryOpts.gcTime ?? config.gcTime
|
|
667
|
-
},
|
|
668
|
-
prefillDetailCache: queryOpts.prefillDetailCache ?? true,
|
|
669
|
-
detailKeyBuilder: (id) => KEYS.scopedDetail(id, organizationId ?? null),
|
|
670
|
-
itemIdResolver: resolveItemId,
|
|
671
|
-
select: queryOpts.select
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
608
|
function useBulkActions() {
|
|
675
609
|
const bulkCreateMutation = useMutationWithTransition({
|
|
676
610
|
mutationFn: (vars) => {
|
|
@@ -682,7 +616,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
682
616
|
data: vars.data
|
|
683
617
|
});
|
|
684
618
|
},
|
|
685
|
-
invalidateQueries: [KEYS.lists()],
|
|
619
|
+
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
686
620
|
messages: {
|
|
687
621
|
success: `${pluralName} created successfully`,
|
|
688
622
|
error: `Failed to create ${pluralName.toLowerCase()}`
|
|
@@ -700,7 +634,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
700
634
|
data: vars.data
|
|
701
635
|
});
|
|
702
636
|
},
|
|
703
|
-
invalidateQueries: [
|
|
637
|
+
invalidateQueries: [
|
|
638
|
+
KEYS.lists(),
|
|
639
|
+
KEYS.details(),
|
|
640
|
+
KEYS.aggregations()
|
|
641
|
+
],
|
|
704
642
|
messages: {
|
|
705
643
|
success: `${pluralName} updated successfully`,
|
|
706
644
|
error: `Failed to update ${pluralName.toLowerCase()}`
|
|
@@ -717,7 +655,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
717
655
|
filter: vars.filter
|
|
718
656
|
});
|
|
719
657
|
},
|
|
720
|
-
invalidateQueries: [KEYS.lists()],
|
|
658
|
+
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
721
659
|
messages: {
|
|
722
660
|
success: `${pluralName} deleted successfully`,
|
|
723
661
|
error: `Failed to delete ${pluralName.toLowerCase()}`
|
|
@@ -745,6 +683,224 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
745
683
|
isBulkDeleting: bulkDeleteMutation.isPending
|
|
746
684
|
};
|
|
747
685
|
}
|
|
686
|
+
function useAction(options) {
|
|
687
|
+
const queryClient = useQueryClient();
|
|
688
|
+
return useMutationWithTransition({
|
|
689
|
+
mutationFn: (vars) => {
|
|
690
|
+
if (!api.dispatchAction) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a dispatchAction method`));
|
|
691
|
+
const action = vars.action ?? options?.action;
|
|
692
|
+
if (!action) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] useAction: action name required (pass via mutate({ action }) or factory options)`));
|
|
693
|
+
const auth = resolveAuth();
|
|
694
|
+
return api.dispatchAction({
|
|
695
|
+
token: auth.token,
|
|
696
|
+
organizationId: auth.organizationId,
|
|
697
|
+
id: vars.id,
|
|
698
|
+
action,
|
|
699
|
+
data: vars.data
|
|
700
|
+
});
|
|
701
|
+
},
|
|
702
|
+
invalidateQueries: options?.invalidateQueries ?? [KEYS.lists(), KEYS.details()],
|
|
703
|
+
onSuccess: (data, vars) => {
|
|
704
|
+
const action = vars.action ?? options?.action ?? "";
|
|
705
|
+
if (vars.id) queryClient.invalidateQueries({ queryKey: KEYS.detail(vars.id) });
|
|
706
|
+
options?.onSuccess?.(data, {
|
|
707
|
+
id: vars.id,
|
|
708
|
+
action,
|
|
709
|
+
data: vars.data
|
|
710
|
+
});
|
|
711
|
+
},
|
|
712
|
+
onError: (error, vars) => {
|
|
713
|
+
const action = vars.action ?? options?.action ?? "";
|
|
714
|
+
options?.onError?.(error, {
|
|
715
|
+
id: vars.id,
|
|
716
|
+
action,
|
|
717
|
+
data: vars.data
|
|
718
|
+
});
|
|
719
|
+
},
|
|
720
|
+
onSettled: (data, error, vars) => {
|
|
721
|
+
const action = vars.action ?? options?.action ?? "";
|
|
722
|
+
options?.onSettled?.(data, error, {
|
|
723
|
+
id: vars.id,
|
|
724
|
+
action,
|
|
725
|
+
data: vars.data
|
|
726
|
+
});
|
|
727
|
+
},
|
|
728
|
+
messages: options?.messages,
|
|
729
|
+
toastHandler: instanceToast
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function useSearchEngine(options) {
|
|
733
|
+
return useMutationWithTransition({
|
|
734
|
+
mutationFn: (vars) => {
|
|
735
|
+
if (!api.searchEngine) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a searchEngine method`));
|
|
736
|
+
const auth = resolveAuth();
|
|
737
|
+
return api.searchEngine({
|
|
738
|
+
token: auth.token,
|
|
739
|
+
organizationId: auth.organizationId,
|
|
740
|
+
query: vars.query,
|
|
741
|
+
body: vars.body,
|
|
742
|
+
path: options?.path
|
|
743
|
+
});
|
|
744
|
+
},
|
|
745
|
+
invalidateQueries: options?.invalidateQueries ?? [],
|
|
746
|
+
messages: options?.messages,
|
|
747
|
+
toastHandler: instanceToast
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function useSearchSimilar(options) {
|
|
751
|
+
return useMutationWithTransition({
|
|
752
|
+
mutationFn: (vars) => {
|
|
753
|
+
if (!api.searchSimilar) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a searchSimilar method`));
|
|
754
|
+
const auth = resolveAuth();
|
|
755
|
+
return api.searchSimilar({
|
|
756
|
+
token: auth.token,
|
|
757
|
+
organizationId: auth.organizationId,
|
|
758
|
+
query: vars.query,
|
|
759
|
+
vector: vars.vector,
|
|
760
|
+
body: vars.body,
|
|
761
|
+
path: options?.path
|
|
762
|
+
});
|
|
763
|
+
},
|
|
764
|
+
invalidateQueries: options?.invalidateQueries ?? [],
|
|
765
|
+
messages: options?.messages,
|
|
766
|
+
toastHandler: instanceToast
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function useEmbed(options) {
|
|
770
|
+
return useMutationWithTransition({
|
|
771
|
+
mutationFn: (vars) => {
|
|
772
|
+
if (!api.embed) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define an embed method`));
|
|
773
|
+
const auth = resolveAuth();
|
|
774
|
+
return api.embed({
|
|
775
|
+
token: auth.token,
|
|
776
|
+
organizationId: auth.organizationId,
|
|
777
|
+
input: vars.input,
|
|
778
|
+
body: vars.body,
|
|
779
|
+
path: options?.path
|
|
780
|
+
});
|
|
781
|
+
},
|
|
782
|
+
invalidateQueries: options?.invalidateQueries ?? [],
|
|
783
|
+
messages: options?.messages,
|
|
784
|
+
toastHandler: instanceToast
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
function useAggregation(args) {
|
|
788
|
+
const { name, filter, public: isPublic, enabled = true, staleTime = config.staleTime, gcTime = config.gcTime, refetchOnWindowFocus = false, refetchInterval, refetchIntervalInBackground, select, request, placeholderData } = args;
|
|
789
|
+
const auth = resolveAuth();
|
|
790
|
+
const filterKey = auth.organizationId ? {
|
|
791
|
+
_org: auth.organizationId,
|
|
792
|
+
...filter ?? {}
|
|
793
|
+
} : filter ?? {};
|
|
794
|
+
return useQuery({
|
|
795
|
+
queryKey: KEYS.aggregation(name, filterKey),
|
|
796
|
+
queryFn: () => {
|
|
797
|
+
if (!api.aggregate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define an aggregate method (requires arc 2.13+)`));
|
|
798
|
+
return api.aggregate({
|
|
799
|
+
name,
|
|
800
|
+
filter,
|
|
801
|
+
token: auth.token,
|
|
802
|
+
organizationId: auth.organizationId,
|
|
803
|
+
...request ? { options: request } : {}
|
|
804
|
+
});
|
|
805
|
+
},
|
|
806
|
+
enabled: !!name && !!api.aggregate && createEnabledRule(auth.token, {
|
|
807
|
+
public: isPublic,
|
|
808
|
+
enabled
|
|
809
|
+
}, resolveAuthMode(), hasStaticAuth),
|
|
810
|
+
staleTime,
|
|
811
|
+
gcTime,
|
|
812
|
+
refetchOnWindowFocus,
|
|
813
|
+
...select ? { select } : {},
|
|
814
|
+
...refetchInterval !== void 0 ? { refetchInterval } : {},
|
|
815
|
+
...refetchIntervalInBackground !== void 0 ? { refetchIntervalInBackground } : {},
|
|
816
|
+
...placeholderData !== void 0 ? { placeholderData } : {}
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Subscribe to live `<resource>.<operation>` broadcasts from arc and
|
|
821
|
+
* auto-invalidate this entity's TanStack Query cache.
|
|
822
|
+
*
|
|
823
|
+
* - `source: 'ws'` (default) → arc's `websocketPlugin` at `/ws`. Sends a
|
|
824
|
+
* `{ type: 'subscribe', resource }` handshake on connect.
|
|
825
|
+
* - `source: 'sse'` → arc's `ssePlugin` at `/events/stream`. Auto-derives
|
|
826
|
+
* patterns from the resource name.
|
|
827
|
+
*
|
|
828
|
+
* Both transports invalidate `KEYS.lists()` on `<resource>.created` and
|
|
829
|
+
* `<resource>.deleted`, and `KEYS.detail(id)` (prefix-matches scoped /
|
|
830
|
+
* parameterized variants) on `<resource>.updated` / `.deleted`.
|
|
831
|
+
*/
|
|
832
|
+
function useResourceSync(options) {
|
|
833
|
+
const queryClient = useQueryClient();
|
|
834
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
835
|
+
const source = options?.source ?? "ws";
|
|
836
|
+
const resource = options?.resource ?? entityKey;
|
|
837
|
+
const enabled = options?.enabled ?? true;
|
|
838
|
+
const path = options?.path;
|
|
839
|
+
const onEventRef = useRef(options?.onEvent);
|
|
840
|
+
onEventRef.current = options?.onEvent;
|
|
841
|
+
const onConnRef = useRef(options?.onConnectionChange);
|
|
842
|
+
onConnRef.current = options?.onConnectionChange;
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
if (!enabled) return;
|
|
845
|
+
const handleBroadcast = (incomingType, payload) => {
|
|
846
|
+
const dot = incomingType.lastIndexOf(".");
|
|
847
|
+
const operation = dot >= 0 ? incomingType.slice(dot + 1) : incomingType;
|
|
848
|
+
if (operation !== "created" && operation !== "updated" && operation !== "deleted") return;
|
|
849
|
+
let doc = payload;
|
|
850
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload) && "data" in payload) {
|
|
851
|
+
const inner = payload.data;
|
|
852
|
+
if (inner !== void 0) doc = inner;
|
|
853
|
+
}
|
|
854
|
+
const id = typeof doc === "object" && doc !== null ? (() => {
|
|
855
|
+
const o = doc;
|
|
856
|
+
const raw = idField ? o[idField] : o._id ?? o.id;
|
|
857
|
+
return raw != null ? String(raw) : void 0;
|
|
858
|
+
})() : void 0;
|
|
859
|
+
queryClient.invalidateQueries({ queryKey: KEYS.lists() });
|
|
860
|
+
queryClient.invalidateQueries({ queryKey: KEYS.aggregations() });
|
|
861
|
+
if (id && (operation === "updated" || operation === "deleted")) queryClient.invalidateQueries({ queryKey: KEYS.detail(id) });
|
|
862
|
+
onEventRef.current?.({
|
|
863
|
+
operation,
|
|
864
|
+
id,
|
|
865
|
+
data: doc
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
if (source === "sse") {
|
|
869
|
+
const handle = subscribeToEvents({
|
|
870
|
+
resource,
|
|
871
|
+
path,
|
|
872
|
+
onConnectionChange: (c) => {
|
|
873
|
+
setIsConnected(c);
|
|
874
|
+
onConnRef.current?.(c);
|
|
875
|
+
},
|
|
876
|
+
onEvent: (event) => {
|
|
877
|
+
handleBroadcast(event.type, event.data);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
return () => handle.close();
|
|
881
|
+
}
|
|
882
|
+
const handle = connectWs({
|
|
883
|
+
path,
|
|
884
|
+
subscribe: [resource],
|
|
885
|
+
patterns: [`${resource}.`],
|
|
886
|
+
onConnectionChange: (c) => {
|
|
887
|
+
setIsConnected(c);
|
|
888
|
+
onConnRef.current?.(c);
|
|
889
|
+
},
|
|
890
|
+
onMessage: (message) => {
|
|
891
|
+
handleBroadcast(message.type, message.data);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
return () => handle.close();
|
|
895
|
+
}, [
|
|
896
|
+
enabled,
|
|
897
|
+
source,
|
|
898
|
+
resource,
|
|
899
|
+
path,
|
|
900
|
+
queryClient
|
|
901
|
+
]);
|
|
902
|
+
return { isConnected };
|
|
903
|
+
}
|
|
748
904
|
const resolvedRouterHook = instanceNavigation ?? useRouterHook ?? (() => ({
|
|
749
905
|
push: () => {},
|
|
750
906
|
replace: () => {}
|
|
@@ -777,10 +933,14 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
777
933
|
useDetailBySlug,
|
|
778
934
|
useTree,
|
|
779
935
|
useChildren,
|
|
780
|
-
useFindBy,
|
|
781
936
|
useUpload,
|
|
782
|
-
useSearch,
|
|
783
937
|
useCustomMutation,
|
|
938
|
+
useAction,
|
|
939
|
+
useSearchEngine,
|
|
940
|
+
useSearchSimilar,
|
|
941
|
+
useEmbed,
|
|
942
|
+
useAggregation,
|
|
943
|
+
useResourceSync,
|
|
784
944
|
useNavigation
|
|
785
945
|
};
|
|
786
946
|
}
|
package/dist/mutation.d.ts
CHANGED
|
@@ -33,6 +33,23 @@ interface TransitionMutationReturn<TData, TVariables> {
|
|
|
33
33
|
* configureToast({ success: toast.success, error: toast.error });
|
|
34
34
|
*/
|
|
35
35
|
declare function configureToast(handler: ToastHandler): void;
|
|
36
|
+
/**
|
|
37
|
+
* Get the configured toast handler. Returns the console-based default when
|
|
38
|
+
* no handler has been configured yet.
|
|
39
|
+
*
|
|
40
|
+
* Use this when domain code outside the react-query lifecycle needs to fire
|
|
41
|
+
* ad-hoc success/error toasts using the same handler the SDK uses internally
|
|
42
|
+
* — avoids the need for consumer SDKs to keep a parallel cache of the handler.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // In a domain helper outside any mutation lifecycle:
|
|
46
|
+
* import { getToastHandler } from '@classytic/arc-next/mutation';
|
|
47
|
+
*
|
|
48
|
+
* function notifySaved(label: string) {
|
|
49
|
+
* getToastHandler().success(`${label} saved`);
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
declare function getToastHandler(): ToastHandler;
|
|
36
53
|
interface TransitionMutationConfig<TData, TVariables> {
|
|
37
54
|
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
38
55
|
invalidateQueries?: QueryKey[];
|
|
@@ -107,4 +124,4 @@ declare function useOptimisticMutation<TData, TVariables>(config: CreateOptimist
|
|
|
107
124
|
}[];
|
|
108
125
|
}>;
|
|
109
126
|
//#endregion
|
|
110
|
-
export { CreateOptimisticMutationConfig, MutationCallbacks, MutationMessages, OptimisticMutationConfig, TransitionMutationConfig, TransitionMutationReturn, configureToast, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
|
|
127
|
+
export { CreateOptimisticMutationConfig, MutationCallbacks, MutationMessages, OptimisticMutationConfig, TransitionMutationConfig, TransitionMutationReturn, configureToast, getToastHandler, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
|