@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,264 @@
1
+ /**
2
+ * Tests for users management hook
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import { useUsers } from './use-users';
8
+ import { createMockClient, createWrapper } from './test-utils';
9
+
10
+ describe('useUsers', () => {
11
+ it('should fetch users on mount when autoFetch is true', async () => {
12
+ const mockUsers = [
13
+ { id: '1', email: 'user1@example.com', role: 'user' },
14
+ { id: '2', email: 'user2@example.com', role: 'admin' },
15
+ ];
16
+ const listUsersMock = vi.fn().mockResolvedValue({
17
+ data: { users: mockUsers, total: 2 },
18
+ error: null,
19
+ });
20
+
21
+ const client = createMockClient({
22
+ admin: { listUsers: listUsersMock },
23
+ } as any);
24
+
25
+ const { result } = renderHook(
26
+ () => useUsers({ autoFetch: true }),
27
+ { wrapper: createWrapper(client) }
28
+ );
29
+
30
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
31
+ expect(result.current.users).toEqual(mockUsers);
32
+ expect(result.current.total).toBe(2);
33
+ });
34
+
35
+ it('should not fetch users when autoFetch is false', async () => {
36
+ const listUsersMock = vi.fn();
37
+
38
+ const client = createMockClient({
39
+ admin: { listUsers: listUsersMock },
40
+ } as any);
41
+
42
+ const { result } = renderHook(
43
+ () => useUsers({ autoFetch: false }),
44
+ { wrapper: createWrapper(client) }
45
+ );
46
+
47
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
48
+ expect(listUsersMock).not.toHaveBeenCalled();
49
+ });
50
+
51
+ it('should pass list options', async () => {
52
+ const listUsersMock = vi.fn().mockResolvedValue({
53
+ data: { users: [], total: 0 },
54
+ error: null,
55
+ });
56
+
57
+ const client = createMockClient({
58
+ admin: { listUsers: listUsersMock },
59
+ } as any);
60
+
61
+ renderHook(
62
+ () => useUsers({ autoFetch: true, limit: 10, search: 'test' }),
63
+ { wrapper: createWrapper(client) }
64
+ );
65
+
66
+ await waitFor(() => {
67
+ expect(listUsersMock).toHaveBeenCalledWith(
68
+ expect.objectContaining({ limit: 10, search: 'test' })
69
+ );
70
+ });
71
+ });
72
+
73
+ it('should invite user', async () => {
74
+ const listUsersMock = vi.fn().mockResolvedValue({
75
+ data: { users: [], total: 0 },
76
+ error: null,
77
+ });
78
+ const inviteUserMock = vi.fn().mockResolvedValue({ data: {}, error: null });
79
+
80
+ const client = createMockClient({
81
+ admin: {
82
+ listUsers: listUsersMock,
83
+ inviteUser: inviteUserMock,
84
+ },
85
+ } as any);
86
+
87
+ const { result } = renderHook(
88
+ () => useUsers({ autoFetch: true }),
89
+ { wrapper: createWrapper(client) }
90
+ );
91
+
92
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
93
+
94
+ await act(async () => {
95
+ await result.current.inviteUser('new@example.com', 'user');
96
+ });
97
+
98
+ expect(inviteUserMock).toHaveBeenCalledWith({ email: 'new@example.com', role: 'user' });
99
+ // Should refetch after invite
100
+ expect(listUsersMock).toHaveBeenCalledTimes(2);
101
+ });
102
+
103
+ it('should update user role', async () => {
104
+ const listUsersMock = vi.fn().mockResolvedValue({
105
+ data: { users: [], total: 0 },
106
+ error: null,
107
+ });
108
+ const updateUserRoleMock = vi.fn().mockResolvedValue({ data: {}, error: null });
109
+
110
+ const client = createMockClient({
111
+ admin: {
112
+ listUsers: listUsersMock,
113
+ updateUserRole: updateUserRoleMock,
114
+ },
115
+ } as any);
116
+
117
+ const { result } = renderHook(
118
+ () => useUsers({ autoFetch: true }),
119
+ { wrapper: createWrapper(client) }
120
+ );
121
+
122
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
123
+
124
+ await act(async () => {
125
+ await result.current.updateUserRole('1', 'admin');
126
+ });
127
+
128
+ expect(updateUserRoleMock).toHaveBeenCalledWith('1', 'admin');
129
+ // Should refetch after update
130
+ expect(listUsersMock).toHaveBeenCalledTimes(2);
131
+ });
132
+
133
+ it('should delete user', async () => {
134
+ const listUsersMock = vi.fn().mockResolvedValue({
135
+ data: { users: [], total: 0 },
136
+ error: null,
137
+ });
138
+ const deleteUserMock = vi.fn().mockResolvedValue({ data: {}, error: null });
139
+
140
+ const client = createMockClient({
141
+ admin: {
142
+ listUsers: listUsersMock,
143
+ deleteUser: deleteUserMock,
144
+ },
145
+ } as any);
146
+
147
+ const { result } = renderHook(
148
+ () => useUsers({ autoFetch: true }),
149
+ { wrapper: createWrapper(client) }
150
+ );
151
+
152
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
153
+
154
+ await act(async () => {
155
+ await result.current.deleteUser('1');
156
+ });
157
+
158
+ expect(deleteUserMock).toHaveBeenCalledWith('1');
159
+ // Should refetch after delete
160
+ expect(listUsersMock).toHaveBeenCalledTimes(2);
161
+ });
162
+
163
+ it('should reset user password', async () => {
164
+ const listUsersMock = vi.fn().mockResolvedValue({
165
+ data: { users: [], total: 0 },
166
+ error: null,
167
+ });
168
+ const resetPasswordMock = vi.fn().mockResolvedValue({
169
+ data: { message: 'Password reset email sent' },
170
+ error: null,
171
+ });
172
+
173
+ const client = createMockClient({
174
+ admin: {
175
+ listUsers: listUsersMock,
176
+ resetUserPassword: resetPasswordMock,
177
+ },
178
+ } as any);
179
+
180
+ const { result } = renderHook(
181
+ () => useUsers({ autoFetch: true }),
182
+ { wrapper: createWrapper(client) }
183
+ );
184
+
185
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
186
+
187
+ let response;
188
+ await act(async () => {
189
+ response = await result.current.resetPassword('1');
190
+ });
191
+
192
+ expect(resetPasswordMock).toHaveBeenCalledWith('1');
193
+ expect(response).toEqual({ message: 'Password reset email sent' });
194
+ });
195
+
196
+ it('should handle fetch error', async () => {
197
+ const error = new Error('Failed to fetch');
198
+ const listUsersMock = vi.fn().mockResolvedValue({ data: null, error });
199
+
200
+ const client = createMockClient({
201
+ admin: { listUsers: listUsersMock },
202
+ } as any);
203
+
204
+ const { result } = renderHook(
205
+ () => useUsers({ autoFetch: true }),
206
+ { wrapper: createWrapper(client) }
207
+ );
208
+
209
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
210
+ expect(result.current.error).toBe(error);
211
+ });
212
+
213
+ it('should set up refetch interval', async () => {
214
+ vi.useFakeTimers();
215
+ const listUsersMock = vi.fn().mockResolvedValue({
216
+ data: { users: [], total: 0 },
217
+ error: null,
218
+ });
219
+
220
+ const client = createMockClient({
221
+ admin: { listUsers: listUsersMock },
222
+ } as any);
223
+
224
+ const { unmount } = renderHook(
225
+ () => useUsers({ autoFetch: true, refetchInterval: 5000 }),
226
+ { wrapper: createWrapper(client) }
227
+ );
228
+
229
+ // Initial fetch
230
+ expect(listUsersMock).toHaveBeenCalledTimes(1);
231
+
232
+ // Advance timer
233
+ await act(async () => {
234
+ vi.advanceTimersByTime(5000);
235
+ });
236
+
237
+ expect(listUsersMock).toHaveBeenCalledTimes(2);
238
+
239
+ unmount();
240
+ vi.useRealTimers();
241
+ });
242
+
243
+ it('should refetch on demand', async () => {
244
+ const listUsersMock = vi.fn().mockResolvedValue({
245
+ data: { users: [], total: 0 },
246
+ error: null,
247
+ });
248
+
249
+ const client = createMockClient({
250
+ admin: { listUsers: listUsersMock },
251
+ } as any);
252
+
253
+ const { result } = renderHook(
254
+ () => useUsers({ autoFetch: false }),
255
+ { wrapper: createWrapper(client) }
256
+ );
257
+
258
+ await act(async () => {
259
+ await result.current.refetch();
260
+ });
261
+
262
+ expect(listUsersMock).toHaveBeenCalledTimes(1);
263
+ });
264
+ });
@@ -0,0 +1,198 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { useFluxbaseClient } from './context'
3
+ import type { EnrichedUser, ListUsersOptions } from '@fluxbase/sdk'
4
+
5
+ export interface UseUsersOptions extends ListUsersOptions {
6
+ /**
7
+ * Whether to automatically fetch users on mount
8
+ * @default true
9
+ */
10
+ autoFetch?: boolean
11
+
12
+ /**
13
+ * Refetch interval in milliseconds (0 to disable)
14
+ * @default 0
15
+ */
16
+ refetchInterval?: number
17
+ }
18
+
19
+ export interface UseUsersReturn {
20
+ /**
21
+ * Array of users
22
+ */
23
+ users: EnrichedUser[]
24
+
25
+ /**
26
+ * Total number of users (for pagination)
27
+ */
28
+ total: number
29
+
30
+ /**
31
+ * Whether users are being fetched
32
+ */
33
+ isLoading: boolean
34
+
35
+ /**
36
+ * Any error that occurred
37
+ */
38
+ error: Error | null
39
+
40
+ /**
41
+ * Refetch users
42
+ */
43
+ refetch: () => Promise<void>
44
+
45
+ /**
46
+ * Invite a new user
47
+ */
48
+ inviteUser: (email: string, role: 'user' | 'admin') => Promise<void>
49
+
50
+ /**
51
+ * Update user role
52
+ */
53
+ updateUserRole: (userId: string, role: 'user' | 'admin') => Promise<void>
54
+
55
+ /**
56
+ * Delete a user
57
+ */
58
+ deleteUser: (userId: string) => Promise<void>
59
+
60
+ /**
61
+ * Reset user password
62
+ */
63
+ resetPassword: (userId: string) => Promise<{ message: string }>
64
+ }
65
+
66
+ /**
67
+ * Hook for managing users
68
+ *
69
+ * Provides user list with pagination, search, and management functions.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * function UserList() {
74
+ * const { users, total, isLoading, refetch, inviteUser, deleteUser } = useUsers({
75
+ * limit: 20,
76
+ * search: searchTerm
77
+ * })
78
+ *
79
+ * return (
80
+ * <div>
81
+ * {isLoading ? <Spinner /> : (
82
+ * <ul>
83
+ * {users.map(user => (
84
+ * <li key={user.id}>
85
+ * {user.email} - {user.role}
86
+ * <button onClick={() => deleteUser(user.id)}>Delete</button>
87
+ * </li>
88
+ * ))}
89
+ * </ul>
90
+ * )}
91
+ * </div>
92
+ * )
93
+ * }
94
+ * ```
95
+ */
96
+ export function useUsers(options: UseUsersOptions = {}): UseUsersReturn {
97
+ const { autoFetch = true, refetchInterval = 0, ...listOptions } = options
98
+ const client = useFluxbaseClient()
99
+
100
+ const [users, setUsers] = useState<EnrichedUser[]>([])
101
+ const [total, setTotal] = useState(0)
102
+ const [isLoading, setIsLoading] = useState(autoFetch)
103
+ const [error, setError] = useState<Error | null>(null)
104
+
105
+ /**
106
+ * Fetch users from API
107
+ */
108
+ const fetchUsers = useCallback(async () => {
109
+ try {
110
+ setIsLoading(true)
111
+ setError(null)
112
+ const { data, error: apiError } = await client.admin.listUsers(listOptions)
113
+ if (apiError) {
114
+ throw apiError
115
+ }
116
+ setUsers(data!.users)
117
+ setTotal(data!.total)
118
+ } catch (err) {
119
+ setError(err as Error)
120
+ } finally {
121
+ setIsLoading(false)
122
+ }
123
+ }, [client, JSON.stringify(listOptions)])
124
+
125
+ /**
126
+ * Invite a new user
127
+ */
128
+ const inviteUser = useCallback(
129
+ async (email: string, role: 'user' | 'admin'): Promise<void> => {
130
+ await client.admin.inviteUser({ email, role })
131
+ await fetchUsers() // Refresh list
132
+ },
133
+ [client, fetchUsers]
134
+ )
135
+
136
+ /**
137
+ * Update user role
138
+ */
139
+ const updateUserRole = useCallback(
140
+ async (userId: string, role: 'user' | 'admin'): Promise<void> => {
141
+ await client.admin.updateUserRole(userId, role)
142
+ await fetchUsers() // Refresh list
143
+ },
144
+ [client, fetchUsers]
145
+ )
146
+
147
+ /**
148
+ * Delete a user
149
+ */
150
+ const deleteUser = useCallback(
151
+ async (userId: string): Promise<void> => {
152
+ await client.admin.deleteUser(userId)
153
+ await fetchUsers() // Refresh list
154
+ },
155
+ [client, fetchUsers]
156
+ )
157
+
158
+ /**
159
+ * Reset user password
160
+ */
161
+ const resetPassword = useCallback(
162
+ async (userId: string): Promise<{ message: string }> => {
163
+ const { data, error } = await client.admin.resetUserPassword(userId)
164
+ if (error) {
165
+ throw error
166
+ }
167
+ return data!
168
+ },
169
+ [client]
170
+ )
171
+
172
+ // Auto-fetch on mount
173
+ useEffect(() => {
174
+ if (autoFetch) {
175
+ fetchUsers()
176
+ }
177
+ }, [autoFetch, fetchUsers])
178
+
179
+ // Set up refetch interval
180
+ useEffect(() => {
181
+ if (refetchInterval > 0) {
182
+ const interval = setInterval(fetchUsers, refetchInterval)
183
+ return () => clearInterval(interval)
184
+ }
185
+ }, [refetchInterval, fetchUsers])
186
+
187
+ return {
188
+ users,
189
+ total,
190
+ isLoading,
191
+ error,
192
+ refetch: fetchUsers,
193
+ inviteUser,
194
+ updateUserRole,
195
+ deleteUser,
196
+ resetPassword
197
+ }
198
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "jsx": "react-jsx",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "esModuleInterop": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noEmit": true,
21
+ "baseUrl": ".",
22
+ "paths": {
23
+ "@fluxbase/sdk": ["../sdk/dist/index.d.ts"]
24
+ }
25
+ },
26
+ "include": ["src/**/*"],
27
+ "exclude": ["node_modules", "dist"]
28
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/context.tsx","./src/index.ts","./src/use-admin-auth.ts","./src/use-admin-hooks.ts","./src/use-client-keys.ts","./src/use-auth.ts","./src/use-query.ts","./src/use-realtime.ts","./src/use-rpc.ts","./src/use-storage.ts","./src/use-users.ts"],"version":"5.9.3"}
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm', 'cjs'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ external: ['react', '@tanstack/react-query', '@fluxbase/sdk'],
11
+ })
package/typedoc.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://typedoc.org/schema.json",
3
+ "entryPoints": ["src/index.ts"],
4
+ "out": "../docs/static/api/sdk-react",
5
+ "name": "@nimbleflux/fluxbase-sdk-react",
6
+ "readme": "README.md",
7
+ "plugin": [],
8
+ "excludePrivate": true,
9
+ "excludeProtected": false,
10
+ "excludeInternal": true,
11
+ "includeVersion": true,
12
+ "searchInComments": true,
13
+ "validation": {
14
+ "notExported": true,
15
+ "invalidLink": true,
16
+ "notDocumented": false
17
+ },
18
+ "categorizeByGroup": true,
19
+ "categoryOrder": [
20
+ "Context",
21
+ "Authentication Hooks",
22
+ "Database Hooks",
23
+ "Realtime Hooks",
24
+ "Storage Hooks",
25
+ "RPC Hooks",
26
+ "Types",
27
+ "*"
28
+ ],
29
+ "navigationLinks": {
30
+ "GitHub": "https://github.com/nimbleflux/fluxbase",
31
+ "Documentation": "https://fluxbase.eu"
32
+ }
33
+ }
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ globals: true,
7
+ setupFiles: ['./src/test-setup.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html', 'lcov'],
11
+ reportsDirectory: './coverage',
12
+ include: ['src/**/*.ts', 'src/**/*.tsx'],
13
+ exclude: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'src/**/*.d.ts', 'src/test-setup.ts'],
14
+ thresholds: {
15
+ statements: 80,
16
+ branches: 80,
17
+ functions: 60,
18
+ lines: 80
19
+ }
20
+ }
21
+ }
22
+ });