@objectstack/client-react 4.0.4 → 4.0.5

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.
@@ -1,634 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- /**
4
- * Data Query Hooks
5
- *
6
- * React hooks for querying and mutating ObjectStack data
7
- */
8
-
9
- import { useState, useEffect, useCallback, useRef } from 'react';
10
- import { QueryAST, FilterCondition } from '@objectstack/spec/data';
11
- import { PaginatedResult } from '@objectstack/client';
12
- import { useClient } from './context';
13
-
14
- /**
15
- * Query options for useQuery hook
16
- *
17
- * Supports both **canonical** (Spec protocol) and **legacy** field names.
18
- * Canonical names are preferred; legacy names are accepted for backward
19
- * compatibility and will be removed in a future major release.
20
- *
21
- * | Canonical | Legacy (deprecated) |
22
- * |-----------|---------------------|
23
- * | `where` | `filters` |
24
- * | `fields` | `select` |
25
- * | `orderBy` | `sort` |
26
- * | `limit` | `top` |
27
- * | `offset` | `skip` |
28
- */
29
- export interface UseQueryOptions<T = any> {
30
- /** Query AST or simplified query options */
31
- query?: Partial<QueryAST>;
32
-
33
- // ── Canonical (Spec protocol) field names ──────────────────────────
34
- /** Filter conditions (WHERE clause). */
35
- where?: FilterCondition;
36
- /** Fields to retrieve (SELECT clause). */
37
- fields?: string[];
38
- /** Sort definition (ORDER BY clause). */
39
- orderBy?: string | string[];
40
- /** Maximum number of records to return (LIMIT). */
41
- limit?: number;
42
- /** Number of records to skip (OFFSET). */
43
- offset?: number;
44
-
45
- // ── Legacy field names (deprecated) ────────────────────────────────
46
- /** @deprecated Use `fields` instead. */
47
- select?: string[];
48
- /** @deprecated Use `where` instead. */
49
- filters?: FilterCondition;
50
- /** @deprecated Use `orderBy` instead. */
51
- sort?: string | string[];
52
- /** @deprecated Use `limit` instead. */
53
- top?: number;
54
- /** @deprecated Use `offset` instead. */
55
- skip?: number;
56
-
57
- /** Enable/disable automatic query execution */
58
- enabled?: boolean;
59
- /** Refetch interval in milliseconds */
60
- refetchInterval?: number;
61
- /** Callback on successful query */
62
- onSuccess?: (data: PaginatedResult<T>) => void;
63
- /** Callback on error */
64
- onError?: (error: Error) => void;
65
- }
66
-
67
- /**
68
- * Query result for useQuery hook
69
- */
70
- export interface UseQueryResult<T = any> {
71
- /** Query result data */
72
- data: PaginatedResult<T> | null;
73
- /** Loading state */
74
- isLoading: boolean;
75
- /** Error state */
76
- error: Error | null;
77
- /** Refetch the query */
78
- refetch: () => Promise<void>;
79
- /** Is currently refetching */
80
- isRefetching: boolean;
81
- }
82
-
83
- /**
84
- * Hook for querying ObjectStack data with automatic caching and refetching
85
- *
86
- * @example
87
- * ```tsx
88
- * function TaskList() {
89
- * const { data, isLoading, error, refetch } = useQuery('todo_task', {
90
- * fields: ['id', 'subject', 'priority'],
91
- * orderBy: ['-created_at'],
92
- * limit: 20
93
- * });
94
- *
95
- * if (isLoading) return <div>Loading...</div>;
96
- * if (error) return <div>Error: {error.message}</div>;
97
- *
98
- * return (
99
- * <div>
100
- * {data?.value.map(task => (
101
- * <div key={task.id}>{task.subject}</div>
102
- * ))}
103
- * </div>
104
- * );
105
- * }
106
- * ```
107
- */
108
- export function useQuery<T = any>(
109
- object: string,
110
- options: UseQueryOptions<T> = {}
111
- ): UseQueryResult<T> {
112
- const client = useClient();
113
- const [data, setData] = useState<PaginatedResult<T> | null>(null);
114
- const [isLoading, setIsLoading] = useState(true);
115
- const [isRefetching, setIsRefetching] = useState(false);
116
- const [error, setError] = useState<Error | null>(null);
117
- const intervalRef = useRef<NodeJS.Timeout | undefined>(undefined);
118
-
119
- const {
120
- query,
121
- // Canonical names take precedence over legacy names
122
- where, fields, orderBy, limit, offset,
123
- // Legacy names (deprecated fallbacks)
124
- select, filters, sort, top, skip,
125
- enabled = true,
126
- refetchInterval,
127
- onSuccess,
128
- onError
129
- } = options;
130
-
131
- // Resolve canonical vs legacy: canonical wins when both are provided
132
- const resolvedFields = fields ?? select;
133
- const resolvedWhere = where ?? filters;
134
- const resolvedSort = orderBy ?? sort;
135
- const resolvedLimit = limit ?? top;
136
- const resolvedOffset = offset ?? skip;
137
-
138
- const fetchData = useCallback(async (isRefetch = false) => {
139
- if (!enabled) return;
140
-
141
- try {
142
- if (isRefetch) {
143
- setIsRefetching(true);
144
- } else {
145
- setIsLoading(true);
146
- }
147
- setError(null);
148
-
149
- let result: PaginatedResult<T>;
150
-
151
- if (query) {
152
- // Use advanced query API
153
- result = await client.data.query<T>(object, query);
154
- } else {
155
- // Use canonical QueryOptionsV2 for the find call
156
- result = await client.data.find<T>(object, {
157
- where: resolvedWhere as any,
158
- fields: resolvedFields,
159
- orderBy: resolvedSort,
160
- limit: resolvedLimit,
161
- offset: resolvedOffset,
162
- });
163
- }
164
-
165
- setData(result);
166
- onSuccess?.(result);
167
- } catch (err) {
168
- const error = err instanceof Error ? err : new Error('Query failed');
169
- setError(error);
170
- onError?.(error);
171
- } finally {
172
- setIsLoading(false);
173
- setIsRefetching(false);
174
- }
175
- }, [client, object, query, resolvedFields, resolvedWhere, resolvedSort, resolvedLimit, resolvedOffset, enabled, onSuccess, onError]);
176
-
177
- // Initial fetch and dependency-based refetch
178
- useEffect(() => {
179
- fetchData();
180
- }, [fetchData]);
181
-
182
- // Setup refetch interval
183
- useEffect(() => {
184
- if (refetchInterval && enabled) {
185
- intervalRef.current = setInterval(() => {
186
- fetchData(true);
187
- }, refetchInterval);
188
-
189
- return () => {
190
- if (intervalRef.current) {
191
- clearInterval(intervalRef.current);
192
- }
193
- };
194
- }
195
- return undefined;
196
- }, [refetchInterval, enabled, fetchData]);
197
-
198
- const refetch = useCallback(async () => {
199
- await fetchData(true);
200
- }, [fetchData]);
201
-
202
- return {
203
- data,
204
- isLoading,
205
- error,
206
- refetch,
207
- isRefetching
208
- };
209
- }
210
-
211
- /**
212
- * Mutation options for useMutation hook
213
- */
214
- export interface UseMutationOptions<TData = any, TVariables = any> {
215
- /** Callback on successful mutation */
216
- onSuccess?: (data: TData, variables: TVariables) => void;
217
- /** Callback on error */
218
- onError?: (error: Error, variables: TVariables) => void;
219
- /** Callback when mutation is settled (success or error) */
220
- onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
221
- }
222
-
223
- /**
224
- * Mutation result for useMutation hook
225
- */
226
- export interface UseMutationResult<TData = any, TVariables = any> {
227
- /** Execute the mutation */
228
- mutate: (variables: TVariables) => Promise<TData>;
229
- /** Async version of mutate that throws errors */
230
- mutateAsync: (variables: TVariables) => Promise<TData>;
231
- /** Mutation result data */
232
- data: TData | null;
233
- /** Loading state */
234
- isLoading: boolean;
235
- /** Error state */
236
- error: Error | null;
237
- /** Reset mutation state */
238
- reset: () => void;
239
- }
240
-
241
- /**
242
- * Hook for creating, updating, or deleting ObjectStack data
243
- *
244
- * @example
245
- * ```tsx
246
- * function CreateTaskForm() {
247
- * const { mutate, isLoading, error } = useMutation('todo_task', 'create', {
248
- * onSuccess: (data) => {
249
- * console.log('Task created:', data);
250
- * }
251
- * });
252
- *
253
- * const handleSubmit = (formData) => {
254
- * mutate(formData);
255
- * };
256
- *
257
- * return <form onSubmit={handleSubmit}>...</form>;
258
- * }
259
- * ```
260
- */
261
- export function useMutation<TData = any, TVariables = any>(
262
- object: string,
263
- operation: 'create' | 'update' | 'delete' | 'createMany' | 'updateMany' | 'deleteMany',
264
- options: UseMutationOptions<TData, TVariables> = {}
265
- ): UseMutationResult<TData, TVariables> {
266
- const client = useClient();
267
- const [data, setData] = useState<TData | null>(null);
268
- const [isLoading, setIsLoading] = useState(false);
269
- const [error, setError] = useState<Error | null>(null);
270
-
271
- const { onSuccess, onError, onSettled } = options;
272
-
273
- const mutateAsync = useCallback(async (variables: TVariables): Promise<TData> => {
274
- setIsLoading(true);
275
- setError(null);
276
-
277
- try {
278
- let result: TData;
279
-
280
- switch (operation) {
281
- case 'create':
282
- result = (await client.data.create(object, variables as any)) as TData;
283
- break;
284
- case 'update':
285
- // Expect variables to be { id: string, data: Partial<T> }
286
- const updateVars = variables as any;
287
- result = (await client.data.update(object, updateVars.id, updateVars.data)) as TData;
288
- break;
289
- case 'delete':
290
- // Expect variables to be { id: string }
291
- const deleteVars = variables as any;
292
- result = await client.data.delete(object, deleteVars.id) as any;
293
- break;
294
- case 'createMany':
295
- // createMany returns an array, which may not match TData type
296
- result = await client.data.createMany(object, variables as any) as any;
297
- break;
298
- case 'updateMany':
299
- // Expect variables to be { records: Array<{ id: string, data: Partial<T> }> }
300
- const updateManyVars = variables as any;
301
- result = await client.data.updateMany(object, updateManyVars.records, updateManyVars.options) as any;
302
- break;
303
- case 'deleteMany':
304
- // Expect variables to be { ids: string[] }
305
- const deleteManyVars = variables as any;
306
- result = await client.data.deleteMany(object, deleteManyVars.ids, deleteManyVars.options) as any;
307
- break;
308
- default:
309
- throw new Error(`Unknown operation: ${operation}`);
310
- }
311
-
312
- setData(result);
313
- onSuccess?.(result, variables);
314
- onSettled?.(result, null, variables);
315
-
316
- return result;
317
- } catch (err) {
318
- const error = err instanceof Error ? err : new Error('Mutation failed');
319
- setError(error);
320
- onError?.(error, variables);
321
- onSettled?.(undefined, error, variables);
322
- throw error;
323
- } finally {
324
- setIsLoading(false);
325
- }
326
- }, [client, object, operation, onSuccess, onError, onSettled]);
327
-
328
- const mutate = useCallback((variables: TVariables): Promise<TData> => {
329
- return mutateAsync(variables).catch(() => {
330
- // Swallow error for non-async version
331
- // Error is still available in the error state
332
- return null as any;
333
- });
334
- }, [mutateAsync]);
335
-
336
- const reset = useCallback(() => {
337
- setData(null);
338
- setError(null);
339
- setIsLoading(false);
340
- }, []);
341
-
342
- return {
343
- mutate,
344
- mutateAsync,
345
- data,
346
- isLoading,
347
- error,
348
- reset
349
- };
350
- }
351
-
352
- /**
353
- * Pagination options for usePagination hook
354
- */
355
- export interface UsePaginationOptions<T = any> extends Omit<UseQueryOptions<T>, 'top' | 'skip' | 'limit' | 'offset'> {
356
- /** Page size */
357
- pageSize?: number;
358
- /** Initial page (1-based) */
359
- initialPage?: number;
360
- }
361
-
362
- /**
363
- * Pagination result for usePagination hook
364
- */
365
- export interface UsePaginationResult<T = any> extends UseQueryResult<T> {
366
- /** Current page (1-based) */
367
- page: number;
368
- /** Total number of pages */
369
- totalPages: number;
370
- /** Total number of records */
371
- totalCount: number;
372
- /** Go to next page */
373
- nextPage: () => void;
374
- /** Go to previous page */
375
- previousPage: () => void;
376
- /** Go to specific page */
377
- goToPage: (page: number) => void;
378
- /** Whether there is a next page */
379
- hasNextPage: boolean;
380
- /** Whether there is a previous page */
381
- hasPreviousPage: boolean;
382
- }
383
-
384
- /**
385
- * Hook for paginated data queries
386
- *
387
- * @example
388
- * ```tsx
389
- * function PaginatedTaskList() {
390
- * const {
391
- * data,
392
- * isLoading,
393
- * page,
394
- * totalPages,
395
- * nextPage,
396
- * previousPage,
397
- * hasNextPage,
398
- * hasPreviousPage
399
- * } = usePagination('todo_task', {
400
- * pageSize: 10,
401
- * orderBy: ['-created_at']
402
- * });
403
- *
404
- * return (
405
- * <div>
406
- * {data?.value.map(task => <div key={task.id}>{task.subject}</div>)}
407
- * <button onClick={previousPage} disabled={!hasPreviousPage}>Previous</button>
408
- * <span>Page {page} of {totalPages}</span>
409
- * <button onClick={nextPage} disabled={!hasNextPage}>Next</button>
410
- * </div>
411
- * );
412
- * }
413
- * ```
414
- */
415
- export function usePagination<T = any>(
416
- object: string,
417
- options: UsePaginationOptions<T> = {}
418
- ): UsePaginationResult<T> {
419
- const { pageSize = 20, initialPage = 1, ...queryOptions } = options;
420
- const [page, setPage] = useState(initialPage);
421
-
422
- const queryResult = useQuery<T>(object, {
423
- ...queryOptions,
424
- limit: pageSize,
425
- offset: (page - 1) * pageSize
426
- });
427
-
428
- const totalCount = queryResult.data?.total || 0;
429
- const totalPages = Math.ceil(totalCount / pageSize);
430
- const hasNextPage = page < totalPages;
431
- const hasPreviousPage = page > 1;
432
-
433
- const nextPage = useCallback(() => {
434
- if (hasNextPage) {
435
- setPage(p => p + 1);
436
- }
437
- }, [hasNextPage]);
438
-
439
- const previousPage = useCallback(() => {
440
- if (hasPreviousPage) {
441
- setPage(p => p - 1);
442
- }
443
- }, [hasPreviousPage]);
444
-
445
- const goToPage = useCallback((newPage: number) => {
446
- const clampedPage = Math.max(1, Math.min(newPage, totalPages));
447
- setPage(clampedPage);
448
- }, [totalPages]);
449
-
450
- return {
451
- ...queryResult,
452
- page,
453
- totalPages,
454
- totalCount,
455
- nextPage,
456
- previousPage,
457
- goToPage,
458
- hasNextPage,
459
- hasPreviousPage
460
- };
461
- }
462
-
463
- /**
464
- * Infinite query options for useInfiniteQuery hook
465
- */
466
- export interface UseInfiniteQueryOptions<T = any> extends Omit<UseQueryOptions<T>, 'skip' | 'offset'> {
467
- /** Page size for each fetch */
468
- pageSize?: number;
469
- /** Get next page parameter */
470
- getNextPageParam?: (lastPage: PaginatedResult<T>, allPages: PaginatedResult<T>[]) => number | undefined;
471
- }
472
-
473
- /**
474
- * Infinite query result for useInfiniteQuery hook
475
- */
476
- export interface UseInfiniteQueryResult<T = any> {
477
- /** All pages of data */
478
- data: PaginatedResult<T>[];
479
- /** Flattened data from all pages */
480
- flatData: T[];
481
- /** Loading state */
482
- isLoading: boolean;
483
- /** Error state */
484
- error: Error | null;
485
- /** Load the next page */
486
- fetchNextPage: () => Promise<void>;
487
- /** Whether there are more pages */
488
- hasNextPage: boolean;
489
- /** Is currently fetching next page */
490
- isFetchingNextPage: boolean;
491
- /** Refetch all pages */
492
- refetch: () => Promise<void>;
493
- }
494
-
495
- /**
496
- * Hook for infinite scrolling / load more functionality
497
- *
498
- * @example
499
- * ```tsx
500
- * function InfiniteTaskList() {
501
- * const {
502
- * flatData,
503
- * isLoading,
504
- * fetchNextPage,
505
- * hasNextPage,
506
- * isFetchingNextPage
507
- * } = useInfiniteQuery('todo_task', {
508
- * pageSize: 20,
509
- * orderBy: ['-created_at']
510
- * });
511
- *
512
- * return (
513
- * <div>
514
- * {flatData.map(task => <div key={task.id}>{task.subject}</div>)}
515
- * {hasNextPage && (
516
- * <button onClick={fetchNextPage} disabled={isFetchingNextPage}>
517
- * {isFetchingNextPage ? 'Loading...' : 'Load More'}
518
- * </button>
519
- * )}
520
- * </div>
521
- * );
522
- * }
523
- * ```
524
- */
525
- export function useInfiniteQuery<T = any>(
526
- object: string,
527
- options: UseInfiniteQueryOptions<T> = {}
528
- ): UseInfiniteQueryResult<T> {
529
- const client = useClient();
530
- const {
531
- pageSize = 20,
532
- // getNextPageParam is reserved for future use
533
- query,
534
- // Canonical names take precedence over legacy names
535
- where, fields, orderBy,
536
- // Legacy names (deprecated fallbacks)
537
- select, filters, sort,
538
- enabled = true,
539
- onSuccess,
540
- onError
541
- } = options;
542
-
543
- // Resolve canonical vs legacy: canonical wins
544
- const resolvedFields = fields ?? select;
545
- const resolvedWhere = where ?? filters;
546
- const resolvedSort = orderBy ?? sort;
547
-
548
- const [pages, setPages] = useState<PaginatedResult<T>[]>([]);
549
- const [isLoading, setIsLoading] = useState(true);
550
- const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
551
- const [error, setError] = useState<Error | null>(null);
552
- const [hasNextPage, setHasNextPage] = useState(true);
553
-
554
- const fetchPage = useCallback(async (skip: number, isNextPage = false) => {
555
- try {
556
- if (isNextPage) {
557
- setIsFetchingNextPage(true);
558
- } else {
559
- setIsLoading(true);
560
- }
561
- setError(null);
562
-
563
- let result: PaginatedResult<T>;
564
-
565
- if (query) {
566
- result = await client.data.query<T>(object, {
567
- ...query,
568
- limit: pageSize,
569
- offset: skip
570
- });
571
- } else {
572
- result = await client.data.find<T>(object, {
573
- where: resolvedWhere as any,
574
- fields: resolvedFields,
575
- orderBy: resolvedSort,
576
- limit: pageSize,
577
- offset: skip,
578
- });
579
- }
580
-
581
- if (isNextPage) {
582
- setPages(prev => [...prev, result]);
583
- } else {
584
- setPages([result]);
585
- }
586
-
587
- // Determine if there's a next page
588
- const fetchedCount = result.records?.length ?? 0;
589
- const hasMore = fetchedCount === pageSize;
590
- setHasNextPage(hasMore);
591
-
592
- onSuccess?.(result);
593
- } catch (err) {
594
- const error = err instanceof Error ? err : new Error('Query failed');
595
- setError(error);
596
- onError?.(error);
597
- } finally {
598
- setIsLoading(false);
599
- setIsFetchingNextPage(false);
600
- }
601
- }, [client, object, query, resolvedFields, resolvedWhere, resolvedSort, pageSize, onSuccess, onError]);
602
-
603
- // Initial fetch
604
- useEffect(() => {
605
- if (enabled) {
606
- fetchPage(0);
607
- }
608
- }, [enabled, fetchPage]);
609
-
610
- const fetchNextPage = useCallback(async () => {
611
- if (!hasNextPage || isFetchingNextPage) return;
612
-
613
- const nextSkip = pages.length * pageSize;
614
- await fetchPage(nextSkip, true);
615
- }, [hasNextPage, isFetchingNextPage, pages.length, pageSize, fetchPage]);
616
-
617
- const refetch = useCallback(async () => {
618
- setPages([]);
619
- await fetchPage(0);
620
- }, [fetchPage]);
621
-
622
- const flatData = pages.flatMap(page => page.records ?? []);
623
-
624
- return {
625
- data: pages,
626
- flatData,
627
- isLoading,
628
- error,
629
- fetchNextPage,
630
- hasNextPage,
631
- isFetchingNextPage,
632
- refetch
633
- };
634
- }
package/src/index.tsx DELETED
@@ -1,59 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- /**
4
- * @objectstack/client-react
5
- *
6
- * React hooks for ObjectStack Client SDK
7
- *
8
- * Provides type-safe React hooks for:
9
- * - Data queries (useQuery, useMutation, usePagination, useInfiniteQuery)
10
- * - Metadata access (useObject, useView, useFields, useMetadata)
11
- * - Client context (ObjectStackProvider, useClient)
12
- */
13
-
14
- // Context & Provider
15
- export {
16
- ObjectStackProvider,
17
- ObjectStackContext,
18
- useClient,
19
- type ObjectStackProviderProps
20
- } from './context';
21
-
22
- // Data Hooks
23
- export {
24
- useQuery,
25
- useMutation,
26
- usePagination,
27
- useInfiniteQuery,
28
- type UseQueryOptions,
29
- type UseQueryResult,
30
- type UseMutationOptions,
31
- type UseMutationResult,
32
- type UsePaginationOptions,
33
- type UsePaginationResult,
34
- type UseInfiniteQueryOptions,
35
- type UseInfiniteQueryResult
36
- } from './data-hooks';
37
-
38
- // Metadata Hooks
39
- export {
40
- useObject,
41
- useView,
42
- useFields,
43
- useMetadata,
44
- type UseMetadataOptions,
45
- type UseMetadataResult
46
- } from './metadata-hooks';
47
-
48
- // Realtime Event Hooks
49
- export {
50
- useMetadataSubscription,
51
- useDataSubscription,
52
- useMetadataSubscriptionCallback,
53
- useDataSubscriptionCallback,
54
- useRealtimeConnection,
55
- useAutoRefresh
56
- } from './realtime-hooks';
57
-
58
- // Re-export ObjectStackClient and types from @objectstack/client
59
- export { ObjectStackClient, type ClientConfig } from '@objectstack/client';