@nimbleflux/fluxbase-sdk-react 2026.5.5-rc.2 → 2026.6.1
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 +10 -17
- package/package.json +2 -2
- package/src/index.ts +43 -0
- package/src/test-utils.tsx +174 -0
- package/src/use-ai.test.ts +193 -0
- package/src/use-ai.ts +253 -0
- package/src/use-branches.test.ts +231 -0
- package/src/use-branches.ts +57 -1
- package/src/use-ddl.test.ts +205 -0
- package/src/use-ddl.ts +73 -0
- package/src/use-functions.test.ts +89 -0
- package/src/use-functions.ts +13 -1
- package/src/use-impersonation.test.ts +273 -0
- package/src/use-impersonation.ts +79 -0
- package/src/use-jobs.test.ts +224 -0
- package/src/use-jobs.ts +48 -0
- package/src/use-knowledge-base.test.ts +310 -0
- package/src/use-knowledge-base.ts +296 -0
- package/src/use-migrations.test.ts +280 -0
- package/src/use-migrations.ts +107 -0
- package/src/use-rpc.test.ts +147 -0
- package/src/use-rpc.ts +58 -0
- package/src/use-secrets.test.ts +253 -0
- package/src/use-secrets.ts +85 -0
- package/src/use-service-keys.test.ts +237 -0
- package/src/use-service-keys.ts +91 -0
- package/src/use-vector.test.ts +176 -0
- package/src/use-vector.ts +55 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Base hooks for Fluxbase React SDK
|
|
3
|
+
* Provides hooks for RAG document management, semantic search, and knowledge graph
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { useFluxbaseClient } from "./context";
|
|
8
|
+
import type {
|
|
9
|
+
CreateKnowledgeBaseRequest,
|
|
10
|
+
UpdateKnowledgeBaseRequest,
|
|
11
|
+
AddDocumentRequest,
|
|
12
|
+
SearchKnowledgeBaseRequest,
|
|
13
|
+
} from "@nimbleflux/fluxbase-sdk";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to list all knowledge bases the user has access to
|
|
17
|
+
*/
|
|
18
|
+
export function useKnowledgeBases() {
|
|
19
|
+
const client = useFluxbaseClient();
|
|
20
|
+
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: ["fluxbase", "knowledge-base", "list"],
|
|
23
|
+
queryFn: async () => {
|
|
24
|
+
const { data, error } = await client.knowledgeBase.list();
|
|
25
|
+
if (error) throw error;
|
|
26
|
+
return data || [];
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hook to get a single knowledge base by ID
|
|
33
|
+
*/
|
|
34
|
+
export function useKnowledgeBase(kbId: string | null) {
|
|
35
|
+
const client = useFluxbaseClient();
|
|
36
|
+
|
|
37
|
+
return useQuery({
|
|
38
|
+
queryKey: ["fluxbase", "knowledge-base", kbId],
|
|
39
|
+
queryFn: async () => {
|
|
40
|
+
if (!kbId) return null;
|
|
41
|
+
const { data, error } = await client.knowledgeBase.get(kbId);
|
|
42
|
+
if (error) throw error;
|
|
43
|
+
return data;
|
|
44
|
+
},
|
|
45
|
+
enabled: !!kbId,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hook to create a new knowledge base
|
|
51
|
+
*/
|
|
52
|
+
export function useCreateKnowledgeBase() {
|
|
53
|
+
const client = useFluxbaseClient();
|
|
54
|
+
const queryClient = useQueryClient();
|
|
55
|
+
|
|
56
|
+
return useMutation({
|
|
57
|
+
mutationFn: async (request: CreateKnowledgeBaseRequest) => {
|
|
58
|
+
const { data, error } = await client.knowledgeBase.create(request);
|
|
59
|
+
if (error) throw error;
|
|
60
|
+
return data;
|
|
61
|
+
},
|
|
62
|
+
onSuccess: () => {
|
|
63
|
+
queryClient.invalidateQueries({
|
|
64
|
+
queryKey: ["fluxbase", "knowledge-base", "list"],
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hook to update a knowledge base
|
|
72
|
+
*/
|
|
73
|
+
export function useUpdateKnowledgeBase() {
|
|
74
|
+
const client = useFluxbaseClient();
|
|
75
|
+
const queryClient = useQueryClient();
|
|
76
|
+
|
|
77
|
+
return useMutation({
|
|
78
|
+
mutationFn: async ({
|
|
79
|
+
kbId,
|
|
80
|
+
updates,
|
|
81
|
+
}: {
|
|
82
|
+
kbId: string;
|
|
83
|
+
updates: UpdateKnowledgeBaseRequest;
|
|
84
|
+
}) => {
|
|
85
|
+
const { data, error } = await client.knowledgeBase.update(kbId, updates);
|
|
86
|
+
if (error) throw error;
|
|
87
|
+
return data;
|
|
88
|
+
},
|
|
89
|
+
onSuccess: (_data, variables) => {
|
|
90
|
+
queryClient.invalidateQueries({
|
|
91
|
+
queryKey: ["fluxbase", "knowledge-base", "list"],
|
|
92
|
+
});
|
|
93
|
+
queryClient.invalidateQueries({
|
|
94
|
+
queryKey: ["fluxbase", "knowledge-base", variables.kbId],
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Hook to delete a knowledge base
|
|
102
|
+
*/
|
|
103
|
+
export function useDeleteKnowledgeBase() {
|
|
104
|
+
const client = useFluxbaseClient();
|
|
105
|
+
const queryClient = useQueryClient();
|
|
106
|
+
|
|
107
|
+
return useMutation({
|
|
108
|
+
mutationFn: async (kbId: string) => {
|
|
109
|
+
const { error } = await client.knowledgeBase.delete(kbId);
|
|
110
|
+
if (error) throw error;
|
|
111
|
+
},
|
|
112
|
+
onSuccess: () => {
|
|
113
|
+
queryClient.invalidateQueries({
|
|
114
|
+
queryKey: ["fluxbase", "knowledge-base", "list"],
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Hook to list documents in a knowledge base
|
|
122
|
+
*/
|
|
123
|
+
export function useKBDocuments(kbId: string | null) {
|
|
124
|
+
const client = useFluxbaseClient();
|
|
125
|
+
|
|
126
|
+
return useQuery({
|
|
127
|
+
queryKey: ["fluxbase", "knowledge-base", kbId, "documents"],
|
|
128
|
+
queryFn: async () => {
|
|
129
|
+
if (!kbId) return [];
|
|
130
|
+
const { data, error } = await client.knowledgeBase.listDocuments(kbId);
|
|
131
|
+
if (error) throw error;
|
|
132
|
+
return data || [];
|
|
133
|
+
},
|
|
134
|
+
enabled: !!kbId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Hook to add a document to a knowledge base
|
|
140
|
+
*/
|
|
141
|
+
export function useAddDocument() {
|
|
142
|
+
const client = useFluxbaseClient();
|
|
143
|
+
const queryClient = useQueryClient();
|
|
144
|
+
|
|
145
|
+
return useMutation({
|
|
146
|
+
mutationFn: async ({
|
|
147
|
+
kbId,
|
|
148
|
+
request,
|
|
149
|
+
}: {
|
|
150
|
+
kbId: string;
|
|
151
|
+
request: AddDocumentRequest;
|
|
152
|
+
}) => {
|
|
153
|
+
const { data, error } = await client.knowledgeBase.addDocument(
|
|
154
|
+
kbId,
|
|
155
|
+
request,
|
|
156
|
+
);
|
|
157
|
+
if (error) throw error;
|
|
158
|
+
return data;
|
|
159
|
+
},
|
|
160
|
+
onSuccess: (_data, variables) => {
|
|
161
|
+
queryClient.invalidateQueries({
|
|
162
|
+
queryKey: ["fluxbase", "knowledge-base", variables.kbId, "documents"],
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Hook to upload a file document to a knowledge base
|
|
170
|
+
*/
|
|
171
|
+
export function useUploadDocument() {
|
|
172
|
+
const client = useFluxbaseClient();
|
|
173
|
+
const queryClient = useQueryClient();
|
|
174
|
+
|
|
175
|
+
return useMutation({
|
|
176
|
+
mutationFn: async ({
|
|
177
|
+
kbId,
|
|
178
|
+
file,
|
|
179
|
+
filename,
|
|
180
|
+
metadata,
|
|
181
|
+
}: {
|
|
182
|
+
kbId: string;
|
|
183
|
+
file: File | Blob | ArrayBuffer;
|
|
184
|
+
filename: string;
|
|
185
|
+
metadata?: Record<string, string>;
|
|
186
|
+
}) => {
|
|
187
|
+
const { data, error } = await client.knowledgeBase.uploadDocument(
|
|
188
|
+
kbId,
|
|
189
|
+
file,
|
|
190
|
+
filename,
|
|
191
|
+
metadata,
|
|
192
|
+
);
|
|
193
|
+
if (error) throw error;
|
|
194
|
+
return data;
|
|
195
|
+
},
|
|
196
|
+
onSuccess: (_data, variables) => {
|
|
197
|
+
queryClient.invalidateQueries({
|
|
198
|
+
queryKey: ["fluxbase", "knowledge-base", variables.kbId, "documents"],
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Hook to delete a document
|
|
206
|
+
*/
|
|
207
|
+
export function useDeleteDocument() {
|
|
208
|
+
const client = useFluxbaseClient();
|
|
209
|
+
const queryClient = useQueryClient();
|
|
210
|
+
|
|
211
|
+
return useMutation({
|
|
212
|
+
mutationFn: async ({
|
|
213
|
+
kbId,
|
|
214
|
+
docId,
|
|
215
|
+
}: {
|
|
216
|
+
kbId: string;
|
|
217
|
+
docId: string;
|
|
218
|
+
}) => {
|
|
219
|
+
const { error } = await client.knowledgeBase.deleteDocument(kbId, docId);
|
|
220
|
+
if (error) throw error;
|
|
221
|
+
},
|
|
222
|
+
onSuccess: (_data, variables) => {
|
|
223
|
+
queryClient.invalidateQueries({
|
|
224
|
+
queryKey: ["fluxbase", "knowledge-base", variables.kbId, "documents"],
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Hook to search a knowledge base with semantic similarity
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```tsx
|
|
235
|
+
* const { data, isPending } = useKBSearch(kbId, {
|
|
236
|
+
* query: 'How to configure auth?',
|
|
237
|
+
* max_chunks: 5,
|
|
238
|
+
* })
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
export function useKBSearch(
|
|
242
|
+
kbId: string | null,
|
|
243
|
+
request: SearchKnowledgeBaseRequest | null,
|
|
244
|
+
) {
|
|
245
|
+
const client = useFluxbaseClient();
|
|
246
|
+
|
|
247
|
+
return useQuery({
|
|
248
|
+
queryKey: ["fluxbase", "knowledge-base", kbId, "search", request],
|
|
249
|
+
queryFn: async () => {
|
|
250
|
+
if (!kbId || !request) return null;
|
|
251
|
+
const { data, error } = await client.knowledgeBase.search(kbId, request);
|
|
252
|
+
if (error) throw error;
|
|
253
|
+
return data;
|
|
254
|
+
},
|
|
255
|
+
enabled: !!kbId && !!request,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Hook to list entities in a knowledge base
|
|
261
|
+
*/
|
|
262
|
+
export function useKBEntities(kbId: string | null, type?: string) {
|
|
263
|
+
const client = useFluxbaseClient();
|
|
264
|
+
|
|
265
|
+
return useQuery({
|
|
266
|
+
queryKey: ["fluxbase", "knowledge-base", kbId, "entities", type],
|
|
267
|
+
queryFn: async () => {
|
|
268
|
+
if (!kbId) return [];
|
|
269
|
+
const { data, error } = await client.knowledgeBase.listEntities(
|
|
270
|
+
kbId,
|
|
271
|
+
type as any,
|
|
272
|
+
);
|
|
273
|
+
if (error) throw error;
|
|
274
|
+
return data || [];
|
|
275
|
+
},
|
|
276
|
+
enabled: !!kbId,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Hook to get the knowledge graph (entities + relationships)
|
|
282
|
+
*/
|
|
283
|
+
export function useKnowledgeGraph(kbId: string | null) {
|
|
284
|
+
const client = useFluxbaseClient();
|
|
285
|
+
|
|
286
|
+
return useQuery({
|
|
287
|
+
queryKey: ["fluxbase", "knowledge-base", kbId, "graph"],
|
|
288
|
+
queryFn: async () => {
|
|
289
|
+
if (!kbId) return null;
|
|
290
|
+
const { data, error } = await client.knowledgeBase.getKnowledgeGraph(kbId);
|
|
291
|
+
if (error) throw error;
|
|
292
|
+
return data;
|
|
293
|
+
},
|
|
294
|
+
enabled: !!kbId,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for migration hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
7
|
+
import {
|
|
8
|
+
useMigrations,
|
|
9
|
+
useApplyMigration,
|
|
10
|
+
useRollbackMigration,
|
|
11
|
+
useSyncMigrations,
|
|
12
|
+
} from "./use-migrations";
|
|
13
|
+
import { createMockClient, createWrapper } from "./test-utils";
|
|
14
|
+
|
|
15
|
+
describe("useMigrations", () => {
|
|
16
|
+
it("should list migrations", async () => {
|
|
17
|
+
const mockMigrations = [
|
|
18
|
+
{
|
|
19
|
+
id: "1",
|
|
20
|
+
namespace: "default",
|
|
21
|
+
name: "001_init",
|
|
22
|
+
up_sql: "CREATE TABLE test();",
|
|
23
|
+
version: 1,
|
|
24
|
+
status: "applied" as const,
|
|
25
|
+
created_at: "",
|
|
26
|
+
updated_at: "",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const client = createMockClient({
|
|
30
|
+
admin: {
|
|
31
|
+
migrations: {
|
|
32
|
+
list: vi.fn().mockResolvedValue({ data: mockMigrations, error: null }),
|
|
33
|
+
},
|
|
34
|
+
} as any,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(() => useMigrations(), {
|
|
38
|
+
wrapper: createWrapper(client),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
42
|
+
expect(result.current.data).toEqual(mockMigrations);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should pass namespace to list", async () => {
|
|
46
|
+
const list = vi.fn().mockResolvedValue({ data: [], error: null });
|
|
47
|
+
const client = createMockClient({
|
|
48
|
+
admin: { migrations: { list } } as any,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
renderHook(() => useMigrations("myapp"), {
|
|
52
|
+
wrapper: createWrapper(client),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(list).toHaveBeenCalledWith("myapp");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should handle errors", async () => {
|
|
61
|
+
const client = createMockClient({
|
|
62
|
+
admin: {
|
|
63
|
+
migrations: {
|
|
64
|
+
list: vi
|
|
65
|
+
.fn()
|
|
66
|
+
.mockResolvedValue({ data: null, error: new Error("Failed") }),
|
|
67
|
+
},
|
|
68
|
+
} as any,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(() => useMigrations(), {
|
|
72
|
+
wrapper: createWrapper(client),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
76
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("useApplyMigration", () => {
|
|
81
|
+
it("should apply a migration", async () => {
|
|
82
|
+
const apply = vi
|
|
83
|
+
.fn()
|
|
84
|
+
.mockResolvedValue({ data: { message: "Applied" }, error: null });
|
|
85
|
+
const client = createMockClient({
|
|
86
|
+
admin: { migrations: { apply } } as any,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const { result } = renderHook(() => useApplyMigration(), {
|
|
90
|
+
wrapper: createWrapper(client),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await act(async () => {
|
|
94
|
+
await result.current.mutateAsync({ name: "001_init" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(apply).toHaveBeenCalledWith("001_init", undefined);
|
|
98
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should pass namespace to apply", async () => {
|
|
102
|
+
const apply = vi
|
|
103
|
+
.fn()
|
|
104
|
+
.mockResolvedValue({ data: { message: "Applied" }, error: null });
|
|
105
|
+
const client = createMockClient({
|
|
106
|
+
admin: { migrations: { apply } } as any,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => useApplyMigration(), {
|
|
110
|
+
wrapper: createWrapper(client),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
await result.current.mutateAsync({ name: "001_init", namespace: "myapp" });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(apply).toHaveBeenCalledWith("001_init", "myapp");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should handle errors", async () => {
|
|
121
|
+
const client = createMockClient({
|
|
122
|
+
admin: {
|
|
123
|
+
migrations: {
|
|
124
|
+
apply: vi
|
|
125
|
+
.fn()
|
|
126
|
+
.mockResolvedValue({ data: null, error: new Error("Failed") }),
|
|
127
|
+
},
|
|
128
|
+
} as any,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { result } = renderHook(() => useApplyMigration(), {
|
|
132
|
+
wrapper: createWrapper(client),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await act(async () => {
|
|
136
|
+
try {
|
|
137
|
+
await result.current.mutateAsync({ name: "001_init" });
|
|
138
|
+
} catch {
|
|
139
|
+
// expected
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("useRollbackMigration", () => {
|
|
148
|
+
it("should rollback a migration", async () => {
|
|
149
|
+
const rollback = vi
|
|
150
|
+
.fn()
|
|
151
|
+
.mockResolvedValue({ data: { message: "Rolled back" }, error: null });
|
|
152
|
+
const client = createMockClient({
|
|
153
|
+
admin: { migrations: { rollback } } as any,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const { result } = renderHook(() => useRollbackMigration(), {
|
|
157
|
+
wrapper: createWrapper(client),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
await result.current.mutateAsync({ name: "001_init" });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(rollback).toHaveBeenCalledWith("001_init", undefined);
|
|
165
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should handle errors", async () => {
|
|
169
|
+
const client = createMockClient({
|
|
170
|
+
admin: {
|
|
171
|
+
migrations: {
|
|
172
|
+
rollback: vi
|
|
173
|
+
.fn()
|
|
174
|
+
.mockResolvedValue({ data: null, error: new Error("Failed") }),
|
|
175
|
+
},
|
|
176
|
+
} as any,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const { result } = renderHook(() => useRollbackMigration(), {
|
|
180
|
+
wrapper: createWrapper(client),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await act(async () => {
|
|
184
|
+
try {
|
|
185
|
+
await result.current.mutateAsync({ name: "001_init" });
|
|
186
|
+
} catch {
|
|
187
|
+
// expected
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("useSyncMigrations", () => {
|
|
196
|
+
it("should sync migrations", async () => {
|
|
197
|
+
const mockResult = {
|
|
198
|
+
message: "Synced",
|
|
199
|
+
namespace: "default",
|
|
200
|
+
summary: {
|
|
201
|
+
created: 1,
|
|
202
|
+
updated: 0,
|
|
203
|
+
unchanged: 0,
|
|
204
|
+
skipped: 0,
|
|
205
|
+
applied: 0,
|
|
206
|
+
errors: 0,
|
|
207
|
+
},
|
|
208
|
+
details: {
|
|
209
|
+
created: ["001_init"],
|
|
210
|
+
updated: [],
|
|
211
|
+
unchanged: [],
|
|
212
|
+
skipped: [],
|
|
213
|
+
applied: [],
|
|
214
|
+
errors: [],
|
|
215
|
+
},
|
|
216
|
+
dry_run: false,
|
|
217
|
+
};
|
|
218
|
+
const sync = vi.fn().mockResolvedValue({ data: mockResult, error: null });
|
|
219
|
+
const client = createMockClient({
|
|
220
|
+
admin: { migrations: { sync } } as any,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const { result } = renderHook(() => useSyncMigrations(), {
|
|
224
|
+
wrapper: createWrapper(client),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await act(async () => {
|
|
228
|
+
await result.current.mutateAsync();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(sync).toHaveBeenCalledWith(undefined);
|
|
232
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
233
|
+
expect(result.current.data).toEqual(mockResult);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should pass options to sync", async () => {
|
|
237
|
+
const sync = vi
|
|
238
|
+
.fn()
|
|
239
|
+
.mockResolvedValue({ data: null, error: null });
|
|
240
|
+
const client = createMockClient({
|
|
241
|
+
admin: { migrations: { sync } } as any,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const { result } = renderHook(() => useSyncMigrations(), {
|
|
245
|
+
wrapper: createWrapper(client),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await act(async () => {
|
|
249
|
+
await result.current.mutateAsync({ auto_apply: true });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(sync).toHaveBeenCalledWith({ auto_apply: true });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should handle errors", async () => {
|
|
256
|
+
const client = createMockClient({
|
|
257
|
+
admin: {
|
|
258
|
+
migrations: {
|
|
259
|
+
sync: vi
|
|
260
|
+
.fn()
|
|
261
|
+
.mockResolvedValue({ data: null, error: new Error("Sync failed") }),
|
|
262
|
+
},
|
|
263
|
+
} as any,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const { result } = renderHook(() => useSyncMigrations(), {
|
|
267
|
+
wrapper: createWrapper(client),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await act(async () => {
|
|
271
|
+
try {
|
|
272
|
+
await result.current.mutateAsync();
|
|
273
|
+
} catch {
|
|
274
|
+
// expected
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration hooks for Fluxbase React SDK
|
|
3
|
+
* Provides hooks for listing, applying, rolling back, and syncing database migrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { useFluxbaseClient } from "./context";
|
|
8
|
+
import type { SyncMigrationsOptions } from "@nimbleflux/fluxbase-sdk";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to list migrations in a namespace
|
|
12
|
+
*/
|
|
13
|
+
export function useMigrations(namespace?: string) {
|
|
14
|
+
const client = useFluxbaseClient();
|
|
15
|
+
|
|
16
|
+
return useQuery({
|
|
17
|
+
queryKey: ["fluxbase", "migrations", namespace],
|
|
18
|
+
queryFn: async () => {
|
|
19
|
+
const { data, error } = await client.admin.migrations.list(namespace);
|
|
20
|
+
if (error) throw error;
|
|
21
|
+
return data || [];
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook to apply a specific migration
|
|
28
|
+
*/
|
|
29
|
+
export function useApplyMigration() {
|
|
30
|
+
const client = useFluxbaseClient();
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
|
|
33
|
+
return useMutation({
|
|
34
|
+
mutationFn: async ({
|
|
35
|
+
name,
|
|
36
|
+
namespace,
|
|
37
|
+
}: {
|
|
38
|
+
name: string;
|
|
39
|
+
namespace?: string;
|
|
40
|
+
}) => {
|
|
41
|
+
const { data, error } = await client.admin.migrations.apply(
|
|
42
|
+
name,
|
|
43
|
+
namespace,
|
|
44
|
+
);
|
|
45
|
+
if (error) throw error;
|
|
46
|
+
return data;
|
|
47
|
+
},
|
|
48
|
+
onSuccess: () => {
|
|
49
|
+
queryClient.invalidateQueries({
|
|
50
|
+
queryKey: ["fluxbase", "migrations"],
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hook to rollback a specific migration
|
|
58
|
+
*/
|
|
59
|
+
export function useRollbackMigration() {
|
|
60
|
+
const client = useFluxbaseClient();
|
|
61
|
+
const queryClient = useQueryClient();
|
|
62
|
+
|
|
63
|
+
return useMutation({
|
|
64
|
+
mutationFn: async ({
|
|
65
|
+
name,
|
|
66
|
+
namespace,
|
|
67
|
+
}: {
|
|
68
|
+
name: string;
|
|
69
|
+
namespace?: string;
|
|
70
|
+
}) => {
|
|
71
|
+
const { data, error } = await client.admin.migrations.rollback(
|
|
72
|
+
name,
|
|
73
|
+
namespace,
|
|
74
|
+
);
|
|
75
|
+
if (error) throw error;
|
|
76
|
+
return data;
|
|
77
|
+
},
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
queryClient.invalidateQueries({
|
|
80
|
+
queryKey: ["fluxbase", "migrations"],
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hook to sync registered migrations
|
|
88
|
+
*/
|
|
89
|
+
export function useSyncMigrations() {
|
|
90
|
+
const client = useFluxbaseClient();
|
|
91
|
+
const queryClient = useQueryClient();
|
|
92
|
+
|
|
93
|
+
return useMutation({
|
|
94
|
+
mutationFn: async (options: SyncMigrationsOptions | void) => {
|
|
95
|
+
const { data, error } = await client.admin.migrations.sync(
|
|
96
|
+
options ?? undefined,
|
|
97
|
+
);
|
|
98
|
+
if (error) throw error;
|
|
99
|
+
return data;
|
|
100
|
+
},
|
|
101
|
+
onSuccess: () => {
|
|
102
|
+
queryClient.invalidateQueries({
|
|
103
|
+
queryKey: ["fluxbase", "migrations"],
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|