@nimbleflux/fluxbase-sdk-react 2026.5.5-rc.1 → 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 +53 -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 +77 -0
- 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 +25 -0
- 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 +81 -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-query.ts +25 -10
- 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-tenant.ts +55 -75
- 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
|
+
}
|
package/src/use-tenant.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @module use-tenant
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useState,
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
9
|
import { useFluxbaseClient } from "./context";
|
|
9
10
|
import type {
|
|
10
11
|
Tenant,
|
|
@@ -98,58 +99,55 @@ export interface UseTenantsReturn {
|
|
|
98
99
|
export function useTenants(options: UseTenantsOptions = {}): UseTenantsReturn {
|
|
99
100
|
const { autoFetch = true } = options;
|
|
100
101
|
const client = useFluxbaseClient();
|
|
102
|
+
const queryClient = useQueryClient();
|
|
101
103
|
|
|
102
|
-
const [tenants, setTenants] = useState<TenantWithRole[]>([]);
|
|
103
|
-
const [isLoading, setIsLoading] = useState(autoFetch);
|
|
104
|
-
const [error, setError] = useState<Error | null>(null);
|
|
105
104
|
const [currentTenantId, setCurrentTenantId] = useState<string | undefined>(
|
|
106
105
|
client.getTenantId(),
|
|
107
106
|
);
|
|
108
107
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
setError(err as Error);
|
|
121
|
-
} finally {
|
|
122
|
-
setIsLoading(false);
|
|
123
|
-
}
|
|
124
|
-
}, [client]);
|
|
108
|
+
const query = useQuery({
|
|
109
|
+
queryKey: ["fluxbase", "tenants", "mine"],
|
|
110
|
+
queryFn: async () => {
|
|
111
|
+
const { data, error } = await client.tenant.listMine();
|
|
112
|
+
if (error) throw error;
|
|
113
|
+
return data || ([] as TenantWithRole[]);
|
|
114
|
+
},
|
|
115
|
+
enabled: autoFetch,
|
|
116
|
+
});
|
|
125
117
|
|
|
126
118
|
const createTenant = useCallback(
|
|
127
119
|
async (opts: CreateTenantOptions): Promise<Tenant> => {
|
|
128
120
|
const { data, error: createError } = await client.tenant.create(opts);
|
|
129
121
|
if (createError) throw createError;
|
|
130
|
-
await
|
|
122
|
+
await queryClient.invalidateQueries({
|
|
123
|
+
queryKey: ["fluxbase", "tenants"],
|
|
124
|
+
});
|
|
131
125
|
return data!;
|
|
132
126
|
},
|
|
133
|
-
[client,
|
|
127
|
+
[client, queryClient],
|
|
134
128
|
);
|
|
135
129
|
|
|
136
130
|
const updateTenant = useCallback(
|
|
137
131
|
async (id: string, opts: UpdateTenantOptions): Promise<Tenant> => {
|
|
138
132
|
const { data, error: updateError } = await client.tenant.update(id, opts);
|
|
139
133
|
if (updateError) throw updateError;
|
|
140
|
-
await
|
|
134
|
+
await queryClient.invalidateQueries({
|
|
135
|
+
queryKey: ["fluxbase", "tenants"],
|
|
136
|
+
});
|
|
141
137
|
return data!;
|
|
142
138
|
},
|
|
143
|
-
[client,
|
|
139
|
+
[client, queryClient],
|
|
144
140
|
);
|
|
145
141
|
|
|
146
142
|
const deleteTenant = useCallback(
|
|
147
143
|
async (id: string): Promise<void> => {
|
|
148
144
|
const { error: deleteError } = await client.tenant.delete(id);
|
|
149
145
|
if (deleteError) throw deleteError;
|
|
150
|
-
await
|
|
146
|
+
await queryClient.invalidateQueries({
|
|
147
|
+
queryKey: ["fluxbase", "tenants"],
|
|
148
|
+
});
|
|
151
149
|
},
|
|
152
|
-
[client,
|
|
150
|
+
[client, queryClient],
|
|
153
151
|
);
|
|
154
152
|
|
|
155
153
|
const setCurrentTenant = useCallback(
|
|
@@ -160,17 +158,13 @@ export function useTenants(options: UseTenantsOptions = {}): UseTenantsReturn {
|
|
|
160
158
|
[client],
|
|
161
159
|
);
|
|
162
160
|
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
if (autoFetch) {
|
|
165
|
-
fetchTenants();
|
|
166
|
-
}
|
|
167
|
-
}, [autoFetch, fetchTenants]);
|
|
168
|
-
|
|
169
161
|
return {
|
|
170
|
-
tenants,
|
|
171
|
-
isLoading,
|
|
172
|
-
error,
|
|
173
|
-
refetch:
|
|
162
|
+
tenants: query.data ?? [],
|
|
163
|
+
isLoading: autoFetch ? query.isLoading : false,
|
|
164
|
+
error: query.error,
|
|
165
|
+
refetch: async () => {
|
|
166
|
+
await query.refetch();
|
|
167
|
+
},
|
|
174
168
|
createTenant,
|
|
175
169
|
updateTenant,
|
|
176
170
|
deleteTenant,
|
|
@@ -249,31 +243,17 @@ export interface UseTenantReturn {
|
|
|
249
243
|
export function useTenant(options: UseTenantOptions): UseTenantReturn {
|
|
250
244
|
const { tenantId, autoFetch = true } = options;
|
|
251
245
|
const client = useFluxbaseClient();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const { data, error: fetchError } = await client.tenant.get(tenantId);
|
|
264
|
-
if (fetchError) {
|
|
265
|
-
setError(fetchError);
|
|
266
|
-
setTenant(null);
|
|
267
|
-
} else {
|
|
268
|
-
setTenant(data);
|
|
269
|
-
}
|
|
270
|
-
} catch (err) {
|
|
271
|
-
setError(err as Error);
|
|
272
|
-
setTenant(null);
|
|
273
|
-
} finally {
|
|
274
|
-
setIsLoading(false);
|
|
275
|
-
}
|
|
276
|
-
}, [client, tenantId]);
|
|
246
|
+
const queryClient = useQueryClient();
|
|
247
|
+
|
|
248
|
+
const query = useQuery({
|
|
249
|
+
queryKey: ["fluxbase", "tenant", tenantId],
|
|
250
|
+
queryFn: async () => {
|
|
251
|
+
const { data, error } = await client.tenant.get(tenantId);
|
|
252
|
+
if (error) throw error;
|
|
253
|
+
return data;
|
|
254
|
+
},
|
|
255
|
+
enabled: autoFetch && !!tenantId,
|
|
256
|
+
});
|
|
277
257
|
|
|
278
258
|
const update = useCallback(
|
|
279
259
|
async (opts: UpdateTenantOptions): Promise<Tenant> => {
|
|
@@ -282,29 +262,29 @@ export function useTenant(options: UseTenantOptions): UseTenantReturn {
|
|
|
282
262
|
opts,
|
|
283
263
|
);
|
|
284
264
|
if (updateError) throw updateError;
|
|
285
|
-
|
|
265
|
+
await queryClient.invalidateQueries({
|
|
266
|
+
queryKey: ["fluxbase", "tenant", tenantId],
|
|
267
|
+
});
|
|
286
268
|
return data!;
|
|
287
269
|
},
|
|
288
|
-
[client, tenantId],
|
|
270
|
+
[client, tenantId, queryClient],
|
|
289
271
|
);
|
|
290
272
|
|
|
291
273
|
const remove = useCallback(async (): Promise<void> => {
|
|
292
274
|
const { error: deleteError } = await client.tenant.delete(tenantId);
|
|
293
275
|
if (deleteError) throw deleteError;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (autoFetch && tenantId) {
|
|
299
|
-
fetchTenant();
|
|
300
|
-
}
|
|
301
|
-
}, [autoFetch, fetchTenant, tenantId]);
|
|
276
|
+
await queryClient.invalidateQueries({
|
|
277
|
+
queryKey: ["fluxbase", "tenant", tenantId],
|
|
278
|
+
});
|
|
279
|
+
}, [client, tenantId, queryClient]);
|
|
302
280
|
|
|
303
281
|
return {
|
|
304
|
-
tenant,
|
|
305
|
-
isLoading,
|
|
306
|
-
error,
|
|
307
|
-
refetch:
|
|
282
|
+
tenant: query.data ?? null,
|
|
283
|
+
isLoading: autoFetch ? query.isLoading : false,
|
|
284
|
+
error: query.error,
|
|
285
|
+
refetch: async () => {
|
|
286
|
+
await query.refetch();
|
|
287
|
+
},
|
|
308
288
|
update,
|
|
309
289
|
remove,
|
|
310
290
|
};
|
|
@@ -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
|
+
});
|