@classytic/arc-next 0.4.1 → 0.5.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 +271 -661
- package/dist/api.d.ts +64 -127
- package/dist/api.js +50 -116
- package/dist/cache.d.ts +108 -0
- package/dist/cache.js +197 -0
- package/dist/client.d.ts +298 -3
- package/dist/client.js +357 -10
- package/dist/hooks.d.ts +96 -20
- package/dist/hooks.js +195 -81
- package/dist/mutation.d.ts +18 -1
- package/dist/mutation.js +20 -1
- package/dist/prefetch.d.ts +13 -2
- package/dist/prefetch.js +27 -3
- package/dist/presets/bulk.d.ts +42 -0
- package/dist/presets/bulk.js +50 -0
- package/dist/presets/search.d.ts +55 -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 +33 -0
- package/dist/presets/soft-delete.js +45 -0
- package/dist/presets/tree.d.ts +31 -0
- package/dist/presets/tree.js +47 -0
- package/dist/query.d.ts +86 -69
- package/dist/query.js +69 -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 +37 -1
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";
|
|
8
|
+
import { subscribeToEvents } from "./sse.js";
|
|
9
|
+
import { connectWs } from "./ws.js";
|
|
7
10
|
import { useQueryClient } from "@tanstack/react-query";
|
|
8
|
-
import { useCallback, useRef } from "react";
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
12
|
|
|
10
13
|
//#region src/hooks.ts
|
|
11
14
|
let useRouterHook = null;
|
|
@@ -452,46 +455,6 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
452
455
|
toastHandler: instanceToast
|
|
453
456
|
});
|
|
454
457
|
}
|
|
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
458
|
function useCustomMutation(mutationConfig) {
|
|
496
459
|
return useMutationWithTransition({
|
|
497
460
|
mutationFn: mutationConfig.mutationFn,
|
|
@@ -634,43 +597,6 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
634
597
|
select: queryOpts.select
|
|
635
598
|
});
|
|
636
599
|
}
|
|
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
600
|
function useBulkActions() {
|
|
675
601
|
const bulkCreateMutation = useMutationWithTransition({
|
|
676
602
|
mutationFn: (vars) => {
|
|
@@ -745,6 +671,191 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
745
671
|
isBulkDeleting: bulkDeleteMutation.isPending
|
|
746
672
|
};
|
|
747
673
|
}
|
|
674
|
+
function useAction(options) {
|
|
675
|
+
const queryClient = useQueryClient();
|
|
676
|
+
return useMutationWithTransition({
|
|
677
|
+
mutationFn: (vars) => {
|
|
678
|
+
if (!api.dispatchAction) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a dispatchAction method`));
|
|
679
|
+
const action = vars.action ?? options?.action;
|
|
680
|
+
if (!action) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] useAction: action name required (pass via mutate({ action }) or factory options)`));
|
|
681
|
+
const auth = resolveAuth();
|
|
682
|
+
return api.dispatchAction({
|
|
683
|
+
token: auth.token,
|
|
684
|
+
organizationId: auth.organizationId,
|
|
685
|
+
id: vars.id,
|
|
686
|
+
action,
|
|
687
|
+
data: vars.data
|
|
688
|
+
});
|
|
689
|
+
},
|
|
690
|
+
invalidateQueries: options?.invalidateQueries ?? [KEYS.lists(), KEYS.details()],
|
|
691
|
+
onSuccess: (data, vars) => {
|
|
692
|
+
const action = vars.action ?? options?.action ?? "";
|
|
693
|
+
if (vars.id) queryClient.invalidateQueries({ queryKey: KEYS.detail(vars.id) });
|
|
694
|
+
options?.onSuccess?.(data, {
|
|
695
|
+
id: vars.id,
|
|
696
|
+
action,
|
|
697
|
+
data: vars.data
|
|
698
|
+
});
|
|
699
|
+
},
|
|
700
|
+
onError: (error, vars) => {
|
|
701
|
+
const action = vars.action ?? options?.action ?? "";
|
|
702
|
+
options?.onError?.(error, {
|
|
703
|
+
id: vars.id,
|
|
704
|
+
action,
|
|
705
|
+
data: vars.data
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
onSettled: (data, error, vars) => {
|
|
709
|
+
const action = vars.action ?? options?.action ?? "";
|
|
710
|
+
options?.onSettled?.(data, error, {
|
|
711
|
+
id: vars.id,
|
|
712
|
+
action,
|
|
713
|
+
data: vars.data
|
|
714
|
+
});
|
|
715
|
+
},
|
|
716
|
+
messages: options?.messages,
|
|
717
|
+
toastHandler: instanceToast
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function useSearchEngine(options) {
|
|
721
|
+
return useMutationWithTransition({
|
|
722
|
+
mutationFn: (vars) => {
|
|
723
|
+
if (!api.searchEngine) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a searchEngine method`));
|
|
724
|
+
const auth = resolveAuth();
|
|
725
|
+
return api.searchEngine({
|
|
726
|
+
token: auth.token,
|
|
727
|
+
organizationId: auth.organizationId,
|
|
728
|
+
query: vars.query,
|
|
729
|
+
body: vars.body,
|
|
730
|
+
path: options?.path
|
|
731
|
+
});
|
|
732
|
+
},
|
|
733
|
+
invalidateQueries: options?.invalidateQueries ?? [],
|
|
734
|
+
messages: options?.messages,
|
|
735
|
+
toastHandler: instanceToast
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function useSearchSimilar(options) {
|
|
739
|
+
return useMutationWithTransition({
|
|
740
|
+
mutationFn: (vars) => {
|
|
741
|
+
if (!api.searchSimilar) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define a searchSimilar method`));
|
|
742
|
+
const auth = resolveAuth();
|
|
743
|
+
return api.searchSimilar({
|
|
744
|
+
token: auth.token,
|
|
745
|
+
organizationId: auth.organizationId,
|
|
746
|
+
query: vars.query,
|
|
747
|
+
vector: vars.vector,
|
|
748
|
+
body: vars.body,
|
|
749
|
+
path: options?.path
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
invalidateQueries: options?.invalidateQueries ?? [],
|
|
753
|
+
messages: options?.messages,
|
|
754
|
+
toastHandler: instanceToast
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function useEmbed(options) {
|
|
758
|
+
return useMutationWithTransition({
|
|
759
|
+
mutationFn: (vars) => {
|
|
760
|
+
if (!api.embed) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define an embed method`));
|
|
761
|
+
const auth = resolveAuth();
|
|
762
|
+
return api.embed({
|
|
763
|
+
token: auth.token,
|
|
764
|
+
organizationId: auth.organizationId,
|
|
765
|
+
input: vars.input,
|
|
766
|
+
body: vars.body,
|
|
767
|
+
path: options?.path
|
|
768
|
+
});
|
|
769
|
+
},
|
|
770
|
+
invalidateQueries: options?.invalidateQueries ?? [],
|
|
771
|
+
messages: options?.messages,
|
|
772
|
+
toastHandler: instanceToast
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Subscribe to live `<resource>.<operation>` broadcasts from arc and
|
|
777
|
+
* auto-invalidate this entity's TanStack Query cache.
|
|
778
|
+
*
|
|
779
|
+
* - `source: 'ws'` (default) → arc's `websocketPlugin` at `/ws`. Sends a
|
|
780
|
+
* `{ type: 'subscribe', resource }` handshake on connect.
|
|
781
|
+
* - `source: 'sse'` → arc's `ssePlugin` at `/events/stream`. Auto-derives
|
|
782
|
+
* patterns from the resource name.
|
|
783
|
+
*
|
|
784
|
+
* Both transports invalidate `KEYS.lists()` on `<resource>.created` and
|
|
785
|
+
* `<resource>.deleted`, and `KEYS.detail(id)` (prefix-matches scoped /
|
|
786
|
+
* parameterized variants) on `<resource>.updated` / `.deleted`.
|
|
787
|
+
*/
|
|
788
|
+
function useResourceSync(options) {
|
|
789
|
+
const queryClient = useQueryClient();
|
|
790
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
791
|
+
const source = options?.source ?? "ws";
|
|
792
|
+
const resource = options?.resource ?? entityKey;
|
|
793
|
+
const enabled = options?.enabled ?? true;
|
|
794
|
+
const path = options?.path;
|
|
795
|
+
const onEventRef = useRef(options?.onEvent);
|
|
796
|
+
onEventRef.current = options?.onEvent;
|
|
797
|
+
const onConnRef = useRef(options?.onConnectionChange);
|
|
798
|
+
onConnRef.current = options?.onConnectionChange;
|
|
799
|
+
useEffect(() => {
|
|
800
|
+
if (!enabled) return;
|
|
801
|
+
const handleBroadcast = (incomingType, payload) => {
|
|
802
|
+
const dot = incomingType.lastIndexOf(".");
|
|
803
|
+
const operation = dot >= 0 ? incomingType.slice(dot + 1) : incomingType;
|
|
804
|
+
if (operation !== "created" && operation !== "updated" && operation !== "deleted") return;
|
|
805
|
+
let doc = payload;
|
|
806
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload) && "data" in payload) {
|
|
807
|
+
const inner = payload.data;
|
|
808
|
+
if (inner !== void 0) doc = inner;
|
|
809
|
+
}
|
|
810
|
+
const id = typeof doc === "object" && doc !== null ? (() => {
|
|
811
|
+
const o = doc;
|
|
812
|
+
const raw = idField ? o[idField] : o._id ?? o.id;
|
|
813
|
+
return raw != null ? String(raw) : void 0;
|
|
814
|
+
})() : void 0;
|
|
815
|
+
queryClient.invalidateQueries({ queryKey: KEYS.lists() });
|
|
816
|
+
if (id && (operation === "updated" || operation === "deleted")) queryClient.invalidateQueries({ queryKey: KEYS.detail(id) });
|
|
817
|
+
onEventRef.current?.({
|
|
818
|
+
operation,
|
|
819
|
+
id,
|
|
820
|
+
data: doc
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
if (source === "sse") {
|
|
824
|
+
const handle = subscribeToEvents({
|
|
825
|
+
resource,
|
|
826
|
+
path,
|
|
827
|
+
onConnectionChange: (c) => {
|
|
828
|
+
setIsConnected(c);
|
|
829
|
+
onConnRef.current?.(c);
|
|
830
|
+
},
|
|
831
|
+
onEvent: (event) => {
|
|
832
|
+
handleBroadcast(event.type, event.data);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
return () => handle.close();
|
|
836
|
+
}
|
|
837
|
+
const handle = connectWs({
|
|
838
|
+
path,
|
|
839
|
+
subscribe: [resource],
|
|
840
|
+
patterns: [`${resource}.`],
|
|
841
|
+
onConnectionChange: (c) => {
|
|
842
|
+
setIsConnected(c);
|
|
843
|
+
onConnRef.current?.(c);
|
|
844
|
+
},
|
|
845
|
+
onMessage: (message) => {
|
|
846
|
+
handleBroadcast(message.type, message.data);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
return () => handle.close();
|
|
850
|
+
}, [
|
|
851
|
+
enabled,
|
|
852
|
+
source,
|
|
853
|
+
resource,
|
|
854
|
+
path,
|
|
855
|
+
queryClient
|
|
856
|
+
]);
|
|
857
|
+
return { isConnected };
|
|
858
|
+
}
|
|
748
859
|
const resolvedRouterHook = instanceNavigation ?? useRouterHook ?? (() => ({
|
|
749
860
|
push: () => {},
|
|
750
861
|
replace: () => {}
|
|
@@ -777,10 +888,13 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
777
888
|
useDetailBySlug,
|
|
778
889
|
useTree,
|
|
779
890
|
useChildren,
|
|
780
|
-
useFindBy,
|
|
781
891
|
useUpload,
|
|
782
|
-
useSearch,
|
|
783
892
|
useCustomMutation,
|
|
893
|
+
useAction,
|
|
894
|
+
useSearchEngine,
|
|
895
|
+
useSearchSimilar,
|
|
896
|
+
useEmbed,
|
|
897
|
+
useResourceSync,
|
|
784
898
|
useNavigation
|
|
785
899
|
};
|
|
786
900
|
}
|
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 };
|
package/dist/mutation.js
CHANGED
|
@@ -21,6 +21,25 @@ let toastHandler = {
|
|
|
21
21
|
function configureToast(handler) {
|
|
22
22
|
toastHandler = handler;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the configured toast handler. Returns the console-based default when
|
|
26
|
+
* no handler has been configured yet.
|
|
27
|
+
*
|
|
28
|
+
* Use this when domain code outside the react-query lifecycle needs to fire
|
|
29
|
+
* ad-hoc success/error toasts using the same handler the SDK uses internally
|
|
30
|
+
* — avoids the need for consumer SDKs to keep a parallel cache of the handler.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // In a domain helper outside any mutation lifecycle:
|
|
34
|
+
* import { getToastHandler } from '@classytic/arc-next/mutation';
|
|
35
|
+
*
|
|
36
|
+
* function notifySaved(label: string) {
|
|
37
|
+
* getToastHandler().success(`${label} saved`);
|
|
38
|
+
* }
|
|
39
|
+
*/
|
|
40
|
+
function getToastHandler() {
|
|
41
|
+
return toastHandler;
|
|
42
|
+
}
|
|
24
43
|
function showToast(type, messages, data, variables, error, handler) {
|
|
25
44
|
const activeHandler = handler ?? toastHandler;
|
|
26
45
|
if (type === "success") {
|
|
@@ -163,4 +182,4 @@ function useOptimisticMutation(config) {
|
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
//#endregion
|
|
166
|
-
export { configureToast, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
|
|
185
|
+
export { configureToast, getToastHandler, useMutationWithOptimistic, useMutationWithTransition, useOptimisticMutation };
|
package/dist/prefetch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QueryClient, dehydrate } from "@tanstack/react-query";
|
|
1
|
+
import { HydrationBoundary, InfiniteData, QueryClient, dehydrate } from "@tanstack/react-query";
|
|
2
2
|
|
|
3
3
|
//#region src/prefetch.d.ts
|
|
4
4
|
interface PrefetchAuthContext {
|
|
@@ -51,6 +51,17 @@ interface CrudPrefetcher {
|
|
|
51
51
|
* Only available when the API has a `getTree` method (tree preset).
|
|
52
52
|
*/
|
|
53
53
|
prefetchTree: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Prefetch an infinite list query (cursor / page-based pagination). Uses the
|
|
56
|
+
* same query keys as `useInfiniteList` and seeds the `{ pages, pageParams }`
|
|
57
|
+
* shape TanStack Query expects for `useInfiniteQuery` — a flat
|
|
58
|
+
* `prefetchQuery` would NOT match the cache shape and the client hook would
|
|
59
|
+
* re-fetch from scratch, defeating the prefetch.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* await productsPrefetcher.prefetchInfiniteList(queryClient, { limit: 20 });
|
|
63
|
+
*/
|
|
64
|
+
prefetchInfiniteList: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
56
67
|
* Create server-safe prefetch helpers for CRUD queries.
|
|
@@ -121,4 +132,4 @@ declare function createCrudPrefetcher(api: {
|
|
|
121
132
|
}) => Promise<unknown>;
|
|
122
133
|
}, entityKey: string): CrudPrefetcher;
|
|
123
134
|
//#endregion
|
|
124
|
-
export { CrudPrefetcher, PrefetchAuthContext, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
|
|
135
|
+
export { CrudPrefetcher, HydrationBoundary, type InfiniteData, PrefetchAuthContext, PrefetchDetailOptions, PrefetchOptions, createCrudPrefetcher, dehydrate };
|
package/dist/prefetch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createQueryKeys } from "./
|
|
2
|
-
import { dehydrate } from "@tanstack/react-query";
|
|
1
|
+
import { createQueryKeys } from "./cache.js";
|
|
2
|
+
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
|
|
3
3
|
|
|
4
4
|
//#region src/prefetch.ts
|
|
5
5
|
/**
|
|
@@ -118,9 +118,33 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
118
118
|
}),
|
|
119
119
|
staleTime: options.staleTime
|
|
120
120
|
});
|
|
121
|
+
},
|
|
122
|
+
async prefetchInfiniteList(queryClient, params = {}, options = {}) {
|
|
123
|
+
const { organizationId: paramOrgId, ...restParams } = params;
|
|
124
|
+
const orgId = paramOrgId ?? options.organizationId ?? null;
|
|
125
|
+
const scope = orgId ? "tenant" : "super-admin";
|
|
126
|
+
const queryKey = [...KEYS.scopedList(scope, {
|
|
127
|
+
...orgId ? { organizationId: orgId } : {},
|
|
128
|
+
...restParams
|
|
129
|
+
}), "infinite"];
|
|
130
|
+
await queryClient.prefetchInfiniteQuery({
|
|
131
|
+
queryKey,
|
|
132
|
+
queryFn: ({ pageParam }) => api.getAll({
|
|
133
|
+
params: {
|
|
134
|
+
...restParams,
|
|
135
|
+
...pageParam ? { page: pageParam } : {}
|
|
136
|
+
},
|
|
137
|
+
token: options.token ?? null,
|
|
138
|
+
organizationId: orgId,
|
|
139
|
+
...options.headers ? { options: { headerOptions: options.headers } } : {}
|
|
140
|
+
}),
|
|
141
|
+
initialPageParam: 1,
|
|
142
|
+
getNextPageParam: () => void 0,
|
|
143
|
+
staleTime: options.staleTime
|
|
144
|
+
});
|
|
121
145
|
}
|
|
122
146
|
};
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
//#endregion
|
|
126
|
-
export { createCrudPrefetcher, dehydrate };
|
|
150
|
+
export { HydrationBoundary, createCrudPrefetcher, dehydrate };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BaseApi, BulkCreateResponse, BulkDeleteResponse, BulkUpdateResponse, ScopedArgs } from "../api.js";
|
|
2
|
+
|
|
3
|
+
//#region src/presets/bulk.d.ts
|
|
4
|
+
interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
|
|
5
|
+
/** Insert many docs in one round-trip. Backend mounts `POST /:resource/bulk`. */
|
|
6
|
+
bulkCreate(args: ScopedArgs & {
|
|
7
|
+
data: TCreate[];
|
|
8
|
+
}): Promise<BulkCreateResponse<TDoc>>;
|
|
9
|
+
/**
|
|
10
|
+
* Update all docs matching `filter` with `data`.
|
|
11
|
+
* Backend mounts `PATCH /:resource/bulk`.
|
|
12
|
+
*/
|
|
13
|
+
bulkUpdate(args: ScopedArgs & {
|
|
14
|
+
filter: Record<string, unknown>;
|
|
15
|
+
data: TUpdate;
|
|
16
|
+
}): Promise<BulkUpdateResponse>;
|
|
17
|
+
/**
|
|
18
|
+
* Delete all docs matching `filter`.
|
|
19
|
+
* Backend mounts `DELETE /:resource/bulk`.
|
|
20
|
+
*/
|
|
21
|
+
bulkDelete(args: ScopedArgs & {
|
|
22
|
+
filter: Record<string, unknown>;
|
|
23
|
+
}): Promise<BulkDeleteResponse>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Adds bulk preset methods to a BaseApi.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors arc's `bulk` preset — three /bulk routes for batch ops. Bulk endpoints
|
|
29
|
+
* require explicit per-resource permissions on the backend (the standard CRUD
|
|
30
|
+
* `allowPublic()` doesn't cover them).
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* import { withBulk } from '@classytic/arc-next/presets/bulk';
|
|
34
|
+
* const todos = withBulk(createCrudApi<Todo>('todos'));
|
|
35
|
+
*
|
|
36
|
+
* await todos.bulkCreate({ data: [{ title: 'A' }, { title: 'B' }] });
|
|
37
|
+
* await todos.bulkUpdate({ filter: { status: 'pending' }, data: { status: 'archived' } });
|
|
38
|
+
* await todos.bulkDelete({ filter: { archivedBefore: '2024-01-01' } });
|
|
39
|
+
*/
|
|
40
|
+
declare function withBulk<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & BulkMethods<TDoc, TCreate, TUpdate>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { BulkMethods, withBulk };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/presets/bulk.ts
|
|
2
|
+
/**
|
|
3
|
+
* Adds bulk preset methods to a BaseApi.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors arc's `bulk` preset — three /bulk routes for batch ops. Bulk endpoints
|
|
6
|
+
* require explicit per-resource permissions on the backend (the standard CRUD
|
|
7
|
+
* `allowPublic()` doesn't cover them).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { withBulk } from '@classytic/arc-next/presets/bulk';
|
|
11
|
+
* const todos = withBulk(createCrudApi<Todo>('todos'));
|
|
12
|
+
*
|
|
13
|
+
* await todos.bulkCreate({ data: [{ title: 'A' }, { title: 'B' }] });
|
|
14
|
+
* await todos.bulkUpdate({ filter: { status: 'pending' }, data: { status: 'archived' } });
|
|
15
|
+
* await todos.bulkDelete({ filter: { archivedBefore: '2024-01-01' } });
|
|
16
|
+
*/
|
|
17
|
+
function withBulk(api) {
|
|
18
|
+
return Object.assign(api, {
|
|
19
|
+
async bulkCreate({ token = null, organizationId = null, data, options = {} }) {
|
|
20
|
+
return api.request("POST", `${api.baseUrl}/bulk`, {
|
|
21
|
+
token,
|
|
22
|
+
organizationId,
|
|
23
|
+
data: { items: data },
|
|
24
|
+
options
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
async bulkUpdate({ token = null, organizationId = null, filter, data, options = {} }) {
|
|
28
|
+
return api.request("PATCH", `${api.baseUrl}/bulk`, {
|
|
29
|
+
token,
|
|
30
|
+
organizationId,
|
|
31
|
+
data: {
|
|
32
|
+
filter,
|
|
33
|
+
data
|
|
34
|
+
},
|
|
35
|
+
options
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
async bulkDelete({ token = null, organizationId = null, filter, options = {} }) {
|
|
39
|
+
return api.request("DELETE", `${api.baseUrl}/bulk`, {
|
|
40
|
+
token,
|
|
41
|
+
organizationId,
|
|
42
|
+
data: { filter },
|
|
43
|
+
options
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { withBulk };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ApiResponse, BaseApi, PaginatedResponse, ScopedArgs } from "../api.js";
|
|
2
|
+
|
|
3
|
+
//#region src/presets/search.d.ts
|
|
4
|
+
interface SearchPresetMethods<TDoc> {
|
|
5
|
+
/**
|
|
6
|
+
* Engine-backed full-text search (Elastic, Algolia, Typesense...).
|
|
7
|
+
* Backend mounts `POST /:resource/search` via `searchPreset()`.
|
|
8
|
+
*/
|
|
9
|
+
searchEngine<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
|
|
10
|
+
/** Free-text query forwarded as `body.query`. */query?: string; /** Engine-specific options merged into the request body. */
|
|
11
|
+
body?: TBody; /** Override path (default `/search`). */
|
|
12
|
+
path?: string;
|
|
13
|
+
}): Promise<ApiResponse<TResult[]> | PaginatedResponse<TResult>>;
|
|
14
|
+
/**
|
|
15
|
+
* Vector / semantic similarity (Atlas, Pinecone, Qdrant...).
|
|
16
|
+
* Backend mounts `POST /:resource/search-similar`.
|
|
17
|
+
*/
|
|
18
|
+
searchSimilar<TResult = TDoc, TBody extends Record<string, unknown> = Record<string, unknown>>(args?: ScopedArgs & {
|
|
19
|
+
/** Text query — backend embeds and searches for nearest neighbors. */query?: string; /** Pre-computed embedding vector — used directly for similarity search. */
|
|
20
|
+
vector?: number[]; /** Vector-engine options (`topK`, `filter`, `index`, ...). */
|
|
21
|
+
body?: TBody; /** Override path (default `/search-similar`). */
|
|
22
|
+
path?: string;
|
|
23
|
+
}): Promise<ApiResponse<TResult[]>>;
|
|
24
|
+
/**
|
|
25
|
+
* Convert text/media to a vector embedding via the engine the resource is wired to.
|
|
26
|
+
* Backend mounts `POST /:resource/embed`.
|
|
27
|
+
*/
|
|
28
|
+
embed(args: ScopedArgs & {
|
|
29
|
+
/** Text or array of texts to embed. */input: string | string[]; /** Embed-engine options (`model`, `dimensions`, ...). */
|
|
30
|
+
body?: Record<string, unknown>; /** Override path (default `/embed`). */
|
|
31
|
+
path?: string;
|
|
32
|
+
}): Promise<ApiResponse<number[] | number[][]>>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Adds search preset methods to a BaseApi.
|
|
36
|
+
*
|
|
37
|
+
* Mirrors arc's `searchPreset()` — three POST routes for engine-backed search,
|
|
38
|
+
* vector similarity, and embedding generation. Each route is independently
|
|
39
|
+
* permission-gated on the backend.
|
|
40
|
+
*
|
|
41
|
+
* Differs from {@link BaseApi.search} (a GET against the list endpoint with
|
|
42
|
+
* filter params) — that's the legacy filtered-list pattern. These methods hit
|
|
43
|
+
* the new POST routes the preset mounts.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* import { withSearchPreset } from '@classytic/arc-next/presets/search';
|
|
47
|
+
* const places = withSearchPreset(createCrudApi<Place>('places'));
|
|
48
|
+
*
|
|
49
|
+
* await places.searchEngine({ query: 'park', body: { filter: { category: 'park' } } });
|
|
50
|
+
* await places.searchSimilar({ vector: [0.1, 0.2, ...], body: { topK: 5 } });
|
|
51
|
+
* await places.embed({ input: 'hello world' });
|
|
52
|
+
*/
|
|
53
|
+
declare function withSearchPreset<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & SearchPresetMethods<TDoc>;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { SearchPresetMethods, withSearchPreset };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/presets/search.ts
|
|
2
|
+
/**
|
|
3
|
+
* Adds search preset methods to a BaseApi.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors arc's `searchPreset()` — three POST routes for engine-backed search,
|
|
6
|
+
* vector similarity, and embedding generation. Each route is independently
|
|
7
|
+
* permission-gated on the backend.
|
|
8
|
+
*
|
|
9
|
+
* Differs from {@link BaseApi.search} (a GET against the list endpoint with
|
|
10
|
+
* filter params) — that's the legacy filtered-list pattern. These methods hit
|
|
11
|
+
* the new POST routes the preset mounts.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { withSearchPreset } from '@classytic/arc-next/presets/search';
|
|
15
|
+
* const places = withSearchPreset(createCrudApi<Place>('places'));
|
|
16
|
+
*
|
|
17
|
+
* await places.searchEngine({ query: 'park', body: { filter: { category: 'park' } } });
|
|
18
|
+
* await places.searchSimilar({ vector: [0.1, 0.2, ...], body: { topK: 5 } });
|
|
19
|
+
* await places.embed({ input: 'hello world' });
|
|
20
|
+
*/
|
|
21
|
+
function withSearchPreset(api) {
|
|
22
|
+
return Object.assign(api, {
|
|
23
|
+
async searchEngine({ token = null, organizationId = null, query, body, path = "/search", options = {} } = {}) {
|
|
24
|
+
const requestBody = { ...body ?? {} };
|
|
25
|
+
if (query !== void 0) requestBody.query = query;
|
|
26
|
+
return api.request("POST", `${api.baseUrl}${path}`, {
|
|
27
|
+
token,
|
|
28
|
+
organizationId,
|
|
29
|
+
data: requestBody,
|
|
30
|
+
options
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
async searchSimilar({ token = null, organizationId = null, query, vector, body, path = "/search-similar", options = {} } = {}) {
|
|
34
|
+
const requestBody = { ...body ?? {} };
|
|
35
|
+
if (query !== void 0) requestBody.query = query;
|
|
36
|
+
if (vector !== void 0) requestBody.vector = vector;
|
|
37
|
+
return api.request("POST", `${api.baseUrl}${path}`, {
|
|
38
|
+
token,
|
|
39
|
+
organizationId,
|
|
40
|
+
data: requestBody,
|
|
41
|
+
options
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
async embed({ token = null, organizationId = null, input, body, path = "/embed", options = {} }) {
|
|
45
|
+
const requestBody = {
|
|
46
|
+
input,
|
|
47
|
+
...body ?? {}
|
|
48
|
+
};
|
|
49
|
+
return api.request("POST", `${api.baseUrl}${path}`, {
|
|
50
|
+
token,
|
|
51
|
+
organizationId,
|
|
52
|
+
data: requestBody,
|
|
53
|
+
options
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
export { withSearchPreset };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ApiResponse, BaseApi, ScopedArgs } from "../api.js";
|
|
2
|
+
|
|
3
|
+
//#region src/presets/slug.d.ts
|
|
4
|
+
interface SlugLookupMethods<TDoc> {
|
|
5
|
+
/** Fetch a single doc by slug. Backend mounts `GET /:resource/slug/:slug`. */
|
|
6
|
+
getBySlug(args: ScopedArgs & {
|
|
7
|
+
slug: string;
|
|
8
|
+
params?: {
|
|
9
|
+
select?: string;
|
|
10
|
+
populate?: string | string[];
|
|
11
|
+
};
|
|
12
|
+
}): Promise<ApiResponse<TDoc>>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Adds slug-lookup preset methods to a BaseApi.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors arc's `slugLookup` preset — exposes `getBySlug` for resources keyed
|
|
18
|
+
* by URL-friendly slugs alongside the canonical id.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { withSlugLookup } from '@classytic/arc-next/presets/slug';
|
|
22
|
+
* const categories = withSlugLookup(createCrudApi<Category>('categories'));
|
|
23
|
+
*
|
|
24
|
+
* const cat = await categories.getBySlug({ slug: 'engineering' });
|
|
25
|
+
*/
|
|
26
|
+
declare function withSlugLookup<TDoc, TCreate, TUpdate>(api: BaseApi<TDoc, TCreate, TUpdate>): BaseApi<TDoc, TCreate, TUpdate> & SlugLookupMethods<TDoc>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { SlugLookupMethods, withSlugLookup };
|