@nimbleflux/fluxbase-sdk-react 2026.3.6-rc.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.
Files changed (42) hide show
  1. package/.nvmrc +1 -0
  2. package/README-ADMIN.md +1076 -0
  3. package/README.md +195 -0
  4. package/examples/AdminDashboard.tsx +513 -0
  5. package/examples/README.md +163 -0
  6. package/package.json +66 -0
  7. package/src/context.test.tsx +147 -0
  8. package/src/context.tsx +33 -0
  9. package/src/index.test.ts +255 -0
  10. package/src/index.ts +175 -0
  11. package/src/test-setup.ts +22 -0
  12. package/src/test-utils.tsx +215 -0
  13. package/src/use-admin-auth.test.ts +175 -0
  14. package/src/use-admin-auth.ts +187 -0
  15. package/src/use-admin-hooks.test.ts +457 -0
  16. package/src/use-admin-hooks.ts +309 -0
  17. package/src/use-auth-config.test.ts +145 -0
  18. package/src/use-auth-config.ts +101 -0
  19. package/src/use-auth.test.ts +313 -0
  20. package/src/use-auth.ts +164 -0
  21. package/src/use-captcha.test.ts +273 -0
  22. package/src/use-captcha.ts +250 -0
  23. package/src/use-client-keys.test.ts +286 -0
  24. package/src/use-client-keys.ts +185 -0
  25. package/src/use-graphql.test.ts +424 -0
  26. package/src/use-graphql.ts +392 -0
  27. package/src/use-query.test.ts +348 -0
  28. package/src/use-query.ts +211 -0
  29. package/src/use-realtime.test.ts +359 -0
  30. package/src/use-realtime.ts +180 -0
  31. package/src/use-saml.test.ts +269 -0
  32. package/src/use-saml.ts +221 -0
  33. package/src/use-storage.test.ts +549 -0
  34. package/src/use-storage.ts +508 -0
  35. package/src/use-table-export.ts +481 -0
  36. package/src/use-users.test.ts +264 -0
  37. package/src/use-users.ts +198 -0
  38. package/tsconfig.json +28 -0
  39. package/tsconfig.tsbuildinfo +1 -0
  40. package/tsup.config.ts +11 -0
  41. package/typedoc.json +33 -0
  42. package/vitest.config.ts +22 -0
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Tests for database query hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import {
8
+ useFluxbaseQuery,
9
+ useTable,
10
+ useInsert,
11
+ useUpdate,
12
+ useUpsert,
13
+ useDelete,
14
+ } from './use-query';
15
+ import { createMockClient, createWrapper, createTestQueryClient } from './test-utils';
16
+
17
+ describe('useFluxbaseQuery', () => {
18
+ it('should execute query and return data', async () => {
19
+ const mockData = [{ id: 1, name: 'Test' }];
20
+ const executeMock = vi.fn().mockResolvedValue({ data: mockData, error: null });
21
+ const fromMock = vi.fn().mockReturnValue({
22
+ select: vi.fn().mockReturnThis(),
23
+ execute: executeMock,
24
+ });
25
+ const client = createMockClient({ from: fromMock } as any);
26
+
27
+ const { result } = renderHook(
28
+ () => useFluxbaseQuery((client) => client.from('products').select('*'), { queryKey: ['products'] }),
29
+ { wrapper: createWrapper(client) }
30
+ );
31
+
32
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
33
+ expect(result.current.data).toEqual(mockData);
34
+ });
35
+
36
+ it('should throw error when query fails', async () => {
37
+ const error = new Error('Query failed');
38
+ const executeMock = vi.fn().mockResolvedValue({ data: null, error });
39
+ const fromMock = vi.fn().mockReturnValue({
40
+ select: vi.fn().mockReturnThis(),
41
+ execute: executeMock,
42
+ });
43
+ const client = createMockClient({ from: fromMock } as any);
44
+
45
+ const { result } = renderHook(
46
+ () => useFluxbaseQuery((client) => client.from('products').select('*'), { queryKey: ['products'] }),
47
+ { wrapper: createWrapper(client) }
48
+ );
49
+
50
+ await waitFor(() => expect(result.current.isError).toBe(true));
51
+ expect(result.current.error).toBe(error);
52
+ });
53
+
54
+ it('should handle single item response', async () => {
55
+ const mockData = { id: 1, name: 'Test' };
56
+ const executeMock = vi.fn().mockResolvedValue({ data: mockData, error: null });
57
+ const fromMock = vi.fn().mockReturnValue({
58
+ select: vi.fn().mockReturnThis(),
59
+ single: vi.fn().mockReturnThis(),
60
+ execute: executeMock,
61
+ });
62
+ const client = createMockClient({ from: fromMock } as any);
63
+
64
+ const { result } = renderHook(
65
+ () => useFluxbaseQuery((client) => client.from('products').select('*'), { queryKey: ['product'] }),
66
+ { wrapper: createWrapper(client) }
67
+ );
68
+
69
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
70
+ expect(result.current.data).toEqual([mockData]);
71
+ });
72
+
73
+ it('should handle null response', async () => {
74
+ const executeMock = vi.fn().mockResolvedValue({ data: null, error: null });
75
+ const fromMock = vi.fn().mockReturnValue({
76
+ select: vi.fn().mockReturnThis(),
77
+ execute: executeMock,
78
+ });
79
+ const client = createMockClient({ from: fromMock } as any);
80
+
81
+ const { result } = renderHook(
82
+ () => useFluxbaseQuery((client) => client.from('products').select('*'), { queryKey: ['products'] }),
83
+ { wrapper: createWrapper(client) }
84
+ );
85
+
86
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
87
+ expect(result.current.data).toEqual([]);
88
+ });
89
+
90
+ it('should generate query key from function when not provided', async () => {
91
+ const mockData = [{ id: 1 }];
92
+ const executeMock = vi.fn().mockResolvedValue({ data: mockData, error: null });
93
+ const fromMock = vi.fn().mockReturnValue({
94
+ select: vi.fn().mockReturnThis(),
95
+ execute: executeMock,
96
+ });
97
+ const client = createMockClient({ from: fromMock } as any);
98
+
99
+ const buildQuery = (client: any) => client.from('test').select('*');
100
+ const { result } = renderHook(
101
+ () => useFluxbaseQuery(buildQuery),
102
+ { wrapper: createWrapper(client) }
103
+ );
104
+
105
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
106
+ expect(result.current.data).toEqual(mockData);
107
+ });
108
+ });
109
+
110
+ describe('useTable', () => {
111
+ it('should query table with builder function', async () => {
112
+ const mockData = [{ id: 1, name: 'Test' }];
113
+ const executeMock = vi.fn().mockResolvedValue({ data: mockData, error: null });
114
+ const fromMock = vi.fn().mockReturnValue({
115
+ select: vi.fn().mockReturnThis(),
116
+ eq: vi.fn().mockReturnThis(),
117
+ execute: executeMock,
118
+ });
119
+ const client = createMockClient({ from: fromMock } as any);
120
+
121
+ const { result } = renderHook(
122
+ () => useTable('products', (q) => q.select('*').eq('active', true)),
123
+ { wrapper: createWrapper(client) }
124
+ );
125
+
126
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
127
+ expect(result.current.data).toEqual(mockData);
128
+ expect(fromMock).toHaveBeenCalledWith('products');
129
+ });
130
+
131
+ it('should query table without builder function', async () => {
132
+ const mockData = [{ id: 1 }];
133
+ const executeMock = vi.fn().mockResolvedValue({ data: mockData, error: null });
134
+ const fromMock = vi.fn().mockReturnValue({
135
+ execute: executeMock,
136
+ });
137
+ const client = createMockClient({ from: fromMock } as any);
138
+
139
+ const { result } = renderHook(
140
+ () => useTable('products'),
141
+ { wrapper: createWrapper(client) }
142
+ );
143
+
144
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
145
+ expect(result.current.data).toEqual(mockData);
146
+ });
147
+
148
+ it('should support custom query key', async () => {
149
+ const mockData = [{ id: 1 }];
150
+ const executeMock = vi.fn().mockResolvedValue({ data: mockData, error: null });
151
+ const fromMock = vi.fn().mockReturnValue({
152
+ execute: executeMock,
153
+ });
154
+ const client = createMockClient({ from: fromMock } as any);
155
+
156
+ const queryClient = createTestQueryClient();
157
+ const { result } = renderHook(
158
+ () => useTable('products', undefined, { queryKey: ['custom', 'key'] }),
159
+ { wrapper: createWrapper(client, queryClient) }
160
+ );
161
+
162
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
163
+ expect(queryClient.getQueryData(['custom', 'key'])).toEqual(mockData);
164
+ });
165
+ });
166
+
167
+ describe('useInsert', () => {
168
+ it('should insert data and invalidate queries', async () => {
169
+ const mockResult = { id: 1, name: 'New Item' };
170
+ const insertMock = vi.fn().mockResolvedValue({ data: mockResult, error: null });
171
+ const fromMock = vi.fn().mockReturnValue({
172
+ insert: insertMock,
173
+ });
174
+ const client = createMockClient({ from: fromMock } as any);
175
+
176
+ const queryClient = createTestQueryClient();
177
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
178
+
179
+ const { result } = renderHook(() => useInsert('products'), {
180
+ wrapper: createWrapper(client, queryClient),
181
+ });
182
+
183
+ await act(async () => {
184
+ await result.current.mutateAsync({ name: 'New Item' });
185
+ });
186
+
187
+ expect(fromMock).toHaveBeenCalledWith('products');
188
+ expect(insertMock).toHaveBeenCalledWith({ name: 'New Item' });
189
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'table', 'products'] });
190
+ });
191
+
192
+ it('should throw error on insert failure', async () => {
193
+ const error = new Error('Insert failed');
194
+ const insertMock = vi.fn().mockResolvedValue({ data: null, error });
195
+ const fromMock = vi.fn().mockReturnValue({
196
+ insert: insertMock,
197
+ });
198
+ const client = createMockClient({ from: fromMock } as any);
199
+
200
+ const { result } = renderHook(() => useInsert('products'), {
201
+ wrapper: createWrapper(client),
202
+ });
203
+
204
+ await expect(act(async () => {
205
+ await result.current.mutateAsync({ name: 'New Item' });
206
+ })).rejects.toThrow('Insert failed');
207
+ });
208
+ });
209
+
210
+ describe('useUpdate', () => {
211
+ it('should update data and invalidate queries', async () => {
212
+ const mockResult = { id: 1, name: 'Updated' };
213
+ const updateMock = vi.fn().mockResolvedValue({ data: mockResult, error: null });
214
+ const eqMock = vi.fn().mockReturnThis();
215
+ const fromMock = vi.fn().mockReturnValue({
216
+ eq: eqMock,
217
+ update: updateMock,
218
+ });
219
+ const client = createMockClient({ from: fromMock } as any);
220
+
221
+ const queryClient = createTestQueryClient();
222
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
223
+
224
+ const { result } = renderHook(() => useUpdate('products'), {
225
+ wrapper: createWrapper(client, queryClient),
226
+ });
227
+
228
+ await act(async () => {
229
+ await result.current.mutateAsync({
230
+ data: { name: 'Updated' },
231
+ buildQuery: (q) => q.eq('id', 1),
232
+ });
233
+ });
234
+
235
+ expect(fromMock).toHaveBeenCalledWith('products');
236
+ expect(updateMock).toHaveBeenCalledWith({ name: 'Updated' });
237
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'table', 'products'] });
238
+ });
239
+
240
+ it('should throw error on update failure', async () => {
241
+ const error = new Error('Update failed');
242
+ const updateMock = vi.fn().mockResolvedValue({ data: null, error });
243
+ const fromMock = vi.fn().mockReturnValue({
244
+ eq: vi.fn().mockReturnThis(),
245
+ update: updateMock,
246
+ });
247
+ const client = createMockClient({ from: fromMock } as any);
248
+
249
+ const { result } = renderHook(() => useUpdate('products'), {
250
+ wrapper: createWrapper(client),
251
+ });
252
+
253
+ await expect(act(async () => {
254
+ await result.current.mutateAsync({
255
+ data: { name: 'Updated' },
256
+ buildQuery: (q) => q.eq('id', 1),
257
+ });
258
+ })).rejects.toThrow('Update failed');
259
+ });
260
+ });
261
+
262
+ describe('useUpsert', () => {
263
+ it('should upsert data and invalidate queries', async () => {
264
+ const mockResult = { id: 1, name: 'Upserted' };
265
+ const upsertMock = vi.fn().mockResolvedValue({ data: mockResult, error: null });
266
+ const fromMock = vi.fn().mockReturnValue({
267
+ upsert: upsertMock,
268
+ });
269
+ const client = createMockClient({ from: fromMock } as any);
270
+
271
+ const queryClient = createTestQueryClient();
272
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
273
+
274
+ const { result } = renderHook(() => useUpsert('products'), {
275
+ wrapper: createWrapper(client, queryClient),
276
+ });
277
+
278
+ await act(async () => {
279
+ await result.current.mutateAsync({ id: 1, name: 'Upserted' });
280
+ });
281
+
282
+ expect(fromMock).toHaveBeenCalledWith('products');
283
+ expect(upsertMock).toHaveBeenCalledWith({ id: 1, name: 'Upserted' });
284
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'table', 'products'] });
285
+ });
286
+
287
+ it('should throw error on upsert failure', async () => {
288
+ const error = new Error('Upsert failed');
289
+ const upsertMock = vi.fn().mockResolvedValue({ data: null, error });
290
+ const fromMock = vi.fn().mockReturnValue({
291
+ upsert: upsertMock,
292
+ });
293
+ const client = createMockClient({ from: fromMock } as any);
294
+
295
+ const { result } = renderHook(() => useUpsert('products'), {
296
+ wrapper: createWrapper(client),
297
+ });
298
+
299
+ await expect(act(async () => {
300
+ await result.current.mutateAsync({ id: 1, name: 'Upserted' });
301
+ })).rejects.toThrow('Upsert failed');
302
+ });
303
+ });
304
+
305
+ describe('useDelete', () => {
306
+ it('should delete data and invalidate queries', async () => {
307
+ const deleteMock = vi.fn().mockResolvedValue({ data: null, error: null });
308
+ const eqMock = vi.fn().mockReturnThis();
309
+ const fromMock = vi.fn().mockReturnValue({
310
+ eq: eqMock,
311
+ delete: deleteMock,
312
+ });
313
+ const client = createMockClient({ from: fromMock } as any);
314
+
315
+ const queryClient = createTestQueryClient();
316
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
317
+
318
+ const { result } = renderHook(() => useDelete('products'), {
319
+ wrapper: createWrapper(client, queryClient),
320
+ });
321
+
322
+ await act(async () => {
323
+ await result.current.mutateAsync((q) => q.eq('id', 1));
324
+ });
325
+
326
+ expect(fromMock).toHaveBeenCalledWith('products');
327
+ expect(deleteMock).toHaveBeenCalled();
328
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['fluxbase', 'table', 'products'] });
329
+ });
330
+
331
+ it('should throw error on delete failure', async () => {
332
+ const error = new Error('Delete failed');
333
+ const deleteMock = vi.fn().mockResolvedValue({ error });
334
+ const fromMock = vi.fn().mockReturnValue({
335
+ eq: vi.fn().mockReturnThis(),
336
+ delete: deleteMock,
337
+ });
338
+ const client = createMockClient({ from: fromMock } as any);
339
+
340
+ const { result } = renderHook(() => useDelete('products'), {
341
+ wrapper: createWrapper(client),
342
+ });
343
+
344
+ await expect(act(async () => {
345
+ await result.current.mutateAsync((q) => q.eq('id', 1));
346
+ })).rejects.toThrow('Delete failed');
347
+ });
348
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Database query hooks for Fluxbase SDK
3
+ */
4
+
5
+ import { useQuery, useMutation, useQueryClient, type UseQueryOptions } from '@tanstack/react-query'
6
+ import { useFluxbaseClient } from './context'
7
+ import type { QueryBuilder } from '@fluxbase/sdk'
8
+
9
+ export interface UseFluxbaseQueryOptions<T> extends Omit<UseQueryOptions<T[], Error>, 'queryKey' | 'queryFn'> {
10
+ /**
11
+ * Custom query key. If not provided, will use table name and filters.
12
+ */
13
+ queryKey?: unknown[]
14
+ }
15
+
16
+ /**
17
+ * Hook to execute a database query
18
+ * @param buildQuery - Function that builds and returns the query
19
+ * @param options - React Query options
20
+ *
21
+ * IMPORTANT: You must provide a stable `queryKey` in options for proper caching.
22
+ * Without a custom queryKey, each render may create a new cache entry.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * // Always provide a queryKey for stable caching
27
+ * useFluxbaseQuery(
28
+ * (client) => client.from('users').select('*'),
29
+ * { queryKey: ['users', 'all'] }
30
+ * )
31
+ * ```
32
+ */
33
+ export function useFluxbaseQuery<T = any>(
34
+ buildQuery: (client: ReturnType<typeof useFluxbaseClient>) => QueryBuilder<T>,
35
+ options?: UseFluxbaseQueryOptions<T>
36
+ ) {
37
+ const client = useFluxbaseClient()
38
+
39
+ // Require queryKey for stable caching - function.toString() is not reliable
40
+ // as it can vary between renders for inline functions
41
+ if (!options?.queryKey) {
42
+ console.warn(
43
+ '[useFluxbaseQuery] No queryKey provided. This may cause cache misses. ' +
44
+ 'Please provide a stable queryKey in options.'
45
+ )
46
+ }
47
+
48
+ const queryKey = options?.queryKey || ['fluxbase', 'query', 'unstable']
49
+
50
+ return useQuery({
51
+ queryKey,
52
+ queryFn: async () => {
53
+ const query = buildQuery(client)
54
+ const { data, error } = await query.execute()
55
+
56
+ if (error) {
57
+ throw error
58
+ }
59
+
60
+ return (Array.isArray(data) ? data : data ? [data] : []) as T[]
61
+ },
62
+ ...options,
63
+ })
64
+ }
65
+
66
+ /**
67
+ * Hook for table queries with a simpler API
68
+ * @param table - Table name
69
+ * @param buildQuery - Optional function to build the query (e.g., add filters)
70
+ * @param options - Query options including a stable queryKey
71
+ *
72
+ * NOTE: When using buildQuery with filters, provide a custom queryKey that includes
73
+ * the filter values to ensure proper caching.
74
+ *
75
+ * @example
76
+ * ```tsx
77
+ * // Simple query - queryKey is auto-generated from table name
78
+ * useTable('users')
79
+ *
80
+ * // With filters - provide queryKey including filter values
81
+ * useTable('users',
82
+ * (q) => q.eq('status', 'active'),
83
+ * { queryKey: ['users', 'active'] }
84
+ * )
85
+ * ```
86
+ */
87
+ export function useTable<T = any>(
88
+ table: string,
89
+ buildQuery?: (query: QueryBuilder<T>) => QueryBuilder<T>,
90
+ options?: UseFluxbaseQueryOptions<T>
91
+ ) {
92
+ const client = useFluxbaseClient()
93
+
94
+ // Generate a stable base queryKey from table name
95
+ // When buildQuery is provided without a custom queryKey, warn about potential cache issues
96
+ if (buildQuery && !options?.queryKey) {
97
+ console.warn(
98
+ `[useTable] Using buildQuery without a custom queryKey for table "${table}". ` +
99
+ 'This may cause cache misses. Provide a queryKey that includes your filter values.'
100
+ )
101
+ }
102
+
103
+ return useFluxbaseQuery(
104
+ (client) => {
105
+ const query = client.from<T>(table)
106
+ return buildQuery ? buildQuery(query) : query
107
+ },
108
+ {
109
+ ...options,
110
+ // Use table name as base key, or custom key if provided
111
+ queryKey: options?.queryKey || ['fluxbase', 'table', table],
112
+ }
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Hook to insert data into a table
118
+ */
119
+ export function useInsert<T = any>(table: string) {
120
+ const client = useFluxbaseClient()
121
+ const queryClient = useQueryClient()
122
+
123
+ return useMutation({
124
+ mutationFn: async (data: Partial<T> | Partial<T>[]) => {
125
+ const query = client.from<T>(table)
126
+ const { data: result, error } = await query.insert(data as Partial<T>)
127
+
128
+ if (error) {
129
+ throw error
130
+ }
131
+
132
+ return result
133
+ },
134
+ onSuccess: () => {
135
+ // Invalidate all queries for this table
136
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
137
+ },
138
+ })
139
+ }
140
+
141
+ /**
142
+ * Hook to update data in a table
143
+ */
144
+ export function useUpdate<T = any>(table: string) {
145
+ const client = useFluxbaseClient()
146
+ const queryClient = useQueryClient()
147
+
148
+ return useMutation({
149
+ mutationFn: async (params: { data: Partial<T>; buildQuery: (query: QueryBuilder<T>) => QueryBuilder<T> }) => {
150
+ const query = client.from<T>(table)
151
+ const builtQuery = params.buildQuery(query)
152
+ const { data: result, error } = await builtQuery.update(params.data)
153
+
154
+ if (error) {
155
+ throw error
156
+ }
157
+
158
+ return result
159
+ },
160
+ onSuccess: () => {
161
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
162
+ },
163
+ })
164
+ }
165
+
166
+ /**
167
+ * Hook to upsert data into a table
168
+ */
169
+ export function useUpsert<T = any>(table: string) {
170
+ const client = useFluxbaseClient()
171
+ const queryClient = useQueryClient()
172
+
173
+ return useMutation({
174
+ mutationFn: async (data: Partial<T> | Partial<T>[]) => {
175
+ const query = client.from<T>(table)
176
+ const { data: result, error } = await query.upsert(data as Partial<T>)
177
+
178
+ if (error) {
179
+ throw error
180
+ }
181
+
182
+ return result
183
+ },
184
+ onSuccess: () => {
185
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
186
+ },
187
+ })
188
+ }
189
+
190
+ /**
191
+ * Hook to delete data from a table
192
+ */
193
+ export function useDelete<T = any>(table: string) {
194
+ const client = useFluxbaseClient()
195
+ const queryClient = useQueryClient()
196
+
197
+ return useMutation({
198
+ mutationFn: async (buildQuery: (query: QueryBuilder<T>) => QueryBuilder<T>) => {
199
+ const query = client.from<T>(table)
200
+ const builtQuery = buildQuery(query)
201
+ const { error } = await builtQuery.delete()
202
+
203
+ if (error) {
204
+ throw error
205
+ }
206
+ },
207
+ onSuccess: () => {
208
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
209
+ },
210
+ })
211
+ }