@fluxbase/sdk-react 0.1.0-rc.1 → 2026.1.1-rc.10

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,392 @@
1
+ /**
2
+ * GraphQL hooks for Fluxbase React SDK
3
+ *
4
+ * Provides React Query integration for GraphQL queries and mutations.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { useGraphQLQuery, useGraphQLMutation } from '@fluxbase/sdk-react'
9
+ *
10
+ * function UsersList() {
11
+ * const { data, isLoading, error } = useGraphQLQuery<UsersQuery>(
12
+ * 'users-list',
13
+ * `query { users { id email } }`
14
+ * )
15
+ *
16
+ * if (isLoading) return <div>Loading...</div>
17
+ * if (error) return <div>Error: {error.message}</div>
18
+ *
19
+ * return (
20
+ * <ul>
21
+ * {data?.users.map(user => (
22
+ * <li key={user.id}>{user.email}</li>
23
+ * ))}
24
+ * </ul>
25
+ * )
26
+ * }
27
+ * ```
28
+ */
29
+
30
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
31
+ import { useFluxbaseClient } from "./context";
32
+ import type {
33
+ GraphQLResponse,
34
+ GraphQLError,
35
+ GraphQLRequestOptions,
36
+ } from "@fluxbase/sdk";
37
+
38
+ /**
39
+ * Options for useGraphQLQuery hook
40
+ */
41
+ export interface UseGraphQLQueryOptions<T> {
42
+ /**
43
+ * Variables to pass to the GraphQL query
44
+ */
45
+ variables?: Record<string, unknown>;
46
+
47
+ /**
48
+ * Operation name when the document contains multiple operations
49
+ */
50
+ operationName?: string;
51
+
52
+ /**
53
+ * Additional request options
54
+ */
55
+ requestOptions?: GraphQLRequestOptions;
56
+
57
+ /**
58
+ * Whether the query is enabled
59
+ * @default true
60
+ */
61
+ enabled?: boolean;
62
+
63
+ /**
64
+ * Time in milliseconds after which the query is considered stale
65
+ * @default 0 (considered stale immediately)
66
+ */
67
+ staleTime?: number;
68
+
69
+ /**
70
+ * Time in milliseconds after which inactive query data is garbage collected
71
+ * @default 5 minutes
72
+ */
73
+ gcTime?: number;
74
+
75
+ /**
76
+ * Whether to refetch on window focus
77
+ * @default true
78
+ */
79
+ refetchOnWindowFocus?: boolean;
80
+
81
+ /**
82
+ * Transform function to process the response data
83
+ */
84
+ select?: (data: T | undefined) => T | undefined;
85
+ }
86
+
87
+ /**
88
+ * Options for useGraphQLMutation hook
89
+ */
90
+ export interface UseGraphQLMutationOptions<T, V> {
91
+ /**
92
+ * Operation name when the document contains multiple operations
93
+ */
94
+ operationName?: string;
95
+
96
+ /**
97
+ * Additional request options
98
+ */
99
+ requestOptions?: GraphQLRequestOptions;
100
+
101
+ /**
102
+ * Callback when mutation succeeds
103
+ */
104
+ onSuccess?: (data: T, variables: V) => void;
105
+
106
+ /**
107
+ * Callback when mutation fails
108
+ */
109
+ onError?: (error: GraphQLError, variables: V) => void;
110
+
111
+ /**
112
+ * Query keys to invalidate on success
113
+ */
114
+ invalidateQueries?: string[];
115
+ }
116
+
117
+ /**
118
+ * Hook to execute GraphQL queries with React Query caching
119
+ *
120
+ * @typeParam T - The expected response data type
121
+ * @param queryKey - Unique key for caching (string or array)
122
+ * @param query - The GraphQL query string
123
+ * @param options - Query options including variables
124
+ * @returns React Query result object
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * interface UsersQuery {
129
+ * users: Array<{ id: string; email: string }>
130
+ * }
131
+ *
132
+ * function UsersList() {
133
+ * const { data, isLoading } = useGraphQLQuery<UsersQuery>(
134
+ * 'users',
135
+ * `query { users { id email } }`
136
+ * )
137
+ *
138
+ * return <div>{data?.users.length} users</div>
139
+ * }
140
+ * ```
141
+ *
142
+ * @example
143
+ * ```tsx
144
+ * // With variables
145
+ * const { data } = useGraphQLQuery<UserQuery>(
146
+ * ['user', userId],
147
+ * `query GetUser($id: ID!) { user(id: $id) { id email } }`,
148
+ * { variables: { id: userId } }
149
+ * )
150
+ * ```
151
+ */
152
+ export function useGraphQLQuery<T = unknown>(
153
+ queryKey: string | readonly unknown[],
154
+ query: string,
155
+ options?: UseGraphQLQueryOptions<T>
156
+ ) {
157
+ const client = useFluxbaseClient();
158
+ const normalizedKey = Array.isArray(queryKey)
159
+ ? ["fluxbase", "graphql", ...queryKey]
160
+ : ["fluxbase", "graphql", queryKey];
161
+
162
+ return useQuery<T | undefined, GraphQLError>({
163
+ queryKey: normalizedKey,
164
+ queryFn: async () => {
165
+ const response: GraphQLResponse<T> = await client.graphql.execute(
166
+ query,
167
+ options?.variables,
168
+ options?.operationName,
169
+ options?.requestOptions
170
+ );
171
+
172
+ if (response.errors && response.errors.length > 0) {
173
+ throw response.errors[0];
174
+ }
175
+
176
+ return response.data;
177
+ },
178
+ enabled: options?.enabled ?? true,
179
+ staleTime: options?.staleTime ?? 0,
180
+ gcTime: options?.gcTime,
181
+ refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true,
182
+ select: options?.select,
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Hook to execute GraphQL mutations
188
+ *
189
+ * @typeParam T - The expected response data type
190
+ * @typeParam V - The variables type
191
+ * @param mutation - The GraphQL mutation string
192
+ * @param options - Mutation options
193
+ * @returns React Query mutation result object
194
+ *
195
+ * @example
196
+ * ```tsx
197
+ * interface CreateUserMutation {
198
+ * insertUser: { id: string; email: string }
199
+ * }
200
+ *
201
+ * interface CreateUserVariables {
202
+ * data: { email: string }
203
+ * }
204
+ *
205
+ * function CreateUserForm() {
206
+ * const mutation = useGraphQLMutation<CreateUserMutation, CreateUserVariables>(
207
+ * `mutation CreateUser($data: UserInput!) {
208
+ * insertUser(data: $data) { id email }
209
+ * }`,
210
+ * {
211
+ * onSuccess: (data) => console.log('Created:', data.insertUser),
212
+ * invalidateQueries: ['users']
213
+ * }
214
+ * )
215
+ *
216
+ * const handleSubmit = (email: string) => {
217
+ * mutation.mutate({ data: { email } })
218
+ * }
219
+ *
220
+ * return (
221
+ * <button
222
+ * onClick={() => handleSubmit('new@example.com')}
223
+ * disabled={mutation.isPending}
224
+ * >
225
+ * Create User
226
+ * </button>
227
+ * )
228
+ * }
229
+ * ```
230
+ */
231
+ export function useGraphQLMutation<
232
+ T = unknown,
233
+ V extends Record<string, unknown> = Record<string, unknown>,
234
+ >(mutation: string, options?: UseGraphQLMutationOptions<T, V>) {
235
+ const client = useFluxbaseClient();
236
+ const queryClient = useQueryClient();
237
+
238
+ return useMutation<T | undefined, GraphQLError, V>({
239
+ mutationFn: async (variables: V) => {
240
+ const response: GraphQLResponse<T> = await client.graphql.execute(
241
+ mutation,
242
+ variables,
243
+ options?.operationName,
244
+ options?.requestOptions
245
+ );
246
+
247
+ if (response.errors && response.errors.length > 0) {
248
+ throw response.errors[0];
249
+ }
250
+
251
+ return response.data;
252
+ },
253
+ onSuccess: (data, variables) => {
254
+ // Invalidate specified queries
255
+ if (options?.invalidateQueries) {
256
+ for (const key of options.invalidateQueries) {
257
+ queryClient.invalidateQueries({
258
+ queryKey: ["fluxbase", "graphql", key],
259
+ });
260
+ }
261
+ }
262
+
263
+ // Call user's onSuccess callback
264
+ if (options?.onSuccess && data !== undefined) {
265
+ options.onSuccess(data, variables);
266
+ }
267
+ },
268
+ onError: (error, variables) => {
269
+ if (options?.onError) {
270
+ options.onError(error, variables);
271
+ }
272
+ },
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Hook to fetch the GraphQL schema via introspection
278
+ *
279
+ * @param options - Query options
280
+ * @returns React Query result with schema introspection data
281
+ *
282
+ * @example
283
+ * ```tsx
284
+ * function SchemaExplorer() {
285
+ * const { data, isLoading } = useGraphQLIntrospection()
286
+ *
287
+ * if (isLoading) return <div>Loading schema...</div>
288
+ *
289
+ * return (
290
+ * <div>
291
+ * <p>Query type: {data?.__schema.queryType.name}</p>
292
+ * <p>Types: {data?.__schema.types.length}</p>
293
+ * </div>
294
+ * )
295
+ * }
296
+ * ```
297
+ */
298
+ export function useGraphQLIntrospection(options?: {
299
+ enabled?: boolean;
300
+ staleTime?: number;
301
+ requestOptions?: GraphQLRequestOptions;
302
+ }) {
303
+ const client = useFluxbaseClient();
304
+
305
+ return useQuery({
306
+ queryKey: ["fluxbase", "graphql", "__introspection"],
307
+ queryFn: async () => {
308
+ const response = await client.graphql.introspect(options?.requestOptions);
309
+
310
+ if (response.errors && response.errors.length > 0) {
311
+ throw response.errors[0];
312
+ }
313
+
314
+ return response.data;
315
+ },
316
+ enabled: options?.enabled ?? true,
317
+ staleTime: options?.staleTime ?? 1000 * 60 * 5, // 5 minutes - schema doesn't change often
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Hook to execute raw GraphQL operations (query or mutation)
323
+ *
324
+ * This is a lower-level hook that doesn't use React Query caching.
325
+ * Useful for one-off operations or when you need full control.
326
+ *
327
+ * @returns Functions to execute queries and mutations
328
+ *
329
+ * @example
330
+ * ```tsx
331
+ * function AdminPanel() {
332
+ * const { executeQuery, executeMutation } = useGraphQL()
333
+ *
334
+ * const handleExport = async () => {
335
+ * const result = await executeQuery<ExportData>(
336
+ * `query { exportAllData { url } }`
337
+ * )
338
+ * if (result.data) {
339
+ * window.open(result.data.exportAllData.url)
340
+ * }
341
+ * }
342
+ *
343
+ * return <button onClick={handleExport}>Export Data</button>
344
+ * }
345
+ * ```
346
+ */
347
+ export function useGraphQL() {
348
+ const client = useFluxbaseClient();
349
+
350
+ return {
351
+ /**
352
+ * Execute a GraphQL query
353
+ */
354
+ executeQuery: <T = unknown>(
355
+ query: string,
356
+ variables?: Record<string, unknown>,
357
+ options?: GraphQLRequestOptions
358
+ ): Promise<GraphQLResponse<T>> => {
359
+ return client.graphql.query<T>(query, variables, options);
360
+ },
361
+
362
+ /**
363
+ * Execute a GraphQL mutation
364
+ */
365
+ executeMutation: <T = unknown>(
366
+ mutation: string,
367
+ variables?: Record<string, unknown>,
368
+ options?: GraphQLRequestOptions
369
+ ): Promise<GraphQLResponse<T>> => {
370
+ return client.graphql.mutation<T>(mutation, variables, options);
371
+ },
372
+
373
+ /**
374
+ * Execute a GraphQL operation with an explicit operation name
375
+ */
376
+ execute: <T = unknown>(
377
+ document: string,
378
+ variables?: Record<string, unknown>,
379
+ operationName?: string,
380
+ options?: GraphQLRequestOptions
381
+ ): Promise<GraphQLResponse<T>> => {
382
+ return client.graphql.execute<T>(document, variables, operationName, options);
383
+ },
384
+
385
+ /**
386
+ * Fetch the GraphQL schema via introspection
387
+ */
388
+ introspect: (options?: GraphQLRequestOptions) => {
389
+ return client.graphql.introspect(options);
390
+ },
391
+ };
392
+ }
@@ -2,99 +2,113 @@
2
2
  * Realtime subscription hooks for Fluxbase SDK
3
3
  */
4
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'
5
+ import { useEffect, useRef } from "react";
6
+ import { useQueryClient } from "@tanstack/react-query";
7
+ import { useFluxbaseClient } from "./context";
8
+ import type {
9
+ RealtimeCallback,
10
+ RealtimePostgresChangesPayload,
11
+ } from "@fluxbase/sdk";
9
12
 
10
13
  export interface UseRealtimeOptions {
11
14
  /**
12
15
  * The channel name (e.g., 'table:public.products')
13
16
  */
14
- channel: string
17
+ channel: string;
15
18
 
16
19
  /**
17
20
  * Event type to listen for ('INSERT', 'UPDATE', 'DELETE', or '*' for all)
18
21
  */
19
- event?: 'INSERT' | 'UPDATE' | 'DELETE' | '*'
22
+ event?: "INSERT" | "UPDATE" | "DELETE" | "*";
20
23
 
21
24
  /**
22
25
  * Callback function when an event is received
23
26
  */
24
- callback?: RealtimeCallback
27
+ callback?: RealtimeCallback;
25
28
 
26
29
  /**
27
30
  * Whether to automatically invalidate queries for the table
28
31
  * Default: true
29
32
  */
30
- autoInvalidate?: boolean
33
+ autoInvalidate?: boolean;
31
34
 
32
35
  /**
33
36
  * Custom query key to invalidate (if autoInvalidate is true)
34
37
  * Default: ['fluxbase', 'table', tableName]
35
38
  */
36
- invalidateKey?: unknown[]
39
+ invalidateKey?: unknown[];
37
40
 
38
41
  /**
39
42
  * Whether the subscription is enabled
40
43
  * Default: true
41
44
  */
42
- enabled?: boolean
45
+ enabled?: boolean;
43
46
  }
44
47
 
45
48
  /**
46
49
  * Hook to subscribe to realtime changes for a channel
47
50
  */
48
51
  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
+ const client = useFluxbaseClient();
53
+ const queryClient = useQueryClient();
54
+ const channelRef = useRef<ReturnType<typeof client.realtime.channel> | null>(
55
+ null,
56
+ );
52
57
 
53
58
  const {
54
59
  channel: channelName,
55
- event = '*',
60
+ event = "*",
56
61
  callback,
57
62
  autoInvalidate = true,
58
63
  invalidateKey,
59
64
  enabled = true,
60
- } = options
65
+ } = options;
61
66
 
62
67
  useEffect(() => {
63
68
  if (!enabled) {
64
- return
69
+ return;
65
70
  }
66
71
 
67
72
  // Create channel and subscribe
68
- const channel = client.realtime.channel(channelName)
69
- channelRef.current = channel
73
+ const channel = client.realtime.channel(channelName);
74
+ channelRef.current = channel;
70
75
 
71
- const handleChange = (payload: RealtimeChangePayload) => {
76
+ const handleChange = (payload: RealtimePostgresChangesPayload) => {
72
77
  // Call user callback
73
78
  if (callback) {
74
- callback(payload)
79
+ callback(payload);
75
80
  }
76
81
 
77
82
  // Auto-invalidate queries if enabled
78
83
  if (autoInvalidate) {
79
84
  // Extract table name from channel (e.g., 'table:public.products' -> 'public.products')
80
- const tableName = channelName.replace(/^table:/, '')
85
+ const tableName = channelName.replace(/^table:/, "");
81
86
 
82
- const key = invalidateKey || ['fluxbase', 'table', tableName]
83
- queryClient.invalidateQueries({ queryKey: key })
87
+ const key = invalidateKey || ["fluxbase", "table", tableName];
88
+ queryClient.invalidateQueries({ queryKey: key });
84
89
  }
85
- }
90
+ };
86
91
 
87
- channel.on(event, handleChange).subscribe()
92
+ channel.on(event, handleChange).subscribe();
88
93
 
89
94
  return () => {
90
- channel.unsubscribe()
91
- channelRef.current = null
92
- }
93
- }, [client, channelName, event, callback, autoInvalidate, invalidateKey, queryClient, enabled])
95
+ channel.unsubscribe();
96
+ channelRef.current = null;
97
+ };
98
+ }, [
99
+ client,
100
+ channelName,
101
+ event,
102
+ callback,
103
+ autoInvalidate,
104
+ invalidateKey,
105
+ queryClient,
106
+ enabled,
107
+ ]);
94
108
 
95
109
  return {
96
110
  channel: channelRef.current,
97
- }
111
+ };
98
112
  }
99
113
 
100
114
  /**
@@ -104,12 +118,12 @@ export function useRealtime(options: UseRealtimeOptions) {
104
118
  */
105
119
  export function useTableSubscription(
106
120
  table: string,
107
- options?: Omit<UseRealtimeOptions, 'channel'>
121
+ options?: Omit<UseRealtimeOptions, "channel">,
108
122
  ) {
109
123
  return useRealtime({
110
124
  ...options,
111
125
  channel: `table:${table}`,
112
- })
126
+ });
113
127
  }
114
128
 
115
129
  /**
@@ -117,15 +131,15 @@ export function useTableSubscription(
117
131
  */
118
132
  export function useTableInserts(
119
133
  table: string,
120
- callback: (payload: RealtimeChangePayload) => void,
121
- options?: Omit<UseRealtimeOptions, 'channel' | 'event' | 'callback'>
134
+ callback: (payload: RealtimePostgresChangesPayload) => void,
135
+ options?: Omit<UseRealtimeOptions, "channel" | "event" | "callback">,
122
136
  ) {
123
137
  return useRealtime({
124
138
  ...options,
125
139
  channel: `table:${table}`,
126
- event: 'INSERT',
140
+ event: "INSERT",
127
141
  callback,
128
- })
142
+ });
129
143
  }
130
144
 
131
145
  /**
@@ -133,15 +147,15 @@ export function useTableInserts(
133
147
  */
134
148
  export function useTableUpdates(
135
149
  table: string,
136
- callback: (payload: RealtimeChangePayload) => void,
137
- options?: Omit<UseRealtimeOptions, 'channel' | 'event' | 'callback'>
150
+ callback: (payload: RealtimePostgresChangesPayload) => void,
151
+ options?: Omit<UseRealtimeOptions, "channel" | "event" | "callback">,
138
152
  ) {
139
153
  return useRealtime({
140
154
  ...options,
141
155
  channel: `table:${table}`,
142
- event: 'UPDATE',
156
+ event: "UPDATE",
143
157
  callback,
144
- })
158
+ });
145
159
  }
146
160
 
147
161
  /**
@@ -149,13 +163,13 @@ export function useTableUpdates(
149
163
  */
150
164
  export function useTableDeletes(
151
165
  table: string,
152
- callback: (payload: RealtimeChangePayload) => void,
153
- options?: Omit<UseRealtimeOptions, 'channel' | 'event' | 'callback'>
166
+ callback: (payload: RealtimePostgresChangesPayload) => void,
167
+ options?: Omit<UseRealtimeOptions, "channel" | "event" | "callback">,
154
168
  ) {
155
169
  return useRealtime({
156
170
  ...options,
157
171
  channel: `table:${table}`,
158
- event: 'DELETE',
172
+ event: "DELETE",
159
173
  callback,
160
- })
174
+ });
161
175
  }