@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,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Service Keys hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
7
|
+
import {
|
|
8
|
+
useServiceKeys,
|
|
9
|
+
useCreateServiceKey,
|
|
10
|
+
useRotateServiceKey,
|
|
11
|
+
useRevokeServiceKey,
|
|
12
|
+
} from "./use-service-keys";
|
|
13
|
+
import { createMockClient, createWrapper } from "./test-utils";
|
|
14
|
+
|
|
15
|
+
describe("useServiceKeys", () => {
|
|
16
|
+
it("should list service keys", async () => {
|
|
17
|
+
const mockKeys = [
|
|
18
|
+
{
|
|
19
|
+
id: "1",
|
|
20
|
+
name: "Production Key",
|
|
21
|
+
key_type: "service" as const,
|
|
22
|
+
scopes: ["*"],
|
|
23
|
+
enabled: true,
|
|
24
|
+
key_prefix: "fb_prod_abcdef",
|
|
25
|
+
created_at: "",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
const client = createMockClient();
|
|
29
|
+
(
|
|
30
|
+
client.admin.serviceKeys.list as ReturnType<typeof vi.fn>
|
|
31
|
+
).mockResolvedValue({ data: mockKeys, error: null });
|
|
32
|
+
|
|
33
|
+
const { result } = renderHook(() => useServiceKeys(), {
|
|
34
|
+
wrapper: createWrapper(client),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
38
|
+
expect(result.current.data).toEqual(mockKeys);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should handle errors", async () => {
|
|
42
|
+
const client = createMockClient();
|
|
43
|
+
(
|
|
44
|
+
client.admin.serviceKeys.list as ReturnType<typeof vi.fn>
|
|
45
|
+
).mockResolvedValue({ data: null, error: new Error("Unauthorized") });
|
|
46
|
+
|
|
47
|
+
const { result } = renderHook(() => useServiceKeys(), {
|
|
48
|
+
wrapper: createWrapper(client),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
52
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("useCreateServiceKey", () => {
|
|
57
|
+
it("should create a service key and invalidate queries", async () => {
|
|
58
|
+
const mockKey = {
|
|
59
|
+
id: "2",
|
|
60
|
+
name: "New Key",
|
|
61
|
+
key_type: "service" as const,
|
|
62
|
+
scopes: ["*"],
|
|
63
|
+
enabled: true,
|
|
64
|
+
key_prefix: "fb_new_123456",
|
|
65
|
+
key: "fb_new_123456789",
|
|
66
|
+
created_at: "",
|
|
67
|
+
};
|
|
68
|
+
const client = createMockClient();
|
|
69
|
+
(
|
|
70
|
+
client.admin.serviceKeys.create as ReturnType<typeof vi.fn>
|
|
71
|
+
).mockResolvedValue({ data: mockKey, error: null });
|
|
72
|
+
|
|
73
|
+
const { result } = renderHook(() => useCreateServiceKey(), {
|
|
74
|
+
wrapper: createWrapper(client),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await act(async () => {
|
|
78
|
+
await result.current.mutateAsync({
|
|
79
|
+
name: "New Key",
|
|
80
|
+
key_type: "service",
|
|
81
|
+
scopes: ["*"],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(client.admin.serviceKeys.create).toHaveBeenCalledWith({
|
|
86
|
+
name: "New Key",
|
|
87
|
+
key_type: "service",
|
|
88
|
+
scopes: ["*"],
|
|
89
|
+
});
|
|
90
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
91
|
+
expect(result.current.data).toEqual(mockKey);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle errors", async () => {
|
|
95
|
+
const client = createMockClient();
|
|
96
|
+
(
|
|
97
|
+
client.admin.serviceKeys.create as ReturnType<typeof vi.fn>
|
|
98
|
+
).mockResolvedValue({ data: null, error: new Error("Limit reached") });
|
|
99
|
+
|
|
100
|
+
const { result } = renderHook(() => useCreateServiceKey(), {
|
|
101
|
+
wrapper: createWrapper(client),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await act(async () => {
|
|
105
|
+
try {
|
|
106
|
+
await result.current.mutateAsync({
|
|
107
|
+
name: "Excess",
|
|
108
|
+
key_type: "service",
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// expected
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("useRotateServiceKey", () => {
|
|
120
|
+
it("should rotate a service key and invalidate queries", async () => {
|
|
121
|
+
const mockKey = {
|
|
122
|
+
id: "3",
|
|
123
|
+
name: "Rotated Key",
|
|
124
|
+
key_type: "service" as const,
|
|
125
|
+
scopes: ["*"],
|
|
126
|
+
enabled: true,
|
|
127
|
+
key_prefix: "fb_rot_newpass",
|
|
128
|
+
key: "fb_rot_newpassword",
|
|
129
|
+
created_at: "",
|
|
130
|
+
deprecated_at: "",
|
|
131
|
+
grace_period_ends_at: "",
|
|
132
|
+
};
|
|
133
|
+
const client = createMockClient();
|
|
134
|
+
(
|
|
135
|
+
client.admin.serviceKeys.rotate as ReturnType<typeof vi.fn>
|
|
136
|
+
).mockResolvedValue({ data: mockKey, error: null });
|
|
137
|
+
|
|
138
|
+
const { result } = renderHook(() => useRotateServiceKey(), {
|
|
139
|
+
wrapper: createWrapper(client),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await act(async () => {
|
|
143
|
+
await result.current.mutateAsync("old-key-id");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(client.admin.serviceKeys.rotate).toHaveBeenCalledWith("old-key-id");
|
|
147
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
148
|
+
expect(result.current.data).toEqual(mockKey);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should handle errors", async () => {
|
|
152
|
+
const client = createMockClient();
|
|
153
|
+
(
|
|
154
|
+
client.admin.serviceKeys.rotate as ReturnType<typeof vi.fn>
|
|
155
|
+
).mockResolvedValue({ data: null, error: new Error("Key not found") });
|
|
156
|
+
|
|
157
|
+
const { result } = renderHook(() => useRotateServiceKey(), {
|
|
158
|
+
wrapper: createWrapper(client),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await act(async () => {
|
|
162
|
+
try {
|
|
163
|
+
await result.current.mutateAsync("missing-id");
|
|
164
|
+
} catch {
|
|
165
|
+
// expected
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("useRevokeServiceKey", () => {
|
|
174
|
+
it("should revoke a service key and invalidate queries", async () => {
|
|
175
|
+
const client = createMockClient();
|
|
176
|
+
(
|
|
177
|
+
client.admin.serviceKeys.revoke as ReturnType<typeof vi.fn>
|
|
178
|
+
).mockResolvedValue({ error: null });
|
|
179
|
+
|
|
180
|
+
const { result } = renderHook(() => useRevokeServiceKey(), {
|
|
181
|
+
wrapper: createWrapper(client),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await act(async () => {
|
|
185
|
+
await result.current.mutateAsync({ id: "compromised-id" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(client.admin.serviceKeys.revoke).toHaveBeenCalledWith(
|
|
189
|
+
"compromised-id",
|
|
190
|
+
undefined,
|
|
191
|
+
);
|
|
192
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should pass revocation reason", async () => {
|
|
196
|
+
const client = createMockClient();
|
|
197
|
+
(
|
|
198
|
+
client.admin.serviceKeys.revoke as ReturnType<typeof vi.fn>
|
|
199
|
+
).mockResolvedValue({ error: null });
|
|
200
|
+
|
|
201
|
+
const { result } = renderHook(() => useRevokeServiceKey(), {
|
|
202
|
+
wrapper: createWrapper(client),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await act(async () => {
|
|
206
|
+
await result.current.mutateAsync({
|
|
207
|
+
id: "key-id",
|
|
208
|
+
request: { reason: "Key was compromised" },
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(client.admin.serviceKeys.revoke).toHaveBeenCalledWith("key-id", {
|
|
213
|
+
reason: "Key was compromised",
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should handle errors", async () => {
|
|
218
|
+
const client = createMockClient();
|
|
219
|
+
(
|
|
220
|
+
client.admin.serviceKeys.revoke as ReturnType<typeof vi.fn>
|
|
221
|
+
).mockResolvedValue({ error: new Error("Already revoked") });
|
|
222
|
+
|
|
223
|
+
const { result } = renderHook(() => useRevokeServiceKey(), {
|
|
224
|
+
wrapper: createWrapper(client),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await act(async () => {
|
|
228
|
+
try {
|
|
229
|
+
await result.current.mutateAsync({ id: "bad-id" });
|
|
230
|
+
} catch {
|
|
231
|
+
// expected
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service keys hooks for Fluxbase React SDK
|
|
3
|
+
* Provides hooks for managing tenant service keys (anon and service)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { useFluxbaseClient } from "./context";
|
|
8
|
+
import type {
|
|
9
|
+
CreateServiceKeyRequest,
|
|
10
|
+
RevokeServiceKeyRequest,
|
|
11
|
+
} from "@nimbleflux/fluxbase-sdk";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to list all service keys
|
|
15
|
+
*/
|
|
16
|
+
export function useServiceKeys() {
|
|
17
|
+
const client = useFluxbaseClient();
|
|
18
|
+
|
|
19
|
+
return useQuery({
|
|
20
|
+
queryKey: ["fluxbase", "service-keys"],
|
|
21
|
+
queryFn: async () => {
|
|
22
|
+
const { data, error } = await client.admin.serviceKeys.list();
|
|
23
|
+
if (error) throw error;
|
|
24
|
+
return data;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Hook to create a new service key
|
|
31
|
+
*
|
|
32
|
+
* The full key value is only returned once — store it securely!
|
|
33
|
+
*/
|
|
34
|
+
export function useCreateServiceKey() {
|
|
35
|
+
const client = useFluxbaseClient();
|
|
36
|
+
const queryClient = useQueryClient();
|
|
37
|
+
|
|
38
|
+
return useMutation({
|
|
39
|
+
mutationFn: async (request: CreateServiceKeyRequest) => {
|
|
40
|
+
const { data, error } = await client.admin.serviceKeys.create(request);
|
|
41
|
+
if (error) throw error;
|
|
42
|
+
return data;
|
|
43
|
+
},
|
|
44
|
+
onSuccess: () => {
|
|
45
|
+
queryClient.invalidateQueries({ queryKey: ["fluxbase", "service-keys"] });
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hook to rotate a service key (creates a replacement and deprecates the old one)
|
|
52
|
+
*/
|
|
53
|
+
export function useRotateServiceKey() {
|
|
54
|
+
const client = useFluxbaseClient();
|
|
55
|
+
const queryClient = useQueryClient();
|
|
56
|
+
|
|
57
|
+
return useMutation({
|
|
58
|
+
mutationFn: async (id: string) => {
|
|
59
|
+
const { data, error } = await client.admin.serviceKeys.rotate(id);
|
|
60
|
+
if (error) throw error;
|
|
61
|
+
return data;
|
|
62
|
+
},
|
|
63
|
+
onSuccess: () => {
|
|
64
|
+
queryClient.invalidateQueries({ queryKey: ["fluxbase", "service-keys"] });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to revoke a service key permanently (emergency)
|
|
71
|
+
*/
|
|
72
|
+
export function useRevokeServiceKey() {
|
|
73
|
+
const client = useFluxbaseClient();
|
|
74
|
+
const queryClient = useQueryClient();
|
|
75
|
+
|
|
76
|
+
return useMutation({
|
|
77
|
+
mutationFn: async (params: {
|
|
78
|
+
id: string;
|
|
79
|
+
request?: RevokeServiceKeyRequest;
|
|
80
|
+
}) => {
|
|
81
|
+
const { error } = await client.admin.serviceKeys.revoke(
|
|
82
|
+
params.id,
|
|
83
|
+
params.request,
|
|
84
|
+
);
|
|
85
|
+
if (error) throw error;
|
|
86
|
+
},
|
|
87
|
+
onSuccess: () => {
|
|
88
|
+
queryClient.invalidateQueries({ queryKey: ["fluxbase", "service-keys"] });
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Vector hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
7
|
+
import { useVectorEmbed, useVectorSearch } from "./use-vector";
|
|
8
|
+
import { createMockClient, createWrapper } from "./test-utils";
|
|
9
|
+
|
|
10
|
+
describe("useVectorEmbed", () => {
|
|
11
|
+
it("should embed text", async () => {
|
|
12
|
+
const mockResponse = {
|
|
13
|
+
embeddings: [[0.1, 0.2, 0.3]],
|
|
14
|
+
model: "text-embedding-3-small",
|
|
15
|
+
dimensions: 3,
|
|
16
|
+
};
|
|
17
|
+
const client = createMockClient();
|
|
18
|
+
(client.vector.embed as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
19
|
+
data: mockResponse,
|
|
20
|
+
error: null,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const { result } = renderHook(() => useVectorEmbed(), {
|
|
24
|
+
wrapper: createWrapper(client),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await act(async () => {
|
|
28
|
+
await result.current.mutateAsync({ text: "Hello world" });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(client.vector.embed).toHaveBeenCalledWith({ text: "Hello world" });
|
|
32
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
33
|
+
expect(result.current.data).toEqual(mockResponse);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should embed multiple texts", async () => {
|
|
37
|
+
const mockResponse = {
|
|
38
|
+
embeddings: [[0.1], [0.2]],
|
|
39
|
+
model: "text-embedding-3-small",
|
|
40
|
+
dimensions: 1,
|
|
41
|
+
};
|
|
42
|
+
const client = createMockClient();
|
|
43
|
+
(client.vector.embed as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
44
|
+
data: mockResponse,
|
|
45
|
+
error: null,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { result } = renderHook(() => useVectorEmbed(), {
|
|
49
|
+
wrapper: createWrapper(client),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await act(async () => {
|
|
53
|
+
await result.current.mutateAsync({
|
|
54
|
+
texts: ["Hello", "World"],
|
|
55
|
+
model: "text-embedding-3-large",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(client.vector.embed).toHaveBeenCalledWith({
|
|
60
|
+
texts: ["Hello", "World"],
|
|
61
|
+
model: "text-embedding-3-large",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle errors", async () => {
|
|
66
|
+
const client = createMockClient();
|
|
67
|
+
(client.vector.embed as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
68
|
+
data: null,
|
|
69
|
+
error: new Error("Embedding failed"),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(() => useVectorEmbed(), {
|
|
73
|
+
wrapper: createWrapper(client),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await act(async () => {
|
|
77
|
+
try {
|
|
78
|
+
await result.current.mutateAsync({ text: "test" });
|
|
79
|
+
} catch {
|
|
80
|
+
// expected
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("useVectorSearch", () => {
|
|
89
|
+
it("should search with text query", async () => {
|
|
90
|
+
const mockResult = {
|
|
91
|
+
data: [{ id: "1", content: "Match 1" }],
|
|
92
|
+
distances: [0.1],
|
|
93
|
+
model: "text-embedding-3-small",
|
|
94
|
+
};
|
|
95
|
+
const client = createMockClient();
|
|
96
|
+
(client.vector.search as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
97
|
+
data: mockResult,
|
|
98
|
+
error: null,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const { result } = renderHook(() => useVectorSearch(), {
|
|
102
|
+
wrapper: createWrapper(client),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const searchOpts = {
|
|
106
|
+
table: "documents",
|
|
107
|
+
column: "embedding",
|
|
108
|
+
query: "How to use TypeScript?",
|
|
109
|
+
match_count: 10,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
await act(async () => {
|
|
113
|
+
await result.current.mutateAsync(searchOpts);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(client.vector.search).toHaveBeenCalledWith(searchOpts);
|
|
117
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
118
|
+
expect(result.current.data).toEqual(mockResult);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should search with pre-computed vector", async () => {
|
|
122
|
+
const mockResult = {
|
|
123
|
+
data: [{ id: "2", content: "Vector match" }],
|
|
124
|
+
distances: [0.05],
|
|
125
|
+
};
|
|
126
|
+
const client = createMockClient();
|
|
127
|
+
(client.vector.search as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
128
|
+
data: mockResult,
|
|
129
|
+
error: null,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const { result } = renderHook(() => useVectorSearch(), {
|
|
133
|
+
wrapper: createWrapper(client),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const searchOpts = {
|
|
137
|
+
table: "documents",
|
|
138
|
+
column: "embedding",
|
|
139
|
+
vector: [0.1, 0.2, 0.3],
|
|
140
|
+
metric: "cosine" as const,
|
|
141
|
+
match_count: 5,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
await result.current.mutateAsync(searchOpts);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(client.vector.search).toHaveBeenCalledWith(searchOpts);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should handle errors", async () => {
|
|
152
|
+
const client = createMockClient();
|
|
153
|
+
(client.vector.search as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
154
|
+
data: null,
|
|
155
|
+
error: new Error("Search failed"),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { result } = renderHook(() => useVectorSearch(), {
|
|
159
|
+
wrapper: createWrapper(client),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await act(async () => {
|
|
163
|
+
try {
|
|
164
|
+
await result.current.mutateAsync({
|
|
165
|
+
table: "documents",
|
|
166
|
+
column: "embedding",
|
|
167
|
+
query: "test",
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
// expected
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector hooks for Fluxbase React SDK
|
|
3
|
+
* Provides hooks for vector embedding and similarity search
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation } from "@tanstack/react-query";
|
|
7
|
+
import { useFluxbaseClient } from "./context";
|
|
8
|
+
import type { EmbedRequest, VectorSearchOptions } from "@nimbleflux/fluxbase-sdk";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to generate embeddings for text
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const { mutateAsync } = useVectorEmbed()
|
|
16
|
+
* const { data } = await mutateAsync({ text: 'Hello world' })
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function useVectorEmbed() {
|
|
20
|
+
const client = useFluxbaseClient();
|
|
21
|
+
|
|
22
|
+
return useMutation({
|
|
23
|
+
mutationFn: async (request: EmbedRequest) => {
|
|
24
|
+
const { data, error } = await client.vector.embed(request);
|
|
25
|
+
if (error) throw error;
|
|
26
|
+
return data;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hook for vector similarity search with automatic text embedding
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const { mutateAsync } = useVectorSearch()
|
|
37
|
+
* const { data } = await mutateAsync({
|
|
38
|
+
* table: 'documents',
|
|
39
|
+
* column: 'embedding',
|
|
40
|
+
* query: 'How to use TypeScript?',
|
|
41
|
+
* match_count: 10,
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useVectorSearch() {
|
|
46
|
+
const client = useFluxbaseClient();
|
|
47
|
+
|
|
48
|
+
return useMutation({
|
|
49
|
+
mutationFn: async (options: VectorSearchOptions) => {
|
|
50
|
+
const { data, error } = await client.vector.search(options);
|
|
51
|
+
if (error) throw error;
|
|
52
|
+
return data;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|