@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.
- package/.nvmrc +1 -0
- package/README-ADMIN.md +1076 -0
- package/README.md +195 -0
- package/examples/AdminDashboard.tsx +513 -0
- package/examples/README.md +163 -0
- package/package.json +66 -0
- package/src/context.test.tsx +147 -0
- package/src/context.tsx +33 -0
- package/src/index.test.ts +255 -0
- package/src/index.ts +175 -0
- package/src/test-setup.ts +22 -0
- package/src/test-utils.tsx +215 -0
- package/src/use-admin-auth.test.ts +175 -0
- package/src/use-admin-auth.ts +187 -0
- package/src/use-admin-hooks.test.ts +457 -0
- package/src/use-admin-hooks.ts +309 -0
- package/src/use-auth-config.test.ts +145 -0
- package/src/use-auth-config.ts +101 -0
- package/src/use-auth.test.ts +313 -0
- package/src/use-auth.ts +164 -0
- package/src/use-captcha.test.ts +273 -0
- package/src/use-captcha.ts +250 -0
- package/src/use-client-keys.test.ts +286 -0
- package/src/use-client-keys.ts +185 -0
- package/src/use-graphql.test.ts +424 -0
- package/src/use-graphql.ts +392 -0
- package/src/use-query.test.ts +348 -0
- package/src/use-query.ts +211 -0
- package/src/use-realtime.test.ts +359 -0
- package/src/use-realtime.ts +180 -0
- package/src/use-saml.test.ts +269 -0
- package/src/use-saml.ts +221 -0
- package/src/use-storage.test.ts +549 -0
- package/src/use-storage.ts +508 -0
- package/src/use-table-export.ts +481 -0
- package/src/use-users.test.ts +264 -0
- package/src/use-users.ts +198 -0
- package/tsconfig.json +28 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +11 -0
- package/typedoc.json +33 -0
- 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
|
+
}
|