@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
package/src/use-query.ts
CHANGED
|
@@ -43,17 +43,22 @@ export function useFluxbaseQuery<T = any>(
|
|
|
43
43
|
options?: UseFluxbaseQueryOptions<T>,
|
|
44
44
|
) {
|
|
45
45
|
const client = useFluxbaseClient();
|
|
46
|
+
const tenantId = client.getTenantId();
|
|
47
|
+
|
|
48
|
+
const { queryKey: customKey, ...queryOptions } = options || {};
|
|
46
49
|
|
|
47
50
|
// Require queryKey for stable caching - function.toString() is not reliable
|
|
48
51
|
// as it can vary between renders for inline functions
|
|
49
|
-
if (!
|
|
52
|
+
if (!customKey) {
|
|
50
53
|
console.warn(
|
|
51
54
|
"[useFluxbaseQuery] No queryKey provided. This may cause cache misses. " +
|
|
52
55
|
"Please provide a stable queryKey in options.",
|
|
53
56
|
);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
const queryKey =
|
|
59
|
+
const queryKey = customKey
|
|
60
|
+
? ["fluxbase", tenantId ?? null, ...customKey]
|
|
61
|
+
: ["fluxbase", tenantId ?? null, "query", "unstable"];
|
|
57
62
|
|
|
58
63
|
return useQuery({
|
|
59
64
|
queryKey,
|
|
@@ -67,7 +72,7 @@ export function useFluxbaseQuery<T = any>(
|
|
|
67
72
|
|
|
68
73
|
return (Array.isArray(data) ? data : data ? [data] : []) as T[];
|
|
69
74
|
},
|
|
70
|
-
...
|
|
75
|
+
...queryOptions,
|
|
71
76
|
});
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -113,8 +118,7 @@ export function useTable<T = any>(
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
...options,
|
|
116
|
-
|
|
117
|
-
queryKey: options?.queryKey || ["fluxbase", "table", table],
|
|
121
|
+
queryKey: options?.queryKey || ["table", table],
|
|
118
122
|
},
|
|
119
123
|
);
|
|
120
124
|
}
|
|
@@ -138,8 +142,10 @@ export function useInsert<T = any>(table: string) {
|
|
|
138
142
|
return result;
|
|
139
143
|
},
|
|
140
144
|
onSuccess: () => {
|
|
141
|
-
|
|
142
|
-
queryClient.invalidateQueries({
|
|
145
|
+
const tenantId = client.getTenantId();
|
|
146
|
+
queryClient.invalidateQueries({
|
|
147
|
+
queryKey: ["fluxbase", tenantId ?? null, "table", table],
|
|
148
|
+
});
|
|
143
149
|
},
|
|
144
150
|
});
|
|
145
151
|
}
|
|
@@ -167,7 +173,10 @@ export function useUpdate<T = any>(table: string) {
|
|
|
167
173
|
return result;
|
|
168
174
|
},
|
|
169
175
|
onSuccess: () => {
|
|
170
|
-
|
|
176
|
+
const tenantId = client.getTenantId();
|
|
177
|
+
queryClient.invalidateQueries({
|
|
178
|
+
queryKey: ["fluxbase", tenantId ?? null, "table", table],
|
|
179
|
+
});
|
|
171
180
|
},
|
|
172
181
|
});
|
|
173
182
|
}
|
|
@@ -191,7 +200,10 @@ export function useUpsert<T = any>(table: string) {
|
|
|
191
200
|
return result;
|
|
192
201
|
},
|
|
193
202
|
onSuccess: () => {
|
|
194
|
-
|
|
203
|
+
const tenantId = client.getTenantId();
|
|
204
|
+
queryClient.invalidateQueries({
|
|
205
|
+
queryKey: ["fluxbase", tenantId ?? null, "table", table],
|
|
206
|
+
});
|
|
195
207
|
},
|
|
196
208
|
});
|
|
197
209
|
}
|
|
@@ -216,7 +228,10 @@ export function useDelete<T = any>(table: string) {
|
|
|
216
228
|
}
|
|
217
229
|
},
|
|
218
230
|
onSuccess: () => {
|
|
219
|
-
|
|
231
|
+
const tenantId = client.getTenantId();
|
|
232
|
+
queryClient.invalidateQueries({
|
|
233
|
+
queryKey: ["fluxbase", tenantId ?? null, "table", table],
|
|
234
|
+
});
|
|
220
235
|
},
|
|
221
236
|
});
|
|
222
237
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for RPC hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
7
|
+
import { useRPCList, useInvokeRPC } from "./use-rpc";
|
|
8
|
+
import { createMockClient, createWrapper } from "./test-utils";
|
|
9
|
+
import type { FluxbaseClient } from "@nimbleflux/fluxbase-sdk";
|
|
10
|
+
|
|
11
|
+
describe("useRPCList", () => {
|
|
12
|
+
it("should list RPC procedures", async () => {
|
|
13
|
+
const mockProcedures = [
|
|
14
|
+
{ id: "1", name: "get-users", namespace: "default", enabled: true, version: 1, source: "filesystem", is_public: true, allowed_tables: [], allowed_schemas: [], max_execution_time_seconds: 30, created_at: "", updated_at: "" },
|
|
15
|
+
];
|
|
16
|
+
const client = createMockClient({
|
|
17
|
+
rpc: Object.assign(vi.fn(), {
|
|
18
|
+
list: vi.fn().mockResolvedValue({ data: mockProcedures, error: null }),
|
|
19
|
+
}) as any,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const { result } = renderHook(() => useRPCList(), {
|
|
23
|
+
wrapper: createWrapper(client),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
27
|
+
expect(result.current.data).toEqual(mockProcedures);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should filter by namespace", async () => {
|
|
31
|
+
const list = vi
|
|
32
|
+
.fn()
|
|
33
|
+
.mockResolvedValue({ data: [], error: null });
|
|
34
|
+
const client = createMockClient({
|
|
35
|
+
rpc: Object.assign(vi.fn(), { list }) as any,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
renderHook(() => useRPCList("custom-ns"), {
|
|
39
|
+
wrapper: createWrapper(client),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await waitFor(() => {
|
|
43
|
+
expect(list).toHaveBeenCalledWith("custom-ns");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should handle errors", async () => {
|
|
48
|
+
const client = createMockClient({
|
|
49
|
+
rpc: Object.assign(vi.fn(), {
|
|
50
|
+
list: vi
|
|
51
|
+
.fn()
|
|
52
|
+
.mockResolvedValue({ data: null, error: new Error("Failed") }),
|
|
53
|
+
}) as any,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { result } = renderHook(() => useRPCList(), {
|
|
57
|
+
wrapper: createWrapper(client),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
61
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("useInvokeRPC", () => {
|
|
66
|
+
it("should invoke an RPC procedure", async () => {
|
|
67
|
+
const mockResponse = {
|
|
68
|
+
execution_id: "exec-1",
|
|
69
|
+
status: "completed" as const,
|
|
70
|
+
result: { count: 42 },
|
|
71
|
+
};
|
|
72
|
+
const client = createMockClient({
|
|
73
|
+
rpc: Object.assign(vi.fn(), {
|
|
74
|
+
invoke: vi.fn().mockResolvedValue({ data: mockResponse, error: null }),
|
|
75
|
+
}) as any,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const { result } = renderHook(() => useInvokeRPC(), {
|
|
79
|
+
wrapper: createWrapper(client),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await act(async () => {
|
|
83
|
+
await result.current.mutateAsync({
|
|
84
|
+
name: "get-count",
|
|
85
|
+
payload: { table: "users" },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect((client.rpc as any).invoke).toHaveBeenCalledWith(
|
|
90
|
+
"get-count",
|
|
91
|
+
{ table: "users" },
|
|
92
|
+
undefined,
|
|
93
|
+
);
|
|
94
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
95
|
+
expect(result.current.data).toEqual(mockResponse);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should pass options", async () => {
|
|
99
|
+
const mockResponse = { execution_id: "exec-2", status: "running" as const };
|
|
100
|
+
const client = createMockClient({
|
|
101
|
+
rpc: Object.assign(vi.fn(), {
|
|
102
|
+
invoke: vi.fn().mockResolvedValue({ data: mockResponse, error: null }),
|
|
103
|
+
}) as any,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const { result } = renderHook(() => useInvokeRPC(), {
|
|
107
|
+
wrapper: createWrapper(client),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await act(async () => {
|
|
111
|
+
await result.current.mutateAsync({
|
|
112
|
+
name: "long-task",
|
|
113
|
+
options: { async: true, namespace: "prod" },
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect((client.rpc as any).invoke).toHaveBeenCalledWith(
|
|
118
|
+
"long-task",
|
|
119
|
+
undefined,
|
|
120
|
+
{ async: true, namespace: "prod" },
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should handle errors", async () => {
|
|
125
|
+
const client = createMockClient({
|
|
126
|
+
rpc: Object.assign(vi.fn(), {
|
|
127
|
+
invoke: vi
|
|
128
|
+
.fn()
|
|
129
|
+
.mockResolvedValue({ data: null, error: new Error("Not found") }),
|
|
130
|
+
}) as any,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const { result } = renderHook(() => useInvokeRPC(), {
|
|
134
|
+
wrapper: createWrapper(client),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await act(async () => {
|
|
138
|
+
try {
|
|
139
|
+
await result.current.mutateAsync({ name: "bad-proc" });
|
|
140
|
+
} catch {
|
|
141
|
+
// expected
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/use-rpc.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC hooks for Fluxbase React SDK
|
|
3
|
+
* Provides hooks for listing and invoking RPC procedures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
7
|
+
import { useFluxbaseClient } from "./context";
|
|
8
|
+
|
|
9
|
+
interface RPCInvokeOptions {
|
|
10
|
+
namespace?: string;
|
|
11
|
+
async?: boolean;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to list available RPC procedures
|
|
17
|
+
*/
|
|
18
|
+
export function useRPCList(namespace?: string) {
|
|
19
|
+
const client = useFluxbaseClient();
|
|
20
|
+
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: ["fluxbase", "rpc", namespace],
|
|
23
|
+
queryFn: async () => {
|
|
24
|
+
const { data, error } = await client.rpc.list(namespace);
|
|
25
|
+
if (error) throw error;
|
|
26
|
+
return data;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hook to invoke an RPC procedure
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const { mutateAsync, data } = useInvokeRPC()
|
|
37
|
+
* await mutateAsync({ name: 'get-user-orders', payload: { user_id: '123' } })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useInvokeRPC() {
|
|
41
|
+
const client = useFluxbaseClient();
|
|
42
|
+
|
|
43
|
+
return useMutation({
|
|
44
|
+
mutationFn: async (params: {
|
|
45
|
+
name: string;
|
|
46
|
+
payload?: Record<string, unknown>;
|
|
47
|
+
options?: RPCInvokeOptions;
|
|
48
|
+
}) => {
|
|
49
|
+
const { data, error } = await client.rpc.invoke(
|
|
50
|
+
params.name,
|
|
51
|
+
params.payload,
|
|
52
|
+
params.options,
|
|
53
|
+
);
|
|
54
|
+
if (error) throw error;
|
|
55
|
+
return data;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Secrets hooks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
7
|
+
import {
|
|
8
|
+
useSecrets,
|
|
9
|
+
useCreateSecret,
|
|
10
|
+
useUpdateSecret,
|
|
11
|
+
useDeleteSecret,
|
|
12
|
+
} from "./use-secrets";
|
|
13
|
+
import { createMockClient, createWrapper } from "./test-utils";
|
|
14
|
+
|
|
15
|
+
describe("useSecrets", () => {
|
|
16
|
+
it("should list secrets", async () => {
|
|
17
|
+
const mockSecrets = [
|
|
18
|
+
{ id: "1", name: "API_KEY", scope: "global", version: 1, is_expired: false, created_at: "", updated_at: "" },
|
|
19
|
+
];
|
|
20
|
+
const client = createMockClient();
|
|
21
|
+
(client.secrets.list as ReturnType<typeof vi.fn>).mockResolvedValue(mockSecrets);
|
|
22
|
+
|
|
23
|
+
const { result } = renderHook(() => useSecrets(), {
|
|
24
|
+
wrapper: createWrapper(client),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
28
|
+
expect(result.current.data).toEqual(mockSecrets);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should pass options to list", async () => {
|
|
32
|
+
const client = createMockClient();
|
|
33
|
+
(client.secrets.list as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
34
|
+
|
|
35
|
+
renderHook(() => useSecrets({ scope: "namespace", namespace: "prod" }), {
|
|
36
|
+
wrapper: createWrapper(client),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(client.secrets.list).toHaveBeenCalledWith({
|
|
41
|
+
scope: "namespace",
|
|
42
|
+
namespace: "prod",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should handle errors", async () => {
|
|
48
|
+
const client = createMockClient();
|
|
49
|
+
(client.secrets.list as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
50
|
+
new Error("Unauthorized"),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const { result } = renderHook(() => useSecrets(), {
|
|
54
|
+
wrapper: createWrapper(client),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
58
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("useCreateSecret", () => {
|
|
63
|
+
it("should create a secret and invalidate queries", async () => {
|
|
64
|
+
const mockSecret = {
|
|
65
|
+
id: "2",
|
|
66
|
+
name: "NEW_KEY",
|
|
67
|
+
scope: "global" as const,
|
|
68
|
+
version: 1,
|
|
69
|
+
created_at: "",
|
|
70
|
+
updated_at: "",
|
|
71
|
+
};
|
|
72
|
+
const client = createMockClient();
|
|
73
|
+
(client.secrets.create as ReturnType<typeof vi.fn>).mockResolvedValue(mockSecret);
|
|
74
|
+
|
|
75
|
+
const { result } = renderHook(() => useCreateSecret(), {
|
|
76
|
+
wrapper: createWrapper(client),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await act(async () => {
|
|
80
|
+
await result.current.mutateAsync({
|
|
81
|
+
name: "NEW_KEY",
|
|
82
|
+
value: "secret-value",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(client.secrets.create).toHaveBeenCalledWith({
|
|
87
|
+
name: "NEW_KEY",
|
|
88
|
+
value: "secret-value",
|
|
89
|
+
});
|
|
90
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
91
|
+
expect(result.current.data).toEqual(mockSecret);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle errors", async () => {
|
|
95
|
+
const client = createMockClient();
|
|
96
|
+
(client.secrets.create as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
97
|
+
new Error("Secret exists"),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const { result } = renderHook(() => useCreateSecret(), {
|
|
101
|
+
wrapper: createWrapper(client),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await act(async () => {
|
|
105
|
+
try {
|
|
106
|
+
await result.current.mutateAsync({ name: "DUP", value: "val" });
|
|
107
|
+
} catch {
|
|
108
|
+
// expected
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("useUpdateSecret", () => {
|
|
117
|
+
it("should update a secret and invalidate queries", async () => {
|
|
118
|
+
const mockSecret = {
|
|
119
|
+
id: "1",
|
|
120
|
+
name: "API_KEY",
|
|
121
|
+
scope: "global" as const,
|
|
122
|
+
version: 2,
|
|
123
|
+
created_at: "",
|
|
124
|
+
updated_at: "",
|
|
125
|
+
};
|
|
126
|
+
const client = createMockClient();
|
|
127
|
+
(client.secrets.update as ReturnType<typeof vi.fn>).mockResolvedValue(mockSecret);
|
|
128
|
+
|
|
129
|
+
const { result } = renderHook(() => useUpdateSecret(), {
|
|
130
|
+
wrapper: createWrapper(client),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await act(async () => {
|
|
134
|
+
await result.current.mutateAsync({
|
|
135
|
+
name: "API_KEY",
|
|
136
|
+
request: { value: "new-value" },
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(client.secrets.update).toHaveBeenCalledWith(
|
|
141
|
+
"API_KEY",
|
|
142
|
+
{ value: "new-value" },
|
|
143
|
+
{ namespace: undefined },
|
|
144
|
+
);
|
|
145
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
146
|
+
expect(result.current.data).toEqual(mockSecret);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should pass namespace option", async () => {
|
|
150
|
+
const client = createMockClient();
|
|
151
|
+
(client.secrets.update as ReturnType<typeof vi.fn>).mockResolvedValue({});
|
|
152
|
+
|
|
153
|
+
const { result } = renderHook(() => useUpdateSecret(), {
|
|
154
|
+
wrapper: createWrapper(client),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await act(async () => {
|
|
158
|
+
await result.current.mutateAsync({
|
|
159
|
+
name: "DB_URL",
|
|
160
|
+
request: { value: "postgres://new" },
|
|
161
|
+
namespace: "production",
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(client.secrets.update).toHaveBeenCalledWith(
|
|
166
|
+
"DB_URL",
|
|
167
|
+
{ value: "postgres://new" },
|
|
168
|
+
{ namespace: "production" },
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should handle errors", async () => {
|
|
173
|
+
const client = createMockClient();
|
|
174
|
+
(client.secrets.update as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
175
|
+
new Error("Not found"),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const { result } = renderHook(() => useUpdateSecret(), {
|
|
179
|
+
wrapper: createWrapper(client),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await act(async () => {
|
|
183
|
+
try {
|
|
184
|
+
await result.current.mutateAsync({
|
|
185
|
+
name: "MISSING",
|
|
186
|
+
request: { value: "val" },
|
|
187
|
+
});
|
|
188
|
+
} catch {
|
|
189
|
+
// expected
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("useDeleteSecret", () => {
|
|
198
|
+
it("should delete a secret and invalidate queries", async () => {
|
|
199
|
+
const client = createMockClient();
|
|
200
|
+
(client.secrets.delete as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
201
|
+
|
|
202
|
+
const { result } = renderHook(() => useDeleteSecret(), {
|
|
203
|
+
wrapper: createWrapper(client),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await act(async () => {
|
|
207
|
+
await result.current.mutateAsync({ name: "OLD_KEY" });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(client.secrets.delete).toHaveBeenCalledWith("OLD_KEY", {
|
|
211
|
+
namespace: undefined,
|
|
212
|
+
});
|
|
213
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should pass namespace option", async () => {
|
|
217
|
+
const client = createMockClient();
|
|
218
|
+
(client.secrets.delete as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
219
|
+
|
|
220
|
+
const { result } = renderHook(() => useDeleteSecret(), {
|
|
221
|
+
wrapper: createWrapper(client),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await act(async () => {
|
|
225
|
+
await result.current.mutateAsync({ name: "DB_URL", namespace: "staging" });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(client.secrets.delete).toHaveBeenCalledWith("DB_URL", {
|
|
229
|
+
namespace: "staging",
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should handle errors", async () => {
|
|
234
|
+
const client = createMockClient();
|
|
235
|
+
(client.secrets.delete as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
236
|
+
new Error("Not found"),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const { result } = renderHook(() => useDeleteSecret(), {
|
|
240
|
+
wrapper: createWrapper(client),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await act(async () => {
|
|
244
|
+
try {
|
|
245
|
+
await result.current.mutateAsync({ name: "MISSING" });
|
|
246
|
+
} catch {
|
|
247
|
+
// expected
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets hooks for Fluxbase React SDK
|
|
3
|
+
* Provides hooks for managing edge function and job secrets
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { useFluxbaseClient } from "./context";
|
|
8
|
+
import type {
|
|
9
|
+
ListSecretsOptions,
|
|
10
|
+
CreateSecretRequest,
|
|
11
|
+
UpdateSecretRequest,
|
|
12
|
+
} from "@nimbleflux/fluxbase-sdk";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook to list all secrets (metadata only, never includes values)
|
|
16
|
+
*/
|
|
17
|
+
export function useSecrets(options?: ListSecretsOptions) {
|
|
18
|
+
const client = useFluxbaseClient();
|
|
19
|
+
|
|
20
|
+
return useQuery({
|
|
21
|
+
queryKey: ["fluxbase", "secrets", options],
|
|
22
|
+
queryFn: async () => {
|
|
23
|
+
return await client.secrets.list(options);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook to create a new secret
|
|
30
|
+
*/
|
|
31
|
+
export function useCreateSecret() {
|
|
32
|
+
const client = useFluxbaseClient();
|
|
33
|
+
const queryClient = useQueryClient();
|
|
34
|
+
|
|
35
|
+
return useMutation({
|
|
36
|
+
mutationFn: async (request: CreateSecretRequest) => {
|
|
37
|
+
return await client.secrets.create(request);
|
|
38
|
+
},
|
|
39
|
+
onSuccess: () => {
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: ["fluxbase", "secrets"] });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hook to update a secret by name
|
|
47
|
+
*/
|
|
48
|
+
export function useUpdateSecret() {
|
|
49
|
+
const client = useFluxbaseClient();
|
|
50
|
+
const queryClient = useQueryClient();
|
|
51
|
+
|
|
52
|
+
return useMutation({
|
|
53
|
+
mutationFn: async (params: {
|
|
54
|
+
name: string;
|
|
55
|
+
request: UpdateSecretRequest;
|
|
56
|
+
namespace?: string;
|
|
57
|
+
}) => {
|
|
58
|
+
return await client.secrets.update(params.name, params.request, {
|
|
59
|
+
namespace: params.namespace,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
onSuccess: () => {
|
|
63
|
+
queryClient.invalidateQueries({ queryKey: ["fluxbase", "secrets"] });
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook to delete a secret by name
|
|
70
|
+
*/
|
|
71
|
+
export function useDeleteSecret() {
|
|
72
|
+
const client = useFluxbaseClient();
|
|
73
|
+
const queryClient = useQueryClient();
|
|
74
|
+
|
|
75
|
+
return useMutation({
|
|
76
|
+
mutationFn: async (params: { name: string; namespace?: string }) => {
|
|
77
|
+
await client.secrets.delete(params.name, {
|
|
78
|
+
namespace: params.namespace,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
onSuccess: () => {
|
|
82
|
+
queryClient.invalidateQueries({ queryKey: ["fluxbase", "secrets"] });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|