@blocksdiy/blocks-client-sdk 1.0.0

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,1238 @@
1
+ import { FatalAppError } from "@blockscom/blocks-client-api/errors";
2
+ import { useWebsockets, useWebsocketsAppSubscribe } from "@blockscom/blocks-client-api/websocketService";
3
+ import { websocketsService } from "@blockscom/blocks-client-api/websocketService";
4
+ import { useAiStreamChunks } from "@blockscom/react-common/useAiStreamChunks";
5
+ import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
6
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
7
+ import { ClientSdk } from "./ClientSdk.js";
8
+ export class ReactClientSdk extends ClientSdk {
9
+ }
10
+ const ENTITIES_QUERY_KEY = "entities";
11
+ const CURRENT_USER_QUERY_KEY = "currentUser";
12
+ const ClientContext = createContext(null);
13
+ const ThemeModeContext = createContext({
14
+ themeMode: "system",
15
+ setThemeMode: () => { },
16
+ });
17
+ const EXECUTE_ACTION_WINDOWS = [{ duration: 20000, maxCount: 5, withData: true }];
18
+ const ENTITY_OPERATION_WINDOWS = [{ duration: 5000, maxCount: 5, withData: true }];
19
+ const ENTITY_ROW_OPERATION_WINDOWS = [
20
+ { duration: 5000, maxCount: 5, withData: true },
21
+ { duration: 10000, maxCount: 15, withData: false },
22
+ ];
23
+ /* public decouple - blocks common */
24
+ const USERS_TABLE_BLOCK_ID = "68760b42d4ce152c91ce0e1c";
25
+ export var AppDataEventTypes;
26
+ (function (AppDataEventTypes) {
27
+ AppDataEventTypes["APP_DATA_INSERT"] = "APP_DATA_INSERT";
28
+ AppDataEventTypes["APP_DATA_UPDATE"] = "APP_DATA_UPDATE";
29
+ AppDataEventTypes["APP_DATA_DELETE"] = "APP_DATA_DELETE";
30
+ })(AppDataEventTypes || (AppDataEventTypes = {}));
31
+ /* ends of blocks common */
32
+ const useComponentName = (functionName) => {
33
+ const componentName = useMemo(() => {
34
+ try {
35
+ const stack = new Error().stack || "";
36
+ const lines = stack.split("\n");
37
+ // Find the line containing the function name and get the line above it
38
+ const functionNameIndex = lines.findIndex((line) => line.includes(functionName));
39
+ if (functionNameIndex > 0) {
40
+ const callerLine = lines[functionNameIndex + 1];
41
+ const fatherComponent = callerLine.split("at ")[1];
42
+ if (fatherComponent) {
43
+ return fatherComponent;
44
+ }
45
+ }
46
+ return "Unknown";
47
+ }
48
+ catch {
49
+ return "Unknown";
50
+ }
51
+ }, []);
52
+ return componentName;
53
+ };
54
+ const useActionRateLimiting = (actionConfig, functionName) => {
55
+ const componentName = useComponentName(functionName);
56
+ const queryClient = useQueryClient();
57
+ const validateExceededMaxCount = (inputs) => {
58
+ const now = Date.now();
59
+ for (let windowIndex = 0; windowIndex < EXECUTE_ACTION_WINDOWS.length; windowIndex++) {
60
+ const executeActionWindow = EXECUTE_ACTION_WINDOWS[windowIndex];
61
+ // Determine the query key based on whether we're checking with or without inputs
62
+ const queryKey = executeActionWindow.withData
63
+ ? [`action-result-${windowIndex}`, actionConfig.actionBlockId, inputs]
64
+ : [`action-result-${windowIndex}`, actionConfig.actionBlockId];
65
+ const executeActionHistory = queryClient.getQueryData(queryKey) || [];
66
+ // Filter history within the current time window
67
+ const executeActionHistoryInWindow = executeActionHistory.filter((history) => history.timestamp > now - executeActionWindow.duration);
68
+ // Check if rate limit is exceeded for this window
69
+ if (executeActionHistoryInWindow.length >= executeActionWindow.maxCount) {
70
+ const errorMessage = executeActionWindow.withData
71
+ ? `Executed action with the same parameters (${JSON.stringify(inputs)}) too many times (${executeActionWindow.maxCount} times in ${executeActionWindow.duration / 1000}s) - actionBlockId: "${actionConfig.actionBlockId}", from component: ${componentName}`
72
+ : `Executed action too many times (${executeActionWindow.maxCount} times in ${executeActionWindow.duration / 1000}s) - actionBlockId: "${actionConfig.actionBlockId}", from component: ${componentName}`;
73
+ throw new FatalAppError(errorMessage);
74
+ }
75
+ // Update the history for this window
76
+ queryClient.setQueryData(queryKey, [...executeActionHistoryInWindow, { timestamp: now }]);
77
+ }
78
+ };
79
+ return { validateExceededMaxCount };
80
+ };
81
+ const useEntityMutationRateLimiting = (entityConfig, functionName) => {
82
+ const componentName = useComponentName(functionName);
83
+ const queryClient = useQueryClient();
84
+ const validateExceededMaxCount = ({ data }) => {
85
+ const now = Date.now();
86
+ for (let windowIndex = 0; windowIndex < ENTITY_OPERATION_WINDOWS.length; windowIndex++) {
87
+ const entityOperationWindow = ENTITY_OPERATION_WINDOWS[windowIndex];
88
+ const queryKey = entityOperationWindow.withData
89
+ ? [`entity-operation-${windowIndex}`, entityConfig.tableBlockId, data]
90
+ : [`entity-operation-${windowIndex}`, entityConfig.tableBlockId];
91
+ const entityOperationHistory = queryClient.getQueryData(queryKey) || [];
92
+ // Filter history within the current time window
93
+ const entityOperationHistoryInWindow = entityOperationHistory.filter((history) => history.timestamp > now - entityOperationWindow.duration);
94
+ // Check if rate limit is exceeded for this window
95
+ if (entityOperationHistoryInWindow.length >= entityOperationWindow.maxCount) {
96
+ const errorMessage = `Too many entity operations (${entityOperationWindow.maxCount} times in ${entityOperationWindow.duration / 1000}s) - tableBlockId: "${entityConfig.tableBlockId}"${data && entityOperationWindow.withData ? `, data: ${JSON.stringify(data)}` : ""}, from component: ${componentName}`;
97
+ throw new FatalAppError(errorMessage);
98
+ }
99
+ // Update the history for this window
100
+ queryClient.setQueryData(queryKey, [...entityOperationHistoryInWindow, { timestamp: now }]);
101
+ }
102
+ };
103
+ return { validateExceededMaxCount };
104
+ };
105
+ const useEntityRowMutationRateLimiting = (entityConfig, functionName) => {
106
+ const componentName = useComponentName(functionName);
107
+ const queryClient = useQueryClient();
108
+ const validateExceededMaxCount = ({ rowId, data }) => {
109
+ const now = Date.now();
110
+ for (let windowIndex = 0; windowIndex < ENTITY_ROW_OPERATION_WINDOWS.length; windowIndex++) {
111
+ const entityOperationWindow = ENTITY_ROW_OPERATION_WINDOWS[windowIndex];
112
+ const queryKey = entityOperationWindow.withData
113
+ ? [`entity-row-operation-${windowIndex}`, entityConfig.tableBlockId, rowId, data]
114
+ : [`entity-row-operation-${windowIndex}`, entityConfig.tableBlockId, rowId];
115
+ const entityOperationHistory = queryClient.getQueryData(queryKey) || [];
116
+ // Filter history within the current time window
117
+ const entityOperationHistoryInWindow = entityOperationHistory.filter((history) => history.timestamp > now - entityOperationWindow.duration);
118
+ // Check if rate limit is exceeded for this window
119
+ if (entityOperationHistoryInWindow.length >= entityOperationWindow.maxCount) {
120
+ const errorMessage = `Too many entity row operations (${entityOperationWindow.maxCount} times in ${entityOperationWindow.duration / 1000}s) - tableBlockId: "${entityConfig.tableBlockId}", rowId: "${rowId}"${data && entityOperationWindow.withData ? `, data: ${JSON.stringify(data)}` : ""}, from component: ${componentName}`;
121
+ throw new FatalAppError(errorMessage);
122
+ }
123
+ // Update the history for this window
124
+ queryClient.setQueryData(queryKey, [...entityOperationHistoryInWindow, { timestamp: now }]);
125
+ }
126
+ };
127
+ return { validateExceededMaxCount };
128
+ };
129
+ /**
130
+ * Provides ClientSdk instance to React component tree
131
+ *
132
+ * This component sets up the necessary context providers for both the ClientSdk
133
+ * and React Query, enabling all the hooks in this module to work properly.
134
+ *
135
+ * @param {Object} props - Component props
136
+ * @param {ClientSdk} props.client - ClientSdk instance to provide
137
+ * @param {React.ReactNode} props.children - Child components
138
+ * @param {ThemeMode} [props.themeMode] - (Optional) Theme mode
139
+ * @param {Function} [props.setThemeMode] - (Optional) Function to set the theme mode
140
+ * @returns {JSX.Element} Provider component
141
+ * @example
142
+ * ```tsx
143
+ * const client = new ClientSdk({ appId: 'my-app-id', user: {
144
+ * email: 'test@test.com',
145
+ * firstName: 'Test',
146
+ * lastName: 'User',
147
+ * isAuthenticated: true,
148
+ * } });
149
+ *
150
+ * function App() {
151
+ * return (
152
+ * <ClientProvider client={client}>
153
+ * <YourAppComponents />
154
+ * </ClientProvider>
155
+ * );
156
+ * }
157
+ * ```
158
+ */
159
+ export const ClientProvider = ({ client, children, themeMode, setThemeMode, }) => {
160
+ const [queryClient] = useState(() => new QueryClient());
161
+ const user = client.getUser();
162
+ const userIdInNumber = useMemo(() => {
163
+ if (user?.id) {
164
+ const num = Number(user.id);
165
+ if (isNaN(num)) {
166
+ return undefined;
167
+ }
168
+ return num;
169
+ }
170
+ return undefined;
171
+ }, [user?.id]);
172
+ useWebsockets({ userId: userIdInNumber, token: client.token });
173
+ useWebsocketsAppSubscribe({ appId: client.appId });
174
+ const dataChangeCallback = useCallback(async (data) => {
175
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, data.table] });
176
+ data.affectRows?.forEach((row) => {
177
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, data.table, "one", { id: row.id }] });
178
+ });
179
+ }, [queryClient]);
180
+ useEffect(() => {
181
+ websocketsService.listen(AppDataEventTypes.APP_DATA_INSERT, dataChangeCallback);
182
+ websocketsService.listen(AppDataEventTypes.APP_DATA_UPDATE, dataChangeCallback);
183
+ websocketsService.listen(AppDataEventTypes.APP_DATA_DELETE, dataChangeCallback);
184
+ return () => {
185
+ websocketsService.unlisten(AppDataEventTypes.APP_DATA_INSERT, dataChangeCallback);
186
+ websocketsService.unlisten(AppDataEventTypes.APP_DATA_UPDATE, dataChangeCallback);
187
+ websocketsService.unlisten(AppDataEventTypes.APP_DATA_DELETE, dataChangeCallback);
188
+ };
189
+ }, [dataChangeCallback]);
190
+ return (<ClientContext value={client}>
191
+ <ThemeModeContext value={{ themeMode, setThemeMode }}>
192
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
193
+ </ThemeModeContext>
194
+ </ClientContext>);
195
+ };
196
+ /**
197
+ * Hook to access the ClientSdk instance from context
198
+ *
199
+ * This hook provides direct access to the ClientSdk, which is the core client
200
+ * for interacting with entities, actions, and pages.
201
+ *
202
+ * @returns {ClientSdk} The ClientSdk instance
203
+ * @throws {Error} If used outside of ClientProvider
204
+ * @example
205
+ * ```tsx
206
+ * function MyComponent() {
207
+ * const client = useClient();
208
+ * // Use client directly for advanced use cases
209
+ * const userEntity = client.entity(userConfig);
210
+ * }
211
+ * ```
212
+ */
213
+ export const useClient = () => {
214
+ const client = useContext(ClientContext);
215
+ if (!client) {
216
+ throw new Error("Client not found");
217
+ }
218
+ return client;
219
+ };
220
+ /**
221
+ * Hook to fetch all entities of a specific type
222
+ *
223
+ * Entities represent data objects stored in tables with standard CRUD operations.
224
+ * This hook provides a React Query-powered way to fetch multiple entities.
225
+ *
226
+ * EntityConfig properties:
227
+ * - tableBlockId: Unique identifier for the table
228
+ * - instanceType: TypeScript type defining the entity's structure
229
+ *
230
+ * @template E - Entity configuration type
231
+ * @param {E} entityConfig - Configuration for the entity type
232
+ * @param {any} [filters] - Optional filters to apply to the query
233
+ * @param {Object} [queryOptions] - Optional React Query configuration options
234
+ * @param {boolean} [queryOptions.enabled] - Whether the query should automatically execute
235
+ * @param {EntityType<E>[]} [queryOptions.initialData] - Initial data to use for the query
236
+ * @param {EntityType<E>[]} [queryOptions.placeholderData] - Placeholder data to use while loading
237
+ * @returns {Object} Query result with data, loading state, and error
238
+ * @returns {EntityType<E>[] | undefined} returns.data - The fetched entities
239
+ * @returns {boolean} returns.isLoading - Whether the query is loading
240
+ * @returns {Error | null} returns.error - Any error that occurred
241
+ * @returns {Function} returns.refetch - Function to manually refetch the data
242
+ * @returns {boolean} returns.isError - Whether the query resulted in an error
243
+ * @returns {boolean} returns.isFetched - Whether the query has been fetched
244
+ * @returns {boolean} returns.isFetching - Whether the query is currently fetching
245
+ * @returns {boolean} returns.isSuccess - Whether the query was successful
246
+ * @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
247
+ * @example
248
+ * ```tsx
249
+ * // Define a Item entity configuration
250
+ * const itemConfig = {
251
+ * tableBlockId: 'items-table',
252
+ * instanceType: {} as {
253
+ * name: string;
254
+ * price: number;
255
+ * }
256
+ * };
257
+ *
258
+ * function ItemsList() {
259
+ * const { data: items, isLoading } = useEntityGetAll(itemConfig);
260
+ *
261
+ * if (isLoading) return <div>Loading...</div>;
262
+ * return (
263
+ * <ul>
264
+ * {items?.map(item => <li key={item.id}>{item.name}</li>)}
265
+ * </ul>
266
+ * );
267
+ * }
268
+ * ```
269
+ */
270
+ export const useEntityGetAll = (entityConfig, filters, queryOptions = {}) => {
271
+ const { enabled, initialData, placeholderData } = queryOptions;
272
+ const client = useClient();
273
+ const { data, isLoading, error, isError, isFetched, isFetching, isSuccess, status } = useQuery({
274
+ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, filters],
275
+ queryFn: () => client.entity(entityConfig).findMany(filters),
276
+ enabled,
277
+ initialData,
278
+ placeholderData,
279
+ });
280
+ return {
281
+ data,
282
+ isLoading,
283
+ error,
284
+ refetch: () => ({ data, isLoading, error, isError, isFetched, isFetching, isSuccess, status }),
285
+ isError,
286
+ isFetched,
287
+ isFetching,
288
+ isSuccess,
289
+ status,
290
+ };
291
+ };
292
+ /**
293
+ * Hook to fetch a single entity by filters
294
+ *
295
+ * Retrieves a specific entity instance using filter criteria.
296
+ * EntityConfig defines the structure and mapping of the entity data.
297
+ *
298
+ * @template EC - Entity configuration type
299
+ * @param {EC} entityConfig - Configuration for the entity type
300
+ * @param {Record<string, any>} filters - Filters to identify the entity (e.g., { id: "123" } or { email: "user@example.com" })
301
+ * @param {Object} [queryOptions] - Optional React Query configuration options
302
+ * @param {boolean} [queryOptions.enabled] - Whether the query should automatically execute
303
+ * @param {EntityType<EC> | null} [queryOptions.initialData] - Initial data to use for the query
304
+ * @returns {Object} Query result with data, loading state, and error
305
+ * @returns {EntityType<EC> | undefined | null} returns.data - The fetched entity
306
+ * @returns {boolean} returns.isLoading - Whether the query is loading
307
+ * @returns {Error | null} returns.error - Any error that occurred
308
+ * @returns {Function} returns.refetch - Function to manually refetch the data
309
+ * @returns {boolean} returns.isError - Whether the query resulted in an error
310
+ * @returns {boolean} returns.isFetched - Whether the query has been fetched
311
+ * @returns {boolean} returns.isFetching - Whether the query is currently fetching
312
+ * @returns {boolean} returns.isSuccess - Whether the query was successful
313
+ * @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
314
+ * @example
315
+ * ```tsx
316
+ * // Define the entity configuration
317
+ * const userConfig = {
318
+ * tableBlockId: 'users-table',
319
+ * instanceType: {} as {
320
+ * name: string;
321
+ * email: string;
322
+ * }
323
+ * };
324
+ *
325
+ * function UserProfile({ userEmail }) {
326
+ * const { data: user, isLoading } = useEntityGetOne(UserEntity, { email: userEmail });
327
+ *
328
+ * if (isLoading) return <div>Loading...</div>;
329
+ * if (!user) return <div>User not found</div>;
330
+ *
331
+ * return <div>Name: {user.name}</div>;
332
+ * }
333
+ * ```
334
+ */
335
+ export const useEntityGetOne = (entityConfig, filters, queryOptions = {}) => {
336
+ const { enabled, initialData } = queryOptions;
337
+ const client = useClient();
338
+ let finalFilters = filters;
339
+ if (typeof filters === "string") {
340
+ finalFilters = { id: parseInt(filters) };
341
+ }
342
+ else if (typeof filters === "number") {
343
+ finalFilters = { id: filters };
344
+ }
345
+ const { data, isLoading, error, isError, isFetched, isFetching, isSuccess, status } = useQuery({
346
+ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", finalFilters],
347
+ queryFn: () => client.entity(entityConfig).findOne(finalFilters),
348
+ enabled,
349
+ initialData,
350
+ });
351
+ return {
352
+ data,
353
+ isLoading,
354
+ error,
355
+ refetch: () => ({ data, isLoading, error, isError, isFetched, isFetching, isSuccess, status }),
356
+ isError,
357
+ isFetched,
358
+ isFetching,
359
+ isSuccess,
360
+ status,
361
+ };
362
+ };
363
+ /**
364
+ * Hook to create a new entity
365
+ *
366
+ * Creates a new entity instance in the data store.
367
+ * EntityTypeOnlyMutable<EC> represents the writable properties of the entity.
368
+ *
369
+ * @template EC - Entity configuration type
370
+ * @param {EC} entityConfig - Configuration for the entity type
371
+ * @returns {Object} Mutation object with create function, loading state, and error
372
+ * @example
373
+ * ```tsx
374
+ * // Define the entity configuration
375
+ * const userConfig = {
376
+ * tableBlockId: 'users-table',
377
+ * instanceType: {} as {
378
+ * name: string;
379
+ * email: string;
380
+ * }
381
+ * };
382
+ *
383
+ * function CreateUserForm() {
384
+ * const { createFunction, isLoading } = useEntityCreate(userConfig);
385
+ * const [name, setName] = useState('');
386
+ * const [email, setEmail] = useState('');
387
+ *
388
+ * const handleSubmit = async (e) => {
389
+ * e.preventDefault();
390
+ * await createFunction({
391
+ * data: {
392
+ * name,
393
+ * email
394
+ * }
395
+ * });
396
+ * setName('');
397
+ * setEmail('');
398
+ * };
399
+ *
400
+ * return (
401
+ * <form onSubmit={handleSubmit}>
402
+ * <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" />
403
+ * <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
404
+ * <button type="submit" disabled={isLoading}>Create</button>
405
+ * </form>
406
+ * );
407
+ * }
408
+ * ```
409
+ */
410
+ export const useEntityCreate = (entityConfig) => {
411
+ const client = useClient();
412
+ const queryClient = useQueryClient();
413
+ const { validateExceededMaxCount } = useEntityMutationRateLimiting(entityConfig, "useEntityCreate");
414
+ const { mutateAsync: createFunction, isPending: isLoading, error, } = useMutation({
415
+ mutationFn: ({ data }) => {
416
+ validateExceededMaxCount({ data });
417
+ return client.entity(entityConfig).create(data);
418
+ },
419
+ onSuccess: () => {
420
+ // Invalidate the list so we refetch it with the newly created item
421
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
422
+ },
423
+ });
424
+ if (error instanceof FatalAppError) {
425
+ throw error;
426
+ }
427
+ return { createFunction, isLoading, error };
428
+ };
429
+ /**
430
+ * Hook to create multiple entities in a single operation
431
+ *
432
+ * Creates multiple entity instances in the data store in one batch operation.
433
+ * This is more efficient than calling useEntityCreate multiple times when you need
434
+ * to create several entities at once. EntityTypeOnlyMutable<EC> represents the
435
+ * writable properties of the entity.
436
+ *
437
+ * @template EC - Entity configuration type
438
+ * @param {EC} entityConfig - Configuration for the entity type
439
+ * @returns {Object} Mutation object with createMany function, loading state, and error
440
+ * @example
441
+ * ```tsx
442
+ * // Define the entity configuration
443
+ * const taskConfig = {
444
+ * tableBlockId: 'tasks-table',
445
+ * instanceType: {} as {
446
+ * title: string;
447
+ * status: 'todo' | 'in_progress' | 'done';
448
+ * assigneeId: string;
449
+ * }
450
+ * };
451
+ *
452
+ * function ImportTasksButton() {
453
+ * const { createManyFunction, isLoading } = useEntityCreateMany(taskConfig);
454
+ *
455
+ * const handleImport = async () => {
456
+ * const tasksToCreate = [
457
+ * { title: 'Review code', status: 'todo', assigneeId: 'user-1' },
458
+ * { title: 'Write tests', status: 'todo', assigneeId: 'user-2' },
459
+ * { title: 'Update docs', status: 'in_progress', assigneeId: 'user-1' }
460
+ * ];
461
+ *
462
+ * const createdTasks = await createManyFunction({ data: tasksToCreate });
463
+ * console.log(`Created ${createdTasks.length} tasks`);
464
+ * };
465
+ *
466
+ * return (
467
+ * <button onClick={handleImport} disabled={isLoading}>
468
+ * {isLoading ? 'Importing...' : 'Import Tasks'}
469
+ * </button>
470
+ * );
471
+ * }
472
+ * ```
473
+ */
474
+ export const useEntityCreateMany = (entityConfig) => {
475
+ const client = useClient();
476
+ const queryClient = useQueryClient();
477
+ const { validateExceededMaxCount } = useEntityMutationRateLimiting(entityConfig, "useEntityCreateMany");
478
+ const { mutateAsync: createManyFunction, isPending: isLoading, error, } = useMutation({
479
+ mutationFn: ({ data }) => {
480
+ validateExceededMaxCount({ data });
481
+ return client.entity(entityConfig).createMany(data);
482
+ },
483
+ onSuccess: () => {
484
+ // Invalidate the list so we refetch it with the newly created item
485
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
486
+ },
487
+ });
488
+ if (error instanceof FatalAppError) {
489
+ throw error;
490
+ }
491
+ return { createManyFunction, isLoading, error };
492
+ };
493
+ /**
494
+ * Hook to update an existing entity
495
+ *
496
+ * Updates an existing entity with the provided partial data.
497
+ * Only the properties included in the data object will be updated.
498
+ *
499
+ * @template EC - Entity configuration type
500
+ * @param {EC} entityConfig - Configuration for the entity type
501
+ * @returns {Object} Mutation object with update function, loading state, and error
502
+ * @example
503
+ * ```tsx
504
+ * // Define the entity configuration
505
+ * const userConfig = {
506
+ * tableBlockId: 'users-table',
507
+ * instanceType: {} as {
508
+ * name: string;
509
+ * email: string;
510
+ * }
511
+ * };
512
+ *
513
+ * function EditUserForm({ user }) {
514
+ * const { updateFunction, isLoading } = useEntityUpdate(userConfig);
515
+ * const [name, setName] = useState(user.name);
516
+ *
517
+ * const handleSubmit = async (e) => {
518
+ * e.preventDefault();
519
+ * await updateFunction({
520
+ * id: user.id,
521
+ * data: { name }
522
+ * });
523
+ * };
524
+ *
525
+ * return (
526
+ * <form onSubmit={handleSubmit}>
527
+ * <input value={name} onChange={e => setName(e.target.value)} />
528
+ * <button type="submit" disabled={isLoading}>Update</button>
529
+ * </form>
530
+ * );
531
+ * }
532
+ * ```
533
+ */
534
+ export const useEntityUpdate = (entityConfig) => {
535
+ const client = useClient();
536
+ const queryClient = useQueryClient();
537
+ const { validateExceededMaxCount } = useEntityRowMutationRateLimiting(entityConfig, "useEntityUpdate");
538
+ const { mutateAsync: updateFunction, isPending: isLoading, error, } = useMutation({
539
+ mutationFn: ({ id, data }) => {
540
+ validateExceededMaxCount({ rowId: id, data });
541
+ return client.entity(entityConfig).updateOne(id, data);
542
+ },
543
+ onSuccess: (_result, { id }) => {
544
+ // Invalidate both the list and the specific item's query
545
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
546
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
547
+ },
548
+ });
549
+ if (error instanceof FatalAppError) {
550
+ throw error;
551
+ }
552
+ return { updateFunction, isLoading, error };
553
+ };
554
+ /**
555
+ * Hook to delete an entity
556
+ *
557
+ * Permanently removes an entity from the data store by its ID.
558
+ * Automatically invalidates relevant queries after deletion.
559
+ *
560
+ * @template EC - Entity configuration type
561
+ * @param {EC} entityConfig - Configuration for the entity type
562
+ * @returns {Object} Mutation object with delete function, loading state, and error
563
+ * @example
564
+ * ```tsx
565
+ * // Define the entity configuration
566
+ * const userConfig = {
567
+ * tableBlockId: 'users-table',
568
+ * instanceType: {} as {
569
+ * name: string;
570
+ * email: string;
571
+ * }
572
+ * };
573
+ *
574
+ * function DeleteUserButton({ userId }) {
575
+ * const { deleteFunction, isLoading } = useEntityDelete(userConfig);
576
+ *
577
+ * const handleDelete = async () => {
578
+ * if (confirm('Are you sure?')) {
579
+ * await deleteFunction({ id: userId });
580
+ * }
581
+ * };
582
+ *
583
+ * return (
584
+ * <button onClick={handleDelete} disabled={isLoading}>
585
+ * Delete User
586
+ * </button>
587
+ * );
588
+ * }
589
+ * ```
590
+ */
591
+ export const useEntityDelete = (entityConfig) => {
592
+ const client = useClient();
593
+ const queryClient = useQueryClient();
594
+ const { validateExceededMaxCount } = useEntityRowMutationRateLimiting(entityConfig, "useEntityDelete");
595
+ const { mutateAsync: deleteFunction, isPending: isLoading, error, } = useMutation({
596
+ mutationFn: ({ id }) => {
597
+ validateExceededMaxCount({ rowId: id });
598
+ return client.entity(entityConfig).deleteOne(id);
599
+ },
600
+ onSuccess: (_result, { id }) => {
601
+ // Invalidate both the list and the specific item's query
602
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
603
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
604
+ },
605
+ });
606
+ if (error instanceof FatalAppError) {
607
+ throw error;
608
+ }
609
+ return { deleteFunction, isLoading, error };
610
+ };
611
+ /**
612
+ * Hook to delete multiple entities at once
613
+ *
614
+ * Permanently removes multiple entities from the data store by their IDs.
615
+ * Automatically invalidates relevant queries after deletion.
616
+ *
617
+ * @template EC - Entity configuration type
618
+ * @param {EC} entityConfig - Configuration for the entity type
619
+ * @returns {Object} Mutation object with delete many function, loading state, and error
620
+ * @example
621
+ * ```tsx
622
+ * // Define the entity configuration
623
+ * const userConfig = {
624
+ * tableBlockId: 'users-table',
625
+ * instanceType: {} as {
626
+ * name: string;
627
+ * email: string;
628
+ * }
629
+ * };
630
+ *
631
+ * function DeleteSelectedUsersButton({ selectedUserIds }) {
632
+ * const { deleteManyFunction, isLoading } = useEntityDeleteMany(userConfig);
633
+ *
634
+ * const handleDeleteSelected = async () => {
635
+ * if (confirm(`Delete ${selectedUserIds.length} users?`)) {
636
+ * await deleteManyFunction({ ids: selectedUserIds });
637
+ * }
638
+ * };
639
+ *
640
+ * return (
641
+ * <button onClick={handleDeleteSelected} disabled={isLoading}>
642
+ * Delete Selected ({selectedUserIds.length})
643
+ * </button>
644
+ * );
645
+ * }
646
+ * ```
647
+ */
648
+ export const useEntityDeleteMany = (entityConfig) => {
649
+ const client = useClient();
650
+ const queryClient = useQueryClient();
651
+ const { validateExceededMaxCount } = useEntityMutationRateLimiting(entityConfig, "useEntityDeleteMany");
652
+ const { mutateAsync: deleteManyFunction, isPending: isLoading, error, } = useMutation({
653
+ mutationFn: ({ ids }) => {
654
+ validateExceededMaxCount({ data: ids });
655
+ return client.entity(entityConfig).deleteMany(ids);
656
+ },
657
+ onSuccess: (_result, { ids }) => {
658
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
659
+ ids.forEach((id) => {
660
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
661
+ });
662
+ },
663
+ });
664
+ if (error instanceof FatalAppError) {
665
+ throw error;
666
+ }
667
+ return { deleteManyFunction, isLoading, error };
668
+ };
669
+ /**
670
+ * Hook to execute an action, with support for streaming results
671
+ *
672
+ * Actions represent executable workflows that perform any type of server-side operation,
673
+ * including data processing, business logic, integrations with external
674
+ * services, file operations, AI functionalities, and more. This hook provides a React-friendly
675
+ * way to execute these actions with proper state management.
676
+ *
677
+ * AI capabilities are a particularly interesting use case for actions - you can leverage
678
+ * various AI models and services while the SDK handles the complexity of streaming responses,
679
+ * state management, and UI integration.
680
+ *
681
+ * The hook provides two different result states:
682
+ * - `result`: Contains the final, complete output once the action execution is done
683
+ * - `streamResult`: Contains incrementally updating results during streaming operations,
684
+ * updated in real-time as chunks arrive from the server before the action is complete
685
+ *
686
+ * ActionConfig properties:
687
+ * - actionBlockId: Unique identifier for the action workflow
688
+ * - inputInstanceType: TypeScript type defining the action's input parameters
689
+ * - outputInstanceType: TypeScript type defining the action's output structure
690
+ *
691
+ * @template AC - Action configuration type
692
+ * @param {AC} actionConfig - Configuration for the action
693
+ * @returns {Object} Action execution object with functions, state, and results
694
+ * @returns {Function} returns.executeFunction - Function to execute the action with inputs
695
+ * @returns {AC["outputInstanceType"]} returns.result - Final result after action completes
696
+ * @returns {AC["outputInstanceType"]} returns.streamResult - Incrementally updated result during streaming
697
+ * @returns {boolean} returns.isLoading - Whether the action is currently executing
698
+ * @returns {boolean} returns.isDone - Whether the action has completed
699
+ * @returns {Error | null} returns.error - Any error that occurred during execution
700
+ * @returns {Function} returns.clear - Function to clear result and streamResult values
701
+ * @example
702
+ * ```tsx
703
+ * // Example 1: Process payment action
704
+ * const processPaymentConfig = {
705
+ * actionBlockId: 'process-payment',
706
+ * inputInstanceType: {} as {
707
+ * amount: number;
708
+ * paymentMethod: string;
709
+ * currency: string;
710
+ * },
711
+ * outputInstanceType: {} as {
712
+ * success: boolean;
713
+ * transactionId: string;
714
+ * receiptUrl?: string;
715
+ * error?: string;
716
+ * }
717
+ * };
718
+ *
719
+ * function PaymentForm() {
720
+ * const { executeFunction, result, isLoading, error } = useExecuteAction(processPaymentConfig);
721
+ * const [amount, setAmount] = useState('');
722
+ * const [paymentMethod, setPaymentMethod] = useState('credit_card');
723
+ *
724
+ * const handleSubmit = async (e) => {
725
+ * e.preventDefault();
726
+ * await executeFunction({
727
+ * amount: parseFloat(amount),
728
+ * paymentMethod,
729
+ * currency: 'USD'
730
+ * });
731
+ * };
732
+ *
733
+ * return (
734
+ * <div>
735
+ * <form onSubmit={handleSubmit}>
736
+ * <input
737
+ * type="number"
738
+ * value={amount}
739
+ * onChange={e => setAmount(e.target.value)}
740
+ * placeholder="Amount"
741
+ * />
742
+ * <select value={paymentMethod} onChange={e => setPaymentMethod(e.target.value)}>
743
+ * <option value="credit_card">Credit Card</option>
744
+ * <option value="paypal">PayPal</option>
745
+ * </select>
746
+ * <button type="submit" disabled={isLoading}>Process Payment</button>
747
+ * </form>
748
+ *
749
+ * {isLoading && <div>Processing payment...</div>}
750
+ * {error && <div className="error">Error: {error.message}</div>}
751
+ * {result?.success && <div className="success">
752
+ * Payment successful! Transaction ID: {result.transactionId}
753
+ * </div>}
754
+ * </div>
755
+ * );
756
+ * }
757
+ *
758
+ * // Example 2: Data export action (with streaming)
759
+ * const exportDataConfig = {
760
+ * actionBlockId: 'export-data',
761
+ * inputInstanceType: {} as {
762
+ * format: 'csv' | 'json';
763
+ * filters: Record<string, any>;
764
+ * },
765
+ * outputInstanceType: {} as {
766
+ * progress: number;
767
+ * downloadUrl?: string;
768
+ * status: string;
769
+ * }
770
+ * };
771
+ *
772
+ * function DataExportTool() {
773
+ * const { executeFunction, result, isLoading, isDone } = useExecuteAction(exportDataConfig);
774
+ * const [format, setFormat] = useState<'csv' | 'json'>('csv');
775
+ *
776
+ * const handleExport = async () => {
777
+ * await executeFunction({
778
+ * format,
779
+ * filters: { startDate: '2023-01-01' }
780
+ * });
781
+ * };
782
+ *
783
+ * return (
784
+ * <div>
785
+ * <div>
786
+ * <select value={format} onChange={e => setFormat(e.target.value as 'csv' | 'json')}>
787
+ * <option value="csv">CSV</option>
788
+ * <option value="json">JSON</option>
789
+ * </select>
790
+ * <button onClick={handleExport} disabled={isLoading}>Export Data</button>
791
+ * </div>
792
+ *
793
+ * {isLoading && !isDone && (
794
+ * <div>
795
+ * {result?.progress ? `Export in progress: ${result.progress}%` : 'Starting export...'}
796
+ * </div>
797
+ * )}
798
+ * {isDone && result?.downloadUrl && (
799
+ * <a href={result.downloadUrl} download>Download your data</a>
800
+ * )}
801
+ * </div>
802
+ * );
803
+ * }
804
+ * ```
805
+ */
806
+ export const useExecuteAction = (actionConfig) => {
807
+ const client = useClient();
808
+ const [isDone, setIsDone] = useState(false);
809
+ const { validateExceededMaxCount } = useActionRateLimiting(actionConfig, "useExecuteAction");
810
+ const [streamResult, setStreamResult] = useState();
811
+ const { handleChunk, onCompleteChunks } = useAiStreamChunks({
812
+ onChunk: (chunkData) => {
813
+ // TODO: This is a very hard-coded solution designed for the generate text action, who uses "response" as the key
814
+ // We should find a better way to handle this
815
+ setStreamResult((prevResult) => {
816
+ const prevResponse = prevResult?.response || "";
817
+ return {
818
+ ...(prevResult || {}),
819
+ response: prevResponse + chunkData,
820
+ };
821
+ });
822
+ },
823
+ });
824
+ const { data: result, mutateAsync: executeFunction, isPending: isLoading, error, reset, } = useMutation({
825
+ mutationFn: async (inputs) => {
826
+ let streamResultHandled = false;
827
+ validateExceededMaxCount(inputs);
828
+ const result = await client.action(actionConfig).execute(inputs, {
829
+ onChunk: (chunkData) => {
830
+ streamResultHandled = true;
831
+ handleChunk(chunkData.contentIndex, chunkData.content);
832
+ },
833
+ });
834
+ if (!streamResultHandled) {
835
+ setStreamResult(result);
836
+ }
837
+ return result;
838
+ },
839
+ onMutate: () => {
840
+ setStreamResult(undefined);
841
+ setIsDone(false);
842
+ },
843
+ onSettled: () => {
844
+ onCompleteChunks();
845
+ setIsDone(true);
846
+ },
847
+ });
848
+ if (error instanceof FatalAppError) {
849
+ throw error;
850
+ }
851
+ const clear = useCallback(() => {
852
+ reset();
853
+ setStreamResult(undefined);
854
+ }, []);
855
+ return { executeFunction, result, streamResult, isLoading, isDone, error, clear };
856
+ };
857
+ /**
858
+ * Hook to get an AgentChat instance
859
+ *
860
+ * This hook provides access to an AgentChat instance for the specified configuration.
861
+ *
862
+ * @template ACC - AgentChat configuration type
863
+ * @param {ACC} agentChatConfig - Configuration for the agent chat
864
+ * @returns {AgentChat<ACC>} An AgentChat instance for the given configuration
865
+ * @example
866
+ * ```tsx
867
+ * // Define the agent chat configuration
868
+ * const agentChatConfig = {
869
+ * agentChatId: 'agent-chat-id'
870
+ * };
871
+ *
872
+ * function AgentChatComponent() {
873
+ * const agentChat = useAgentChat(agentChatConfig);
874
+ * return <AgentChat agentChat={agentChat} />;
875
+ * }
876
+ * ```
877
+ */
878
+ export const useAgentChat = (agentChatConfig) => {
879
+ const client = useClient();
880
+ return client.agentChat(agentChatConfig);
881
+ };
882
+ /**
883
+ * Hook to handle file uploads
884
+ *
885
+ * Provides a function to upload files with progress tracking
886
+ *
887
+ * @returns {Object} Upload object with upload functionality and status
888
+ * @returns {Function} returns.uploadFunction - Function that takes a File and returns a Promise with the URL
889
+ * @returns {boolean} returns.isLoading - Whether an upload is in progress
890
+ * @returns {number} returns.uploadPercentage - Current upload progress (0-100)
891
+ * @example
892
+ * ```tsx
893
+ * function FileUploader() {
894
+ * const { uploadFunction, isLoading, uploadPercentage } = useFileUpload();
895
+ * const [fileUrl, setFileUrl] = useState('');
896
+ *
897
+ * const handleFileChange = async (event) => {
898
+ * const file = event.target.files[0];
899
+ * if (file) {
900
+ * try {
901
+ * const url = await uploadFunction(file);
902
+ * setFileUrl(url);
903
+ * } catch (error) {
904
+ * console.error('Upload failed:', error);
905
+ * }
906
+ * }
907
+ * };
908
+ *
909
+ * return (
910
+ * <div>
911
+ * <input type="file" onChange={handleFileChange} disabled={isLoading} />
912
+ * {isLoading && <Progress value={uploadPercentage} />}
913
+ * {fileUrl && <img src={fileUrl} alt="Uploaded file" />}
914
+ * </div>
915
+ * );
916
+ * }
917
+ * ```
918
+ */
919
+ export const useFileUpload = () => {
920
+ const client = useClient();
921
+ const [isLoading, setIsLoading] = useState(false);
922
+ const [uploadPercentage, setUploadPercentage] = useState(0);
923
+ const uploadFunction = useCallback(async (file) => {
924
+ setIsLoading(true);
925
+ setUploadPercentage(0);
926
+ try {
927
+ const protectedUrl = await client.upload(file, {
928
+ onProgress: (percentage) => {
929
+ setUploadPercentage(percentage);
930
+ },
931
+ });
932
+ return protectedUrl;
933
+ }
934
+ finally {
935
+ setIsLoading(false);
936
+ }
937
+ }, [client]);
938
+ return { uploadFunction, isLoading, uploadPercentage };
939
+ };
940
+ /**
941
+ * Represents a user's data
942
+ * @interface User
943
+ * @property {string} [id] - The user's ID (optional, must exist if user is authenticated)
944
+ * @property {string} [email] - The user's email (optional, must exist if user is authenticated)
945
+ * @property {string} [name] - The user's name (optional, must exist if user is authenticated)
946
+ * @property {string} [profileImageUrl] - The user's profile image URL (optional)
947
+ * @property {string} [firstName] - The user's first name (optional)
948
+ * @property {string} [lastName] - The user's last name (optional)
949
+ * @property {string} [role] - The user's role name (optional)
950
+ * @property {string} [permission] - The user's permission level: 'build' (can modify app) or 'use' (read-only) (optional)
951
+ */
952
+ /**
953
+ * Hook to get the current user information
954
+ *
955
+ * This hook provides access to the user's data
956
+ * that was provided during SDK initialization, including role information for multi-role apps.
957
+ *
958
+ * @returns {User} The current user object with optional role property
959
+ * @example
960
+ * ```tsx
961
+ * const user = useUser();
962
+ * console.log(user.isAuthenticated ? `Logged in as: ${user.firstName} ${user.lastName}` : "Not logged in");
963
+ *
964
+ * // For multi-role apps, conditionally render based on role
965
+ * if (user.role === "Manager") {
966
+ * return <ManagerDashboard />;
967
+ * } else if (user.role === "Employee") {
968
+ * return <EmployeeDashboard />;
969
+ * }
970
+ * ```
971
+ */
972
+ export const useUser = () => {
973
+ const client = useClient();
974
+ const { data: user } = useQuery({
975
+ queryKey: [CURRENT_USER_QUERY_KEY],
976
+ queryFn: () => client.getUser(),
977
+ initialData: client.getUser(),
978
+ });
979
+ return user;
980
+ };
981
+ /**
982
+ * Hook to manage the application theme mode
983
+ *
984
+ * This hook provides access to the current theme mode and a function to change it.
985
+ * It supports three theme modes: dark, light, and system (which follows the user's OS preference).
986
+ *
987
+ * @returns {Object} Object containing current theme mode and setter function
988
+ * @returns {("dark" | "light" | "system")} themeMode - The current theme mode
989
+ * @returns {Function} setThemeMode - Function to update the theme mode
990
+ * @example
991
+ * ```tsx
992
+ * function ThemeToggle() {
993
+ * const { themeMode, setThemeMode } = useThemeMode();
994
+ *
995
+ * return (
996
+ * <div>
997
+ * <p>Current theme: {themeMode}</p>
998
+ * <button onClick={() => setThemeMode("dark")}>Dark</button>
999
+ * <button onClick={() => setThemeMode("light")}>Light</button>
1000
+ * <button onClick={() => setThemeMode("system")}>System</button>
1001
+ * </div>
1002
+ * );
1003
+ * }
1004
+ *
1005
+ * // Example with a select dropdown
1006
+ * function ThemeSelector() {
1007
+ * const { themeMode, setThemeMode } = useThemeMode();
1008
+ *
1009
+ * return (
1010
+ * <select
1011
+ * value={themeMode}
1012
+ * onChange={(e) => setThemeMode(e.target.value as "dark" | "light" | "system")}
1013
+ * >
1014
+ * <option value="system">System</option>
1015
+ * <option value="light">Light</option>
1016
+ * <option value="dark">Dark</option>
1017
+ * </select>
1018
+ * );
1019
+ * }
1020
+ * ```
1021
+ */
1022
+ export const useThemeMode = () => {
1023
+ const { themeMode, setThemeMode } = useContext(ThemeModeContext);
1024
+ return { themeMode, setThemeMode };
1025
+ };
1026
+ /**
1027
+ * Hook to change a user's role in multi-role applications
1028
+ *
1029
+ * This hook provides functionality to change a user's role, but only if the current user
1030
+ * has "build" permission. The hook automatically invalidates relevant queries after a successful
1031
+ * role change, including the current user's data if they changed their own role.
1032
+ *
1033
+ * @param {Object} [params] - Hook parameters
1034
+ * @param {UpdateUserOptions} [params.options] - Optional configuration for the user update
1035
+ * @returns {Object} Mutation object with role change function, loading state, error, and authorization state
1036
+ * @returns {Function} returns.changeUserRoleFunction - Function to change a user's role
1037
+ * @returns {boolean} returns.isLoading - Whether the role change is in progress
1038
+ * @returns {Error | null} returns.error - Any error that occurred during the role change
1039
+ * @returns {boolean} returns.isEnabled - Whether the current user is authorized to change roles (has "build" permission)
1040
+ * @example
1041
+ * ```tsx
1042
+ * function UserRoleManager({ userId, currentRole }) {
1043
+ * const { changeUserRoleFunction, isLoading, error, isEnabled } = useChangeUserRole();
1044
+ * const [selectedRole, setSelectedRole] = useState(currentRole);
1045
+ *
1046
+ * const handleRoleChange = async () => {
1047
+ * try {
1048
+ * await changeUserRoleFunction({
1049
+ * userId,
1050
+ * role: selectedRole
1051
+ * });
1052
+ * alert('Role changed successfully!');
1053
+ * } catch (err) {
1054
+ * console.error('Failed to change role:', err);
1055
+ * }
1056
+ * };
1057
+ *
1058
+ * if (!isEnabled) {
1059
+ * return <div>You don't have permission to change user roles</div>;
1060
+ * }
1061
+ *
1062
+ * return (
1063
+ * <div>
1064
+ * <select
1065
+ * value={selectedRole}
1066
+ * onChange={(e) => setSelectedRole(e.target.value)}
1067
+ * disabled={isLoading}
1068
+ * >
1069
+ * <option value="Admin">Admin</option>
1070
+ * <option value="Manager">Manager</option>
1071
+ * <option value="Employee">Employee</option>
1072
+ * </select>
1073
+ * <button onClick={handleRoleChange} disabled={isLoading}>
1074
+ * {isLoading ? 'Changing...' : 'Change Role'}
1075
+ * </button>
1076
+ * {error && <div className="error">Error: {error.message}</div>}
1077
+ * </div>
1078
+ * );
1079
+ * }
1080
+ * ```
1081
+ */
1082
+ export const useChangeUserRole = ({ options } = {}) => {
1083
+ const client = useClient();
1084
+ const queryClient = useQueryClient();
1085
+ const currentUser = useUser();
1086
+ const isEnabled = currentUser?.permission === "build";
1087
+ const { mutateAsync: changeUserRoleFunction, isPending: isLoading, error, } = useMutation({
1088
+ mutationFn: ({ userId, role }) => {
1089
+ if (!isEnabled) {
1090
+ throw new Error("You are not authorized to change user roles");
1091
+ }
1092
+ return client.changeUserRole(userId, role, options);
1093
+ },
1094
+ onSuccess: (_result, { userId }) => {
1095
+ if (currentUser.id && userId === currentUser.id) {
1096
+ queryClient.invalidateQueries({ queryKey: [CURRENT_USER_QUERY_KEY] });
1097
+ }
1098
+ queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, USERS_TABLE_BLOCK_ID] });
1099
+ queryClient.invalidateQueries({
1100
+ queryKey: [ENTITIES_QUERY_KEY, USERS_TABLE_BLOCK_ID, "one", { id: userId }],
1101
+ });
1102
+ },
1103
+ });
1104
+ if (error instanceof FatalAppError) {
1105
+ throw error;
1106
+ }
1107
+ return { changeUserRoleFunction, isLoading, error, isEnabled };
1108
+ };
1109
+ /**
1110
+ * Hook to send a passwordless login link (magic link) to a user's email
1111
+ *
1112
+ * This hook provides functionality to send an email containing a one-time login link
1113
+ * that allows users to authenticate without entering a password. When the user clicks
1114
+ * the link in their email, they will be automatically logged into the application.
1115
+ * The hook manages loading state, success state, and error handling automatically.
1116
+ *
1117
+ * @returns {Object} Object with send function, loading state, success state, and error
1118
+ * @returns {Function} returns.sendLoginLink - Function to send the login link to an email address
1119
+ * @returns {boolean} returns.isLoading - Whether the request is currently in progress
1120
+ * @returns {boolean} returns.isSuccess - Whether the login link was sent successfully
1121
+ * @returns {Error | null} returns.error - Any error that occurred during the request
1122
+ * @example
1123
+ * ```tsx
1124
+ * function PasswordlessLoginForm() {
1125
+ * const { sendLoginLink, isLoading, isSuccess, error } = useSendLoginLink();
1126
+ * const [email, setEmail] = useState('');
1127
+ *
1128
+ * const handleSubmit = async (e: React.FormEvent) => {
1129
+ * e.preventDefault();
1130
+ * await sendLoginLink({ email });
1131
+ * };
1132
+ *
1133
+ * if (isSuccess) {
1134
+ * return <div>Check your email for a login link!</div>;
1135
+ * }
1136
+ *
1137
+ * return (
1138
+ * <form onSubmit={handleSubmit}>
1139
+ * <input
1140
+ * type="email"
1141
+ * value={email}
1142
+ * onChange={(e) => setEmail(e.target.value)}
1143
+ * placeholder="Enter your email"
1144
+ * disabled={isLoading}
1145
+ * />
1146
+ * <button type="submit" disabled={isLoading}>
1147
+ * {isLoading ? 'Sending...' : 'Send Login Link'}
1148
+ * </button>
1149
+ * {error && <div className="error">{error.message}</div>}
1150
+ * </form>
1151
+ * );
1152
+ * }
1153
+ * ```
1154
+ */
1155
+ export const useSendLoginLink = () => {
1156
+ const { sendLoginLink: clientSendLoginLink } = useClient();
1157
+ const [isLoading, setIsLoading] = useState(false);
1158
+ const [isSuccess, setIsSuccess] = useState(false);
1159
+ const [error, setError] = useState(null);
1160
+ const sendLoginLink = useCallback(async ({ email }) => {
1161
+ setIsLoading(true);
1162
+ setIsSuccess(false);
1163
+ setError(null);
1164
+ try {
1165
+ await clientSendLoginLink({ email });
1166
+ setIsSuccess(true);
1167
+ setError(null);
1168
+ }
1169
+ catch (error) {
1170
+ setIsSuccess(false);
1171
+ setError(error);
1172
+ }
1173
+ finally {
1174
+ setIsLoading(false);
1175
+ }
1176
+ }, [clientSendLoginLink]);
1177
+ return { sendLoginLink, isLoading, isSuccess, error };
1178
+ };
1179
+ /**
1180
+ * Hook to get the Google OAuth login URL
1181
+ *
1182
+ * This hook returns the URL for initiating the Google OAuth login flow.
1183
+ * When users navigate to this URL, they will be redirected to Google's authentication page.
1184
+ * After successful authentication with Google, they will be redirected back to the application
1185
+ * and automatically logged in.
1186
+ *
1187
+ * @returns {string} The Google OAuth login URL
1188
+ * @example
1189
+ * ```tsx
1190
+ * // Example 1: Use in a Link component
1191
+ * function GoogleLoginLink() {
1192
+ * const googleLoginUrl = useGoogleLogin();
1193
+ *
1194
+ * return (
1195
+ * <Link to={googleLoginUrl}>
1196
+ * <Button>Sign in with Google</Button>
1197
+ * </Link>
1198
+ * );
1199
+ * }
1200
+ *
1201
+ * // Example 2: Redirect with a button
1202
+ * function GoogleLoginButton() {
1203
+ * const googleLoginUrl = useGoogleLogin();
1204
+ *
1205
+ * const handleGoogleLogin = () => {
1206
+ * window.location.href = googleLoginUrl;
1207
+ * };
1208
+ *
1209
+ * return <Button onClick={handleGoogleLogin}>Sign in with Google</Button>;
1210
+ * }
1211
+ * ```
1212
+ */
1213
+ export const useGoogleLogin = () => {
1214
+ const { getGoogleLoginUrl: clientGetGoogleLoginUrl } = useClient();
1215
+ return clientGetGoogleLoginUrl();
1216
+ };
1217
+ /**
1218
+ * @deprecated
1219
+ * This hook is deprecated and should not be used.
1220
+ */
1221
+ export const usePageParams = (pageConfig) => {
1222
+ const client = useClient();
1223
+ const pageParams = useMemo(() => {
1224
+ const page = client.page(pageConfig);
1225
+ return page.getParams();
1226
+ }, [client, pageConfig.pageBlockId]);
1227
+ return pageParams;
1228
+ };
1229
+ /**
1230
+ * @deprecated
1231
+ * This hook is deprecated and should not be used.
1232
+ */
1233
+ export const usePageUrl = (pageConfig) => {
1234
+ const client = useClient();
1235
+ const pageParams = usePageParams(pageConfig);
1236
+ return client.page(pageConfig).getUrl(pageParams);
1237
+ };
1238
+ //# sourceMappingURL=ReactClientSdk.jsx.map