@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/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 (!options?.queryKey) {
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 = options?.queryKey || ["fluxbase", "query", "unstable"];
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
- ...options,
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
- // Use table name as base key, or custom key if provided
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
- // Invalidate all queries for this table
142
- queryClient.invalidateQueries({ queryKey: ["fluxbase", "table", table] });
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
- queryClient.invalidateQueries({ queryKey: ["fluxbase", "table", table] });
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
- queryClient.invalidateQueries({ queryKey: ["fluxbase", "table", table] });
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
- queryClient.invalidateQueries({ queryKey: ["fluxbase", "table", table] });
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
+ }