@fluxbase/sdk-react 0.0.1-rc.2

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.
@@ -0,0 +1,165 @@
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
+ export function useFluxbaseQuery<T = any>(
22
+ buildQuery: (client: ReturnType<typeof useFluxbaseClient>) => QueryBuilder<T>,
23
+ options?: UseFluxbaseQueryOptions<T>
24
+ ) {
25
+ const client = useFluxbaseClient()
26
+
27
+ // Build a stable query key
28
+ const queryKey = options?.queryKey || ['fluxbase', 'query', buildQuery.toString()]
29
+
30
+ return useQuery({
31
+ queryKey,
32
+ queryFn: async () => {
33
+ const query = buildQuery(client)
34
+ const { data, error } = await query.execute()
35
+
36
+ if (error) {
37
+ throw error
38
+ }
39
+
40
+ return (Array.isArray(data) ? data : data ? [data] : []) as T[]
41
+ },
42
+ ...options,
43
+ })
44
+ }
45
+
46
+ /**
47
+ * Hook for table queries with a simpler API
48
+ * @param table - Table name
49
+ * @param buildQuery - Function to build the query
50
+ */
51
+ export function useTable<T = any>(
52
+ table: string,
53
+ buildQuery?: (query: QueryBuilder<T>) => QueryBuilder<T>,
54
+ options?: UseFluxbaseQueryOptions<T>
55
+ ) {
56
+ const client = useFluxbaseClient()
57
+
58
+ return useFluxbaseQuery(
59
+ (client) => {
60
+ const query = client.from<T>(table)
61
+ return buildQuery ? buildQuery(query) : query
62
+ },
63
+ {
64
+ ...options,
65
+ queryKey: options?.queryKey || ['fluxbase', 'table', table, buildQuery?.toString()],
66
+ }
67
+ )
68
+ }
69
+
70
+ /**
71
+ * Hook to insert data into a table
72
+ */
73
+ export function useInsert<T = any>(table: string) {
74
+ const client = useFluxbaseClient()
75
+ const queryClient = useQueryClient()
76
+
77
+ return useMutation({
78
+ mutationFn: async (data: Partial<T> | Partial<T>[]) => {
79
+ const query = client.from<T>(table)
80
+ const { data: result, error } = await query.insert(data as Partial<T>)
81
+
82
+ if (error) {
83
+ throw error
84
+ }
85
+
86
+ return result
87
+ },
88
+ onSuccess: () => {
89
+ // Invalidate all queries for this table
90
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
91
+ },
92
+ })
93
+ }
94
+
95
+ /**
96
+ * Hook to update data in a table
97
+ */
98
+ export function useUpdate<T = any>(table: string) {
99
+ const client = useFluxbaseClient()
100
+ const queryClient = useQueryClient()
101
+
102
+ return useMutation({
103
+ mutationFn: async (params: { data: Partial<T>; buildQuery: (query: QueryBuilder<T>) => QueryBuilder<T> }) => {
104
+ const query = client.from<T>(table)
105
+ const builtQuery = params.buildQuery(query)
106
+ const { data: result, error } = await builtQuery.update(params.data)
107
+
108
+ if (error) {
109
+ throw error
110
+ }
111
+
112
+ return result
113
+ },
114
+ onSuccess: () => {
115
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
116
+ },
117
+ })
118
+ }
119
+
120
+ /**
121
+ * Hook to upsert data into a table
122
+ */
123
+ export function useUpsert<T = any>(table: string) {
124
+ const client = useFluxbaseClient()
125
+ const queryClient = useQueryClient()
126
+
127
+ return useMutation({
128
+ mutationFn: async (data: Partial<T> | Partial<T>[]) => {
129
+ const query = client.from<T>(table)
130
+ const { data: result, error } = await query.upsert(data as Partial<T>)
131
+
132
+ if (error) {
133
+ throw error
134
+ }
135
+
136
+ return result
137
+ },
138
+ onSuccess: () => {
139
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
140
+ },
141
+ })
142
+ }
143
+
144
+ /**
145
+ * Hook to delete data from a table
146
+ */
147
+ export function useDelete<T = any>(table: string) {
148
+ const client = useFluxbaseClient()
149
+ const queryClient = useQueryClient()
150
+
151
+ return useMutation({
152
+ mutationFn: async (buildQuery: (query: QueryBuilder<T>) => QueryBuilder<T>) => {
153
+ const query = client.from<T>(table)
154
+ const builtQuery = buildQuery(query)
155
+ const { error } = await builtQuery.delete()
156
+
157
+ if (error) {
158
+ throw error
159
+ }
160
+ },
161
+ onSuccess: () => {
162
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'table', table] })
163
+ },
164
+ })
165
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Realtime subscription hooks for Fluxbase SDK
3
+ */
4
+
5
+ import { useEffect, useRef } from 'react'
6
+ import { useQueryClient } from '@tanstack/react-query'
7
+ import { useFluxbaseClient } from './context'
8
+ import type { RealtimeCallback, RealtimeChangePayload } from '@fluxbase/sdk'
9
+
10
+ export interface UseRealtimeOptions {
11
+ /**
12
+ * The channel name (e.g., 'table:public.products')
13
+ */
14
+ channel: string
15
+
16
+ /**
17
+ * Event type to listen for ('INSERT', 'UPDATE', 'DELETE', or '*' for all)
18
+ */
19
+ event?: 'INSERT' | 'UPDATE' | 'DELETE' | '*'
20
+
21
+ /**
22
+ * Callback function when an event is received
23
+ */
24
+ callback?: RealtimeCallback
25
+
26
+ /**
27
+ * Whether to automatically invalidate queries for the table
28
+ * Default: true
29
+ */
30
+ autoInvalidate?: boolean
31
+
32
+ /**
33
+ * Custom query key to invalidate (if autoInvalidate is true)
34
+ * Default: ['fluxbase', 'table', tableName]
35
+ */
36
+ invalidateKey?: unknown[]
37
+
38
+ /**
39
+ * Whether the subscription is enabled
40
+ * Default: true
41
+ */
42
+ enabled?: boolean
43
+ }
44
+
45
+ /**
46
+ * Hook to subscribe to realtime changes for a channel
47
+ */
48
+ export function useRealtime(options: UseRealtimeOptions) {
49
+ const client = useFluxbaseClient()
50
+ const queryClient = useQueryClient()
51
+ const channelRef = useRef<ReturnType<typeof client.realtime.channel> | null>(null)
52
+
53
+ const {
54
+ channel: channelName,
55
+ event = '*',
56
+ callback,
57
+ autoInvalidate = true,
58
+ invalidateKey,
59
+ enabled = true,
60
+ } = options
61
+
62
+ useEffect(() => {
63
+ if (!enabled) {
64
+ return
65
+ }
66
+
67
+ // Create channel and subscribe
68
+ const channel = client.realtime.channel(channelName)
69
+ channelRef.current = channel
70
+
71
+ const handleChange = (payload: RealtimeChangePayload) => {
72
+ // Call user callback
73
+ if (callback) {
74
+ callback(payload)
75
+ }
76
+
77
+ // Auto-invalidate queries if enabled
78
+ if (autoInvalidate) {
79
+ // Extract table name from channel (e.g., 'table:public.products' -> 'public.products')
80
+ const tableName = channelName.replace(/^table:/, '')
81
+
82
+ const key = invalidateKey || ['fluxbase', 'table', tableName]
83
+ queryClient.invalidateQueries({ queryKey: key })
84
+ }
85
+ }
86
+
87
+ channel.on(event, handleChange).subscribe()
88
+
89
+ return () => {
90
+ channel.unsubscribe()
91
+ channelRef.current = null
92
+ }
93
+ }, [client, channelName, event, callback, autoInvalidate, invalidateKey, queryClient, enabled])
94
+
95
+ return {
96
+ channel: channelRef.current,
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Hook to subscribe to a table's changes
102
+ * @param table - Table name (with optional schema, e.g., 'public.products')
103
+ * @param options - Subscription options
104
+ */
105
+ export function useTableSubscription(
106
+ table: string,
107
+ options?: Omit<UseRealtimeOptions, 'channel'>
108
+ ) {
109
+ return useRealtime({
110
+ ...options,
111
+ channel: `table:${table}`,
112
+ })
113
+ }
114
+
115
+ /**
116
+ * Hook to subscribe to INSERT events on a table
117
+ */
118
+ export function useTableInserts(
119
+ table: string,
120
+ callback: (payload: RealtimeChangePayload) => void,
121
+ options?: Omit<UseRealtimeOptions, 'channel' | 'event' | 'callback'>
122
+ ) {
123
+ return useRealtime({
124
+ ...options,
125
+ channel: `table:${table}`,
126
+ event: 'INSERT',
127
+ callback,
128
+ })
129
+ }
130
+
131
+ /**
132
+ * Hook to subscribe to UPDATE events on a table
133
+ */
134
+ export function useTableUpdates(
135
+ table: string,
136
+ callback: (payload: RealtimeChangePayload) => void,
137
+ options?: Omit<UseRealtimeOptions, 'channel' | 'event' | 'callback'>
138
+ ) {
139
+ return useRealtime({
140
+ ...options,
141
+ channel: `table:${table}`,
142
+ event: 'UPDATE',
143
+ callback,
144
+ })
145
+ }
146
+
147
+ /**
148
+ * Hook to subscribe to DELETE events on a table
149
+ */
150
+ export function useTableDeletes(
151
+ table: string,
152
+ callback: (payload: RealtimeChangePayload) => void,
153
+ options?: Omit<UseRealtimeOptions, 'channel' | 'event' | 'callback'>
154
+ ) {
155
+ return useRealtime({
156
+ ...options,
157
+ channel: `table:${table}`,
158
+ event: 'DELETE',
159
+ callback,
160
+ })
161
+ }
package/src/use-rpc.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * React hooks for RPC (Remote Procedure Calls)
3
+ * Call PostgreSQL functions with React Query integration
4
+ */
5
+
6
+ import { useQuery, useMutation, useQueryClient, type UseQueryOptions, type UseMutationOptions } from '@tanstack/react-query'
7
+ import { useFluxbaseClient } from './context'
8
+ import type { PostgrestResponse } from '@fluxbase/sdk'
9
+
10
+ /**
11
+ * Hook to call a PostgreSQL function and cache the result
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * const { data, isLoading, error } = useRPC(
16
+ * 'calculate_total',
17
+ * { order_id: 123 },
18
+ * { enabled: !!orderId }
19
+ * )
20
+ * ```
21
+ */
22
+ export function useRPC<TData = unknown, TParams extends Record<string, unknown> = Record<string, unknown>>(
23
+ functionName: string,
24
+ params?: TParams,
25
+ options?: Omit<UseQueryOptions<TData, Error>, 'queryKey' | 'queryFn'>
26
+ ) {
27
+ const client = useFluxbaseClient()
28
+
29
+ return useQuery<TData, Error>({
30
+ queryKey: ['rpc', functionName, params],
31
+ queryFn: async () => {
32
+ const { data, error } = await client.rpc<TData>(functionName, params)
33
+ if (error) {
34
+ throw new Error(error.message)
35
+ }
36
+ return data as TData
37
+ },
38
+ ...options,
39
+ })
40
+ }
41
+
42
+ /**
43
+ * Hook to create a mutation for calling PostgreSQL functions
44
+ * Useful for functions that modify data
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * const createOrder = useRPCMutation('create_order')
49
+ *
50
+ * const handleSubmit = async () => {
51
+ * await createOrder.mutateAsync({
52
+ * user_id: 123,
53
+ * items: [{ product_id: 1, quantity: 2 }]
54
+ * })
55
+ * }
56
+ * ```
57
+ */
58
+ export function useRPCMutation<TData = unknown, TParams extends Record<string, unknown> = Record<string, unknown>>(
59
+ functionName: string,
60
+ options?: Omit<UseMutationOptions<TData, Error, TParams>, 'mutationFn'>
61
+ ) {
62
+ const client = useFluxbaseClient()
63
+
64
+ return useMutation<TData, Error, TParams>({
65
+ mutationFn: async (params: TParams) => {
66
+ const { data, error } = await client.rpc<TData>(functionName, params)
67
+ if (error) {
68
+ throw new Error(error.message)
69
+ }
70
+ return data as TData
71
+ },
72
+ ...options,
73
+ })
74
+ }
75
+
76
+ /**
77
+ * Hook to call multiple RPC functions in parallel
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * const { data, isLoading } = useRPCBatch([
82
+ * { name: 'get_user_stats', params: { user_id: 123 } },
83
+ * { name: 'get_recent_orders', params: { limit: 10 } },
84
+ * ])
85
+ * ```
86
+ */
87
+ export function useRPCBatch<TData = unknown>(
88
+ calls: Array<{ name: string; params?: Record<string, unknown> }>,
89
+ options?: Omit<UseQueryOptions<TData[], Error, TData[], readonly unknown[]>, 'queryKey' | 'queryFn'>
90
+ ) {
91
+ const client = useFluxbaseClient()
92
+
93
+ return useQuery({
94
+ queryKey: ['rpc-batch', calls] as const,
95
+ queryFn: async () => {
96
+ const results = await Promise.all(
97
+ calls.map(async ({ name, params }) => {
98
+ const { data, error } = await client.rpc<TData>(name, params)
99
+ if (error) {
100
+ throw new Error(`${name}: ${error.message}`)
101
+ }
102
+ return data
103
+ })
104
+ )
105
+ return results as TData[]
106
+ },
107
+ ...options,
108
+ })
109
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Storage hooks for Fluxbase SDK
3
+ */
4
+
5
+ import { useMutation, useQuery, useQueryClient, type UseQueryOptions } from '@tanstack/react-query'
6
+ import { useFluxbaseClient } from './context'
7
+ import type { ListOptions, UploadOptions } from '@fluxbase/sdk'
8
+
9
+ /**
10
+ * Hook to list files in a bucket
11
+ */
12
+ export function useStorageList(
13
+ bucket: string,
14
+ options?: ListOptions & Omit<UseQueryOptions<any[], Error>, 'queryKey' | 'queryFn'>
15
+ ) {
16
+ const client = useFluxbaseClient()
17
+ const { prefix, limit, offset, ...queryOptions } = options || {}
18
+
19
+ return useQuery({
20
+ queryKey: ['fluxbase', 'storage', bucket, 'list', { prefix, limit, offset }],
21
+ queryFn: async () => {
22
+ const { data, error } = await client.storage.from(bucket).list({ prefix, limit, offset })
23
+
24
+ if (error) {
25
+ throw error
26
+ }
27
+
28
+ return data || []
29
+ },
30
+ ...queryOptions,
31
+ })
32
+ }
33
+
34
+ /**
35
+ * Hook to upload a file to a bucket
36
+ */
37
+ export function useStorageUpload(bucket: string) {
38
+ const client = useFluxbaseClient()
39
+ const queryClient = useQueryClient()
40
+
41
+ return useMutation({
42
+ mutationFn: async (params: {
43
+ path: string
44
+ file: File | Blob | ArrayBuffer
45
+ options?: UploadOptions
46
+ }) => {
47
+ const { path, file, options } = params
48
+ const { data, error } = await client.storage.from(bucket).upload(path, file, options)
49
+
50
+ if (error) {
51
+ throw error
52
+ }
53
+
54
+ return data
55
+ },
56
+ onSuccess: () => {
57
+ // Invalidate list queries for this bucket
58
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'storage', bucket, 'list'] })
59
+ },
60
+ })
61
+ }
62
+
63
+ /**
64
+ * Hook to download a file from a bucket
65
+ */
66
+ export function useStorageDownload(bucket: string, path: string | null, enabled = true) {
67
+ const client = useFluxbaseClient()
68
+
69
+ return useQuery({
70
+ queryKey: ['fluxbase', 'storage', bucket, 'download', path],
71
+ queryFn: async () => {
72
+ if (!path) {
73
+ return null
74
+ }
75
+
76
+ const { data, error } = await client.storage.from(bucket).download(path)
77
+
78
+ if (error) {
79
+ throw error
80
+ }
81
+
82
+ return data
83
+ },
84
+ enabled: enabled && !!path,
85
+ })
86
+ }
87
+
88
+ /**
89
+ * Hook to delete files from a bucket
90
+ */
91
+ export function useStorageDelete(bucket: string) {
92
+ const client = useFluxbaseClient()
93
+ const queryClient = useQueryClient()
94
+
95
+ return useMutation({
96
+ mutationFn: async (paths: string[]) => {
97
+ const { error } = await client.storage.from(bucket).remove(paths)
98
+
99
+ if (error) {
100
+ throw error
101
+ }
102
+ },
103
+ onSuccess: () => {
104
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'storage', bucket, 'list'] })
105
+ },
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Hook to get a public URL for a file
111
+ */
112
+ export function useStoragePublicUrl(bucket: string, path: string | null) {
113
+ const client = useFluxbaseClient()
114
+
115
+ if (!path) {
116
+ return null
117
+ }
118
+
119
+ const { data } = client.storage.from(bucket).getPublicUrl(path)
120
+ return data.publicUrl
121
+ }
122
+
123
+ /**
124
+ * Hook to create a signed URL
125
+ */
126
+ export function useStorageSignedUrl(bucket: string, path: string | null, expiresIn?: number) {
127
+ const client = useFluxbaseClient()
128
+
129
+ return useQuery({
130
+ queryKey: ['fluxbase', 'storage', bucket, 'signed-url', path, expiresIn],
131
+ queryFn: async () => {
132
+ if (!path) {
133
+ return null
134
+ }
135
+
136
+ const { data, error } = await client.storage.from(bucket).createSignedUrl(path, { expiresIn })
137
+
138
+ if (error) {
139
+ throw error
140
+ }
141
+
142
+ return data?.signedUrl || null
143
+ },
144
+ enabled: !!path,
145
+ staleTime: expiresIn ? expiresIn * 1000 - 60000 : 1000 * 60 * 50, // Refresh 1 minute before expiry
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Hook to move a file
151
+ */
152
+ export function useStorageMove(bucket: string) {
153
+ const client = useFluxbaseClient()
154
+ const queryClient = useQueryClient()
155
+
156
+ return useMutation({
157
+ mutationFn: async (params: { fromPath: string; toPath: string }) => {
158
+ const { fromPath, toPath } = params
159
+ const { data, error } = await client.storage.from(bucket).move(fromPath, toPath)
160
+
161
+ if (error) {
162
+ throw error
163
+ }
164
+
165
+ return data
166
+ },
167
+ onSuccess: () => {
168
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'storage', bucket, 'list'] })
169
+ },
170
+ })
171
+ }
172
+
173
+ /**
174
+ * Hook to copy a file
175
+ */
176
+ export function useStorageCopy(bucket: string) {
177
+ const client = useFluxbaseClient()
178
+ const queryClient = useQueryClient()
179
+
180
+ return useMutation({
181
+ mutationFn: async (params: { fromPath: string; toPath: string }) => {
182
+ const { fromPath, toPath } = params
183
+ const { data, error } = await client.storage.from(bucket).copy(fromPath, toPath)
184
+
185
+ if (error) {
186
+ throw error
187
+ }
188
+
189
+ return data
190
+ },
191
+ onSuccess: () => {
192
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'storage', bucket, 'list'] })
193
+ },
194
+ })
195
+ }
196
+
197
+ /**
198
+ * Hook to manage buckets
199
+ */
200
+ export function useStorageBuckets() {
201
+ const client = useFluxbaseClient()
202
+
203
+ return useQuery({
204
+ queryKey: ['fluxbase', 'storage', 'buckets'],
205
+ queryFn: async () => {
206
+ const { data, error } = await client.storage.listBuckets()
207
+
208
+ if (error) {
209
+ throw error
210
+ }
211
+
212
+ return data || []
213
+ },
214
+ })
215
+ }
216
+
217
+ /**
218
+ * Hook to create a bucket
219
+ */
220
+ export function useCreateBucket() {
221
+ const client = useFluxbaseClient()
222
+ const queryClient = useQueryClient()
223
+
224
+ return useMutation({
225
+ mutationFn: async (bucketName: string) => {
226
+ const { error } = await client.storage.createBucket(bucketName)
227
+
228
+ if (error) {
229
+ throw error
230
+ }
231
+ },
232
+ onSuccess: () => {
233
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'storage', 'buckets'] })
234
+ },
235
+ })
236
+ }
237
+
238
+ /**
239
+ * Hook to delete a bucket
240
+ */
241
+ export function useDeleteBucket() {
242
+ const client = useFluxbaseClient()
243
+ const queryClient = useQueryClient()
244
+
245
+ return useMutation({
246
+ mutationFn: async (bucketName: string) => {
247
+ const { error } = await client.storage.deleteBucket(bucketName)
248
+
249
+ if (error) {
250
+ throw error
251
+ }
252
+ },
253
+ onSuccess: () => {
254
+ queryClient.invalidateQueries({ queryKey: ['fluxbase', 'storage', 'buckets'] })
255
+ },
256
+ })
257
+ }