@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,508 @@
1
+ /**
2
+ * Storage hooks for Fluxbase SDK
3
+ */
4
+
5
+ import { useState } from "react";
6
+ import {
7
+ useMutation,
8
+ useQuery,
9
+ useQueryClient,
10
+ type UseQueryOptions,
11
+ } from "@tanstack/react-query";
12
+ import { useFluxbaseClient } from "./context";
13
+ import type {
14
+ ListOptions,
15
+ UploadOptions,
16
+ UploadProgress,
17
+ TransformOptions,
18
+ SignedUrlOptions,
19
+ } from "@fluxbase/sdk";
20
+
21
+ /**
22
+ * Hook to list files in a bucket
23
+ */
24
+ export function useStorageList(
25
+ bucket: string,
26
+ options?: ListOptions &
27
+ Omit<UseQueryOptions<any[], Error>, "queryKey" | "queryFn">,
28
+ ) {
29
+ const client = useFluxbaseClient();
30
+ const { prefix, limit, offset, ...queryOptions } = options || {};
31
+
32
+ return useQuery({
33
+ queryKey: [
34
+ "fluxbase",
35
+ "storage",
36
+ bucket,
37
+ "list",
38
+ { prefix, limit, offset },
39
+ ],
40
+ queryFn: async () => {
41
+ const { data, error } = await client.storage
42
+ .from(bucket)
43
+ .list({ prefix, limit, offset });
44
+
45
+ if (error) {
46
+ throw error;
47
+ }
48
+
49
+ return data || [];
50
+ },
51
+ ...queryOptions,
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Hook to upload a file to a bucket
57
+ *
58
+ * Note: You can track upload progress by passing an `onUploadProgress` callback in the options:
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const upload = useStorageUpload('avatars')
63
+ *
64
+ * upload.mutate({
65
+ * path: 'user.jpg',
66
+ * file: file,
67
+ * options: {
68
+ * onUploadProgress: (progress) => {
69
+ * console.log(`${progress.percentage}% uploaded`)
70
+ * }
71
+ * }
72
+ * })
73
+ * ```
74
+ *
75
+ * For automatic progress state management, use `useStorageUploadWithProgress` instead.
76
+ */
77
+ export function useStorageUpload(bucket: string) {
78
+ const client = useFluxbaseClient();
79
+ const queryClient = useQueryClient();
80
+
81
+ return useMutation({
82
+ mutationFn: async (params: {
83
+ path: string;
84
+ file: File | Blob | ArrayBuffer;
85
+ options?: UploadOptions;
86
+ }) => {
87
+ const { path, file, options } = params;
88
+ const { data, error } = await client.storage
89
+ .from(bucket)
90
+ .upload(path, file, options);
91
+
92
+ if (error) {
93
+ throw error;
94
+ }
95
+
96
+ return data;
97
+ },
98
+ onSuccess: () => {
99
+ // Invalidate list queries for this bucket
100
+ queryClient.invalidateQueries({
101
+ queryKey: ["fluxbase", "storage", bucket, "list"],
102
+ });
103
+ },
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Hook to upload a file to a bucket with built-in progress tracking
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * const { upload, progress, reset } = useStorageUploadWithProgress('avatars')
113
+ *
114
+ * // Upload with automatic progress tracking
115
+ * upload.mutate({
116
+ * path: 'user.jpg',
117
+ * file: file
118
+ * })
119
+ *
120
+ * // Display progress
121
+ * console.log(progress) // { loaded: 1024, total: 2048, percentage: 50 }
122
+ * ```
123
+ */
124
+ export function useStorageUploadWithProgress(bucket: string) {
125
+ const client = useFluxbaseClient();
126
+ const queryClient = useQueryClient();
127
+ const [progress, setProgress] = useState<UploadProgress | null>(null);
128
+
129
+ const mutation = useMutation({
130
+ mutationFn: async (params: {
131
+ path: string;
132
+ file: File | Blob | ArrayBuffer;
133
+ options?: Omit<UploadOptions, "onUploadProgress">;
134
+ }) => {
135
+ const { path, file, options } = params;
136
+
137
+ // Reset progress at the start of upload
138
+ setProgress({ loaded: 0, total: 0, percentage: 0 });
139
+
140
+ const { data, error } = await client.storage
141
+ .from(bucket)
142
+ .upload(path, file, {
143
+ ...options,
144
+ onUploadProgress: (p: import("@fluxbase/sdk").UploadProgress) => {
145
+ setProgress(p);
146
+ },
147
+ });
148
+
149
+ if (error) {
150
+ throw error;
151
+ }
152
+
153
+ return data;
154
+ },
155
+ onSuccess: () => {
156
+ // Invalidate list queries for this bucket
157
+ queryClient.invalidateQueries({
158
+ queryKey: ["fluxbase", "storage", bucket, "list"],
159
+ });
160
+ },
161
+ onError: () => {
162
+ // Reset progress on error
163
+ setProgress(null);
164
+ },
165
+ });
166
+
167
+ return {
168
+ upload: mutation,
169
+ progress,
170
+ reset: () => setProgress(null),
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Hook to download a file from a bucket
176
+ */
177
+ export function useStorageDownload(
178
+ bucket: string,
179
+ path: string | null,
180
+ enabled = true,
181
+ ) {
182
+ const client = useFluxbaseClient();
183
+
184
+ return useQuery({
185
+ queryKey: ["fluxbase", "storage", bucket, "download", path],
186
+ queryFn: async () => {
187
+ if (!path) {
188
+ return null;
189
+ }
190
+
191
+ const { data, error } = await client.storage.from(bucket).download(path);
192
+
193
+ if (error) {
194
+ throw error;
195
+ }
196
+
197
+ return data;
198
+ },
199
+ enabled: enabled && !!path,
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Hook to delete files from a bucket
205
+ */
206
+ export function useStorageDelete(bucket: string) {
207
+ const client = useFluxbaseClient();
208
+ const queryClient = useQueryClient();
209
+
210
+ return useMutation({
211
+ mutationFn: async (paths: string[]) => {
212
+ const { error } = await client.storage.from(bucket).remove(paths);
213
+
214
+ if (error) {
215
+ throw error;
216
+ }
217
+ },
218
+ onSuccess: () => {
219
+ queryClient.invalidateQueries({
220
+ queryKey: ["fluxbase", "storage", bucket, "list"],
221
+ });
222
+ },
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Hook to get a public URL for a file
228
+ */
229
+ export function useStoragePublicUrl(bucket: string, path: string | null) {
230
+ const client = useFluxbaseClient();
231
+
232
+ if (!path) {
233
+ return null;
234
+ }
235
+
236
+ const { data } = client.storage.from(bucket).getPublicUrl(path);
237
+ return data.publicUrl;
238
+ }
239
+
240
+ /**
241
+ * Hook to get a public URL for an image with transformations applied
242
+ *
243
+ * Only works for image files (JPEG, PNG, WebP, GIF, AVIF, etc.)
244
+ *
245
+ * @param bucket - The storage bucket name
246
+ * @param path - The file path (or null to disable)
247
+ * @param transform - Transformation options (width, height, format, quality, fit)
248
+ *
249
+ * @example
250
+ * ```tsx
251
+ * function ImageThumbnail({ path }: { path: string }) {
252
+ * const url = useStorageTransformUrl('images', path, {
253
+ * width: 300,
254
+ * height: 200,
255
+ * format: 'webp',
256
+ * quality: 85,
257
+ * fit: 'cover'
258
+ * });
259
+ *
260
+ * return <img src={url || ''} alt="Thumbnail" />;
261
+ * }
262
+ * ```
263
+ */
264
+ export function useStorageTransformUrl(
265
+ bucket: string,
266
+ path: string | null,
267
+ transform: TransformOptions,
268
+ ): string | null {
269
+ const client = useFluxbaseClient();
270
+
271
+ if (!path) {
272
+ return null;
273
+ }
274
+
275
+ return client.storage.from(bucket).getTransformUrl(path, transform);
276
+ }
277
+
278
+ /**
279
+ * Hook to create a signed URL
280
+ *
281
+ * @deprecated Use useStorageSignedUrlWithOptions for more control including transforms
282
+ */
283
+ export function useStorageSignedUrl(
284
+ bucket: string,
285
+ path: string | null,
286
+ expiresIn?: number,
287
+ ) {
288
+ const client = useFluxbaseClient();
289
+
290
+ return useQuery({
291
+ queryKey: ["fluxbase", "storage", bucket, "signed-url", path, expiresIn],
292
+ queryFn: async () => {
293
+ if (!path) {
294
+ return null;
295
+ }
296
+
297
+ const { data, error } = await client.storage
298
+ .from(bucket)
299
+ .createSignedUrl(path, { expiresIn });
300
+
301
+ if (error) {
302
+ throw error;
303
+ }
304
+
305
+ return data?.signedUrl || null;
306
+ },
307
+ enabled: !!path,
308
+ // Refresh 1 minute before expiry, but ensure staleTime is never negative
309
+ // For very short expirations (<60s), use half the expiration time
310
+ staleTime: expiresIn
311
+ ? Math.max(expiresIn * 500, expiresIn * 1000 - 60000) // At least half the expiration time
312
+ : 1000 * 60 * 50, // 50 minutes default
313
+ });
314
+ }
315
+
316
+ /**
317
+ * Hook to create a signed URL with full options including image transformations
318
+ *
319
+ * @param bucket - The storage bucket name
320
+ * @param path - The file path (or null to disable)
321
+ * @param options - Signed URL options including expiration and transforms
322
+ *
323
+ * @example
324
+ * ```tsx
325
+ * function SecureThumbnail({ path }: { path: string }) {
326
+ * const { data: url } = useStorageSignedUrlWithOptions('images', path, {
327
+ * expiresIn: 3600,
328
+ * transform: {
329
+ * width: 400,
330
+ * height: 300,
331
+ * format: 'webp',
332
+ * quality: 85,
333
+ * fit: 'cover'
334
+ * }
335
+ * });
336
+ *
337
+ * return <img src={url || ''} alt="Secure Thumbnail" />;
338
+ * }
339
+ * ```
340
+ */
341
+ export function useStorageSignedUrlWithOptions(
342
+ bucket: string,
343
+ path: string | null,
344
+ options?: SignedUrlOptions,
345
+ ) {
346
+ const client = useFluxbaseClient();
347
+ const expiresIn = options?.expiresIn;
348
+
349
+ // Create a stable cache key from transform options
350
+ const transformKey = options?.transform
351
+ ? JSON.stringify(options.transform)
352
+ : null;
353
+
354
+ return useQuery({
355
+ queryKey: [
356
+ "fluxbase",
357
+ "storage",
358
+ bucket,
359
+ "signed-url",
360
+ path,
361
+ expiresIn,
362
+ transformKey,
363
+ ],
364
+ queryFn: async () => {
365
+ if (!path) {
366
+ return null;
367
+ }
368
+
369
+ const { data, error } = await client.storage
370
+ .from(bucket)
371
+ .createSignedUrl(path, options);
372
+
373
+ if (error) {
374
+ throw error;
375
+ }
376
+
377
+ return data?.signedUrl || null;
378
+ },
379
+ enabled: !!path,
380
+ // Refresh 1 minute before expiry, but ensure staleTime is never negative
381
+ // For very short expirations (<60s), use half the expiration time
382
+ staleTime: expiresIn
383
+ ? Math.max(expiresIn * 500, expiresIn * 1000 - 60000) // At least half the expiration time
384
+ : 1000 * 60 * 50, // 50 minutes default
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Hook to move a file
390
+ */
391
+ export function useStorageMove(bucket: string) {
392
+ const client = useFluxbaseClient();
393
+ const queryClient = useQueryClient();
394
+
395
+ return useMutation({
396
+ mutationFn: async (params: { fromPath: string; toPath: string }) => {
397
+ const { fromPath, toPath } = params;
398
+ const { data, error } = await client.storage
399
+ .from(bucket)
400
+ .move(fromPath, toPath);
401
+
402
+ if (error) {
403
+ throw error;
404
+ }
405
+
406
+ return data;
407
+ },
408
+ onSuccess: () => {
409
+ queryClient.invalidateQueries({
410
+ queryKey: ["fluxbase", "storage", bucket, "list"],
411
+ });
412
+ },
413
+ });
414
+ }
415
+
416
+ /**
417
+ * Hook to copy a file
418
+ */
419
+ export function useStorageCopy(bucket: string) {
420
+ const client = useFluxbaseClient();
421
+ const queryClient = useQueryClient();
422
+
423
+ return useMutation({
424
+ mutationFn: async (params: { fromPath: string; toPath: string }) => {
425
+ const { fromPath, toPath } = params;
426
+ const { data, error } = await client.storage
427
+ .from(bucket)
428
+ .copy(fromPath, toPath);
429
+
430
+ if (error) {
431
+ throw error;
432
+ }
433
+
434
+ return data;
435
+ },
436
+ onSuccess: () => {
437
+ queryClient.invalidateQueries({
438
+ queryKey: ["fluxbase", "storage", bucket, "list"],
439
+ });
440
+ },
441
+ });
442
+ }
443
+
444
+ /**
445
+ * Hook to manage buckets
446
+ */
447
+ export function useStorageBuckets() {
448
+ const client = useFluxbaseClient();
449
+
450
+ return useQuery({
451
+ queryKey: ["fluxbase", "storage", "buckets"],
452
+ queryFn: async () => {
453
+ const { data, error } = await client.storage.listBuckets();
454
+
455
+ if (error) {
456
+ throw error;
457
+ }
458
+
459
+ return data || [];
460
+ },
461
+ });
462
+ }
463
+
464
+ /**
465
+ * Hook to create a bucket
466
+ */
467
+ export function useCreateBucket() {
468
+ const client = useFluxbaseClient();
469
+ const queryClient = useQueryClient();
470
+
471
+ return useMutation({
472
+ mutationFn: async (bucketName: string) => {
473
+ const { error } = await client.storage.createBucket(bucketName);
474
+
475
+ if (error) {
476
+ throw error;
477
+ }
478
+ },
479
+ onSuccess: () => {
480
+ queryClient.invalidateQueries({
481
+ queryKey: ["fluxbase", "storage", "buckets"],
482
+ });
483
+ },
484
+ });
485
+ }
486
+
487
+ /**
488
+ * Hook to delete a bucket
489
+ */
490
+ export function useDeleteBucket() {
491
+ const client = useFluxbaseClient();
492
+ const queryClient = useQueryClient();
493
+
494
+ return useMutation({
495
+ mutationFn: async (bucketName: string) => {
496
+ const { error } = await client.storage.deleteBucket(bucketName);
497
+
498
+ if (error) {
499
+ throw error;
500
+ }
501
+ },
502
+ onSuccess: () => {
503
+ queryClient.invalidateQueries({
504
+ queryKey: ["fluxbase", "storage", "buckets"],
505
+ });
506
+ },
507
+ });
508
+ }