@nimbleflux/fluxbase-sdk-react 2026.5.5-rc.2 → 2026.6.2

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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Impersonation hooks for Fluxbase React SDK
3
+ * Provides hooks for managing user impersonation sessions
4
+ */
5
+
6
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
7
+ import { useFluxbaseClient } from "./context";
8
+ import type { ListImpersonationSessionsOptions } from "@nimbleflux/fluxbase-sdk";
9
+
10
+ /**
11
+ * Hook to list impersonation sessions (audit trail)
12
+ */
13
+ export function useImpersonationSessions(
14
+ options?: ListImpersonationSessionsOptions,
15
+ ) {
16
+ const client = useFluxbaseClient();
17
+
18
+ return useQuery({
19
+ queryKey: ["fluxbase", "impersonation", "sessions", options],
20
+ queryFn: async () => {
21
+ return await client.admin.impersonation.listSessions(options);
22
+ },
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Hook to get the current impersonation session
28
+ */
29
+ export function useCurrentImpersonation() {
30
+ const client = useFluxbaseClient();
31
+
32
+ return useQuery({
33
+ queryKey: ["fluxbase", "impersonation", "current"],
34
+ queryFn: async () => {
35
+ return await client.admin.impersonation.getCurrent();
36
+ },
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Hook to start impersonating a specific user
42
+ */
43
+ export function useImpersonateUser() {
44
+ const client = useFluxbaseClient();
45
+ const queryClient = useQueryClient();
46
+
47
+ return useMutation({
48
+ mutationFn: async (request: {
49
+ target_user_id: string;
50
+ reason: string;
51
+ }) => {
52
+ return await client.admin.impersonation.impersonateUser(request);
53
+ },
54
+ onSuccess: () => {
55
+ queryClient.invalidateQueries({
56
+ queryKey: ["fluxbase", "impersonation"],
57
+ });
58
+ },
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Hook to stop the current impersonation session
64
+ */
65
+ export function useStopImpersonation() {
66
+ const client = useFluxbaseClient();
67
+ const queryClient = useQueryClient();
68
+
69
+ return useMutation({
70
+ mutationFn: async () => {
71
+ return await client.admin.impersonation.stop();
72
+ },
73
+ onSuccess: () => {
74
+ queryClient.invalidateQueries({
75
+ queryKey: ["fluxbase", "impersonation"],
76
+ });
77
+ },
78
+ });
79
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Tests for Jobs hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi } from "vitest";
6
+ import { renderHook, waitFor, act } from "@testing-library/react";
7
+ import {
8
+ useSubmitJob,
9
+ useJobStatus,
10
+ useJobs,
11
+ useCancelJob,
12
+ useRetryJob,
13
+ } from "./use-jobs";
14
+ import { createMockClient, createWrapper } from "./test-utils";
15
+
16
+ describe("useSubmitJob", () => {
17
+ it("should submit a job", async () => {
18
+ const submit = vi.fn().mockResolvedValue({
19
+ data: { id: "job-1", status: "pending" },
20
+ error: null,
21
+ });
22
+ const client = createMockClient({
23
+ jobs: { submit } as any,
24
+ });
25
+
26
+ const { result } = renderHook(() => useSubmitJob(), {
27
+ wrapper: createWrapper(client),
28
+ });
29
+
30
+ await act(async () => {
31
+ await result.current.mutateAsync({ name: "send-email", payload: { to: "a@b.com" } });
32
+ });
33
+
34
+ expect(submit).toHaveBeenCalledWith("send-email", { to: "a@b.com" });
35
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
36
+ });
37
+
38
+ it("should handle errors", async () => {
39
+ const client = createMockClient({
40
+ jobs: {
41
+ submit: vi.fn().mockResolvedValue({ data: null, error: new Error("Failed") }),
42
+ } as any,
43
+ });
44
+
45
+ const { result } = renderHook(() => useSubmitJob(), {
46
+ wrapper: createWrapper(client),
47
+ });
48
+
49
+ await act(async () => {
50
+ try {
51
+ await result.current.mutateAsync({ name: "bad-job" });
52
+ } catch {
53
+ // expected
54
+ }
55
+ });
56
+
57
+ await waitFor(() => expect(result.current.isError).toBe(true));
58
+ });
59
+ });
60
+
61
+ describe("useJobStatus", () => {
62
+ it("should get job status by ID", async () => {
63
+ const mockJob = { id: "job-1", status: "completed" };
64
+ const client = createMockClient({
65
+ jobs: {
66
+ get: vi.fn().mockResolvedValue({ data: mockJob, error: null }),
67
+ } as any,
68
+ });
69
+
70
+ const { result } = renderHook(() => useJobStatus("job-1"), {
71
+ wrapper: createWrapper(client),
72
+ });
73
+
74
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
75
+ expect(result.current.data).toEqual(mockJob);
76
+ });
77
+
78
+ it("should not fetch when jobId is null", () => {
79
+ const client = createMockClient();
80
+ const { result } = renderHook(() => useJobStatus(null), {
81
+ wrapper: createWrapper(client),
82
+ });
83
+ expect(result.current.isLoading).toBe(false);
84
+ expect(result.current.data).toBeUndefined();
85
+ });
86
+ });
87
+
88
+ describe("useJobs", () => {
89
+ it("should list jobs", async () => {
90
+ const mockJobs = [
91
+ { id: "job-1", status: "completed" },
92
+ { id: "job-2", status: "running" },
93
+ ];
94
+ const client = createMockClient({
95
+ jobs: {
96
+ list: vi.fn().mockResolvedValue({ data: mockJobs, error: null }),
97
+ } as any,
98
+ });
99
+
100
+ const { result } = renderHook(() => useJobs(), {
101
+ wrapper: createWrapper(client),
102
+ });
103
+
104
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
105
+ expect(result.current.data).toEqual(mockJobs);
106
+ });
107
+
108
+ it("should pass options to list", async () => {
109
+ const list = vi.fn().mockResolvedValue({ data: [], error: null });
110
+ const client = createMockClient({
111
+ jobs: { list } as any,
112
+ });
113
+
114
+ renderHook(() => useJobs({ status: "running", limit: 10 }), {
115
+ wrapper: createWrapper(client),
116
+ });
117
+
118
+ await waitFor(() => {
119
+ expect(list).toHaveBeenCalledWith({ status: "running", limit: 10 });
120
+ });
121
+ });
122
+
123
+ it("should handle errors", async () => {
124
+ const client = createMockClient({
125
+ jobs: {
126
+ list: vi.fn().mockResolvedValue({ data: null, error: new Error("Failed") }),
127
+ } as any,
128
+ });
129
+
130
+ const { result } = renderHook(() => useJobs(), {
131
+ wrapper: createWrapper(client),
132
+ });
133
+
134
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
135
+ expect(result.current.error).toBeInstanceOf(Error);
136
+ });
137
+ });
138
+
139
+ describe("useCancelJob", () => {
140
+ it("should cancel a job and invalidate queries", async () => {
141
+ const cancel = vi.fn().mockResolvedValue({ data: null, error: null });
142
+ const client = createMockClient({
143
+ jobs: { cancel } as any,
144
+ });
145
+
146
+ const { result } = renderHook(() => useCancelJob(), {
147
+ wrapper: createWrapper(client),
148
+ });
149
+
150
+ await act(async () => {
151
+ await result.current.mutateAsync("job-1");
152
+ });
153
+
154
+ expect(cancel).toHaveBeenCalledWith("job-1");
155
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
156
+ });
157
+
158
+ it("should handle errors", async () => {
159
+ const client = createMockClient({
160
+ jobs: {
161
+ cancel: vi.fn().mockResolvedValue({ data: null, error: new Error("Not found") }),
162
+ } as any,
163
+ });
164
+
165
+ const { result } = renderHook(() => useCancelJob(), {
166
+ wrapper: createWrapper(client),
167
+ });
168
+
169
+ await act(async () => {
170
+ try {
171
+ await result.current.mutateAsync("job-1");
172
+ } catch {
173
+ // expected
174
+ }
175
+ });
176
+
177
+ await waitFor(() => expect(result.current.isError).toBe(true));
178
+ });
179
+ });
180
+
181
+ describe("useRetryJob", () => {
182
+ it("should retry a job and invalidate queries", async () => {
183
+ const retry = vi.fn().mockResolvedValue({
184
+ data: { id: "job-2", status: "pending" },
185
+ error: null,
186
+ });
187
+ const client = createMockClient({
188
+ jobs: { retry } as any,
189
+ });
190
+
191
+ const { result } = renderHook(() => useRetryJob(), {
192
+ wrapper: createWrapper(client),
193
+ });
194
+
195
+ await act(async () => {
196
+ await result.current.mutateAsync("job-1");
197
+ });
198
+
199
+ expect(retry).toHaveBeenCalledWith("job-1");
200
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
201
+ });
202
+
203
+ it("should handle errors", async () => {
204
+ const client = createMockClient({
205
+ jobs: {
206
+ retry: vi.fn().mockResolvedValue({ data: null, error: new Error("Failed") }),
207
+ } as any,
208
+ });
209
+
210
+ const { result } = renderHook(() => useRetryJob(), {
211
+ wrapper: createWrapper(client),
212
+ });
213
+
214
+ await act(async () => {
215
+ try {
216
+ await result.current.mutateAsync("job-1");
217
+ } catch {
218
+ // expected
219
+ }
220
+ });
221
+
222
+ await waitFor(() => expect(result.current.isError).toBe(true));
223
+ });
224
+ });
package/src/use-jobs.ts CHANGED
@@ -31,3 +31,51 @@ export function useJobStatus(jobId: string | null) {
31
31
  },
32
32
  })
33
33
  }
34
+
35
+ export interface UseJobsOptions {
36
+ namespace?: string;
37
+ limit?: number;
38
+ offset?: number;
39
+ status?: string;
40
+ }
41
+
42
+ export function useJobs(options?: UseJobsOptions) {
43
+ const client = useFluxbaseClient()
44
+ return useQuery({
45
+ queryKey: ["fluxbase", "jobs", options],
46
+ queryFn: async (): Promise<unknown> => {
47
+ const { data, error } = await client.jobs.list(options)
48
+ if (error) throw error
49
+ return data
50
+ },
51
+ })
52
+ }
53
+
54
+ export function useCancelJob() {
55
+ const client = useFluxbaseClient()
56
+ const queryClient = useQueryClient()
57
+ return useMutation({
58
+ mutationFn: async (jobId: string): Promise<void> => {
59
+ const { error } = await client.jobs.cancel(jobId)
60
+ if (error) throw error
61
+ },
62
+ onSuccess: () => {
63
+ queryClient.invalidateQueries({ queryKey: ["fluxbase", "jobs"] })
64
+ },
65
+ })
66
+ }
67
+
68
+ export function useRetryJob() {
69
+ const client = useFluxbaseClient()
70
+ const queryClient = useQueryClient()
71
+ return useMutation({
72
+ mutationFn: async (jobId: string): Promise<unknown> => {
73
+ const { data, error } = await client.jobs.retry(jobId)
74
+ if (error) throw error
75
+ return data
76
+ },
77
+ onSuccess: () => {
78
+ queryClient.invalidateQueries({ queryKey: ["fluxbase", "jobs"] })
79
+ },
80
+ })
81
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Tests for Knowledge Base hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from "vitest";
6
+ import { renderHook, waitFor, act } from "@testing-library/react";
7
+ import {
8
+ useKnowledgeBases,
9
+ useKnowledgeBase,
10
+ useKBDocuments,
11
+ useCreateKnowledgeBase,
12
+ useDeleteKnowledgeBase,
13
+ useAddDocument,
14
+ useUploadDocument,
15
+ useDeleteDocument,
16
+ useKBSearch,
17
+ useKBEntities,
18
+ useKnowledgeGraph,
19
+ } from "./use-knowledge-base";
20
+ import {
21
+ createMockAIClient,
22
+ createWrapper,
23
+ } from "./test-utils";
24
+
25
+ describe("useKnowledgeBases", () => {
26
+ it("should list knowledge bases", async () => {
27
+ const mockKbs = [
28
+ { id: "kb1", name: "Test KB", namespace: "default", description: "", enabled: true, document_count: 0, total_chunks: 0, embedding_model: "text-embedding-3-small", created_at: "", updated_at: "" },
29
+ ];
30
+ const client = createMockAIClient({
31
+ knowledgeBase: {
32
+ list: vi.fn().mockResolvedValue({ data: mockKbs, error: null }),
33
+ } as any,
34
+ });
35
+
36
+ const { result } = renderHook(() => useKnowledgeBases(), {
37
+ wrapper: createWrapper(client),
38
+ });
39
+
40
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
41
+ expect(result.current.data).toEqual(mockKbs);
42
+ });
43
+
44
+ it("should handle errors", async () => {
45
+ const client = createMockAIClient({
46
+ knowledgeBase: {
47
+ list: vi.fn().mockResolvedValue({ data: null, error: new Error("Failed") }),
48
+ } as any,
49
+ });
50
+
51
+ const { result } = renderHook(() => useKnowledgeBases(), {
52
+ wrapper: createWrapper(client),
53
+ });
54
+
55
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
56
+ expect(result.current.error).toBeInstanceOf(Error);
57
+ });
58
+ });
59
+
60
+ describe("useKnowledgeBase", () => {
61
+ it("should get a single knowledge base", async () => {
62
+ const mockKb = {
63
+ id: "kb1", name: "Test KB", namespace: "default", description: "Test",
64
+ enabled: true, document_count: 5, total_chunks: 50,
65
+ embedding_model: "text-embedding-3-small", embedding_dimensions: 1536,
66
+ chunk_size: 1000, chunk_overlap: 200, chunk_strategy: "recursive",
67
+ source: "", created_at: "", updated_at: "",
68
+ };
69
+ const client = createMockAIClient({
70
+ knowledgeBase: {
71
+ get: vi.fn().mockResolvedValue({ data: mockKb, error: null }),
72
+ } as any,
73
+ });
74
+
75
+ const { result } = renderHook(() => useKnowledgeBase("kb1"), {
76
+ wrapper: createWrapper(client),
77
+ });
78
+
79
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
80
+ expect(result.current.data).toEqual(mockKb);
81
+ });
82
+
83
+ it("should not fetch when kbId is null", () => {
84
+ const client = createMockAIClient();
85
+ const { result } = renderHook(() => useKnowledgeBase(null), {
86
+ wrapper: createWrapper(client),
87
+ });
88
+ expect(result.current.data).toBeUndefined();
89
+ });
90
+ });
91
+
92
+ describe("useCreateKnowledgeBase", () => {
93
+ it("should create a knowledge base", async () => {
94
+ const createFn = vi.fn().mockResolvedValue({
95
+ data: { id: "kb1", name: "New" },
96
+ error: null,
97
+ });
98
+ const client = createMockAIClient({
99
+ knowledgeBase: { create: createFn } as any,
100
+ });
101
+
102
+ const { result } = renderHook(() => useCreateKnowledgeBase(), {
103
+ wrapper: createWrapper(client),
104
+ });
105
+
106
+ await act(async () => {
107
+ await result.current.mutateAsync({ name: "New" });
108
+ });
109
+
110
+ expect(createFn).toHaveBeenCalledWith({ name: "New" });
111
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
112
+ });
113
+ });
114
+
115
+ describe("useDeleteKnowledgeBase", () => {
116
+ it("should delete a knowledge base", async () => {
117
+ const deleteFn = vi.fn().mockResolvedValue({ data: true, error: null });
118
+ const client = createMockAIClient({
119
+ knowledgeBase: { delete: deleteFn } as any,
120
+ });
121
+
122
+ const { result } = renderHook(() => useDeleteKnowledgeBase(), {
123
+ wrapper: createWrapper(client),
124
+ });
125
+
126
+ await act(async () => {
127
+ await result.current.mutateAsync("kb1");
128
+ });
129
+
130
+ expect(deleteFn).toHaveBeenCalledWith("kb1");
131
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
132
+ });
133
+ });
134
+
135
+ describe("useKBDocuments", () => {
136
+ it("should list documents in a knowledge base", async () => {
137
+ const mockDocs = [
138
+ { id: "doc1", knowledge_base_id: "kb1", title: "Doc 1", mime_type: "text/plain", content_hash: "abc", chunk_count: 5, status: "indexed", created_at: "", updated_at: "" },
139
+ ];
140
+ const client = createMockAIClient({
141
+ knowledgeBase: {
142
+ listDocuments: vi.fn().mockResolvedValue({ data: mockDocs, error: null }),
143
+ } as any,
144
+ });
145
+
146
+ const { result } = renderHook(() => useKBDocuments("kb1"), {
147
+ wrapper: createWrapper(client),
148
+ });
149
+
150
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
151
+ expect(result.current.data).toEqual(mockDocs);
152
+ });
153
+
154
+ it("should not fetch when kbId is null", () => {
155
+ const client = createMockAIClient();
156
+ const { result } = renderHook(() => useKBDocuments(null), {
157
+ wrapper: createWrapper(client),
158
+ });
159
+ expect(result.current.isLoading).toBe(false);
160
+ });
161
+ });
162
+
163
+ describe("useAddDocument", () => {
164
+ it("should add a document", async () => {
165
+ const addFn = vi.fn().mockResolvedValue({
166
+ data: { document_id: "doc1", status: "processing", message: "Added" },
167
+ error: null,
168
+ });
169
+ const client = createMockAIClient({
170
+ knowledgeBase: { addDocument: addFn } as any,
171
+ });
172
+
173
+ const { result } = renderHook(() => useAddDocument(), {
174
+ wrapper: createWrapper(client),
175
+ });
176
+
177
+ await act(async () => {
178
+ await result.current.mutateAsync({
179
+ kbId: "kb1",
180
+ request: { title: "Test", content: "Hello world" },
181
+ });
182
+ });
183
+
184
+ expect(addFn).toHaveBeenCalledWith("kb1", { title: "Test", content: "Hello world" });
185
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
186
+ });
187
+ });
188
+
189
+ describe("useUploadDocument", () => {
190
+ it("should upload a file document", async () => {
191
+ const uploadFn = vi.fn().mockResolvedValue({
192
+ data: { document_id: "doc1", status: "processing", message: "Uploaded", filename: "test.pdf", extracted_length: 1000, mime_type: "application/pdf" },
193
+ error: null,
194
+ });
195
+ const client = createMockAIClient({
196
+ knowledgeBase: { uploadDocument: uploadFn } as any,
197
+ });
198
+
199
+ const { result } = renderHook(() => useUploadDocument(), {
200
+ wrapper: createWrapper(client),
201
+ });
202
+
203
+ const file = new Blob(["content"], { type: "application/pdf" });
204
+
205
+ await act(async () => {
206
+ await result.current.mutateAsync({
207
+ kbId: "kb1",
208
+ file,
209
+ filename: "test.pdf",
210
+ });
211
+ });
212
+
213
+ expect(uploadFn).toHaveBeenCalledWith("kb1", file, "test.pdf", undefined);
214
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
215
+ });
216
+ });
217
+
218
+ describe("useDeleteDocument", () => {
219
+ it("should delete a document", async () => {
220
+ const deleteFn = vi.fn().mockResolvedValue({ data: true, error: null });
221
+ const client = createMockAIClient({
222
+ knowledgeBase: { deleteDocument: deleteFn } as any,
223
+ });
224
+
225
+ const { result } = renderHook(() => useDeleteDocument(), {
226
+ wrapper: createWrapper(client),
227
+ });
228
+
229
+ await act(async () => {
230
+ await result.current.mutateAsync({ kbId: "kb1", docId: "doc1" });
231
+ });
232
+
233
+ expect(deleteFn).toHaveBeenCalledWith("kb1", "doc1");
234
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
235
+ });
236
+ });
237
+
238
+ describe("useKBSearch", () => {
239
+ it("should search a knowledge base", async () => {
240
+ const mockResults = {
241
+ results: [{ chunk_id: "c1", document_id: "d1", document_title: "Doc 1", content: "test", similarity: 0.95 }],
242
+ count: 1,
243
+ query: "test",
244
+ };
245
+ const searchFn = vi.fn().mockResolvedValue({ data: mockResults, error: null });
246
+ const client = createMockAIClient({
247
+ knowledgeBase: { search: searchFn } as any,
248
+ });
249
+
250
+ const { result } = renderHook(
251
+ () => useKBSearch("kb1", { query: "test" }),
252
+ { wrapper: createWrapper(client) },
253
+ );
254
+
255
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
256
+ expect(result.current.data).toEqual(mockResults);
257
+ expect(searchFn).toHaveBeenCalledWith("kb1", { query: "test" });
258
+ });
259
+
260
+ it("should not search when request is null", () => {
261
+ const client = createMockAIClient();
262
+ const { result } = renderHook(() => useKBSearch("kb1", null), {
263
+ wrapper: createWrapper(client),
264
+ });
265
+ expect(result.current.data).toBeUndefined();
266
+ });
267
+ });
268
+
269
+ describe("useKBEntities", () => {
270
+ it("should list entities", async () => {
271
+ const mockEntities = [
272
+ { id: "e1", knowledge_base_id: "kb1", entity_type: "person", name: "John", canonical_name: "John", aliases: [], metadata: {}, created_at: "", updated_at: "" },
273
+ ];
274
+ const client = createMockAIClient({
275
+ knowledgeBase: {
276
+ listEntities: vi.fn().mockResolvedValue({ data: mockEntities, error: null }),
277
+ } as any,
278
+ });
279
+
280
+ const { result } = renderHook(() => useKBEntities("kb1"), {
281
+ wrapper: createWrapper(client),
282
+ });
283
+
284
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
285
+ expect(result.current.data).toEqual(mockEntities);
286
+ });
287
+ });
288
+
289
+ describe("useKnowledgeGraph", () => {
290
+ it("should get the knowledge graph", async () => {
291
+ const mockGraph = {
292
+ entities: [],
293
+ relationships: [],
294
+ entity_count: 0,
295
+ relationship_count: 0,
296
+ };
297
+ const client = createMockAIClient({
298
+ knowledgeBase: {
299
+ getKnowledgeGraph: vi.fn().mockResolvedValue({ data: mockGraph, error: null }),
300
+ } as any,
301
+ });
302
+
303
+ const { result } = renderHook(() => useKnowledgeGraph("kb1"), {
304
+ wrapper: createWrapper(client),
305
+ });
306
+
307
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
308
+ expect(result.current.data).toEqual(mockGraph);
309
+ });
310
+ });