@objectstack/client-react 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +202 -0
- package/README.md +273 -0
- package/dist/context.d.ts +45 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +86 -0
- package/dist/data-hooks.d.ts +246 -0
- package/dist/data-hooks.d.ts.map +1 -0
- package/dist/data-hooks.js +398 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/metadata-hooks.d.ts +121 -0
- package/dist/metadata-hooks.d.ts.map +1 -0
- package/dist/metadata-hooks.js +243 -0
- package/package.json +22 -0
- package/src/context.tsx +66 -0
- package/src/data-hooks.tsx +593 -0
- package/src/index.tsx +47 -0
- package/src/metadata-hooks.tsx +312 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Query Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for querying and mutating ObjectStack data
|
|
5
|
+
*/
|
|
6
|
+
import { QueryAST, FilterCondition } from '@objectstack/spec/data';
|
|
7
|
+
import { PaginatedResult } from '@objectstack/client';
|
|
8
|
+
/**
|
|
9
|
+
* Query options for useQuery hook
|
|
10
|
+
*/
|
|
11
|
+
export interface UseQueryOptions<T = any> {
|
|
12
|
+
/** Query AST or simplified query options */
|
|
13
|
+
query?: Partial<QueryAST>;
|
|
14
|
+
/** Simple field selection */
|
|
15
|
+
select?: string[];
|
|
16
|
+
/** Simple filters */
|
|
17
|
+
filters?: FilterCondition;
|
|
18
|
+
/** Sort configuration */
|
|
19
|
+
sort?: string | string[];
|
|
20
|
+
/** Limit results */
|
|
21
|
+
top?: number;
|
|
22
|
+
/** Skip results (for pagination) */
|
|
23
|
+
skip?: number;
|
|
24
|
+
/** Enable/disable automatic query execution */
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
/** Refetch interval in milliseconds */
|
|
27
|
+
refetchInterval?: number;
|
|
28
|
+
/** Callback on successful query */
|
|
29
|
+
onSuccess?: (data: PaginatedResult<T>) => void;
|
|
30
|
+
/** Callback on error */
|
|
31
|
+
onError?: (error: Error) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Query result for useQuery hook
|
|
35
|
+
*/
|
|
36
|
+
export interface UseQueryResult<T = any> {
|
|
37
|
+
/** Query result data */
|
|
38
|
+
data: PaginatedResult<T> | null;
|
|
39
|
+
/** Loading state */
|
|
40
|
+
isLoading: boolean;
|
|
41
|
+
/** Error state */
|
|
42
|
+
error: Error | null;
|
|
43
|
+
/** Refetch the query */
|
|
44
|
+
refetch: () => Promise<void>;
|
|
45
|
+
/** Is currently refetching */
|
|
46
|
+
isRefetching: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Hook for querying ObjectStack data with automatic caching and refetching
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* function TaskList() {
|
|
54
|
+
* const { data, isLoading, error, refetch } = useQuery('todo_task', {
|
|
55
|
+
* select: ['id', 'subject', 'priority'],
|
|
56
|
+
* sort: ['-created_at'],
|
|
57
|
+
* top: 20
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
61
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
62
|
+
*
|
|
63
|
+
* return (
|
|
64
|
+
* <div>
|
|
65
|
+
* {data?.value.map(task => (
|
|
66
|
+
* <div key={task.id}>{task.subject}</div>
|
|
67
|
+
* ))}
|
|
68
|
+
* </div>
|
|
69
|
+
* );
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare function useQuery<T = any>(object: string, options?: UseQueryOptions<T>): UseQueryResult<T>;
|
|
74
|
+
/**
|
|
75
|
+
* Mutation options for useMutation hook
|
|
76
|
+
*/
|
|
77
|
+
export interface UseMutationOptions<TData = any, TVariables = any> {
|
|
78
|
+
/** Callback on successful mutation */
|
|
79
|
+
onSuccess?: (data: TData, variables: TVariables) => void;
|
|
80
|
+
/** Callback on error */
|
|
81
|
+
onError?: (error: Error, variables: TVariables) => void;
|
|
82
|
+
/** Callback when mutation is settled (success or error) */
|
|
83
|
+
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Mutation result for useMutation hook
|
|
87
|
+
*/
|
|
88
|
+
export interface UseMutationResult<TData = any, TVariables = any> {
|
|
89
|
+
/** Execute the mutation */
|
|
90
|
+
mutate: (variables: TVariables) => Promise<TData>;
|
|
91
|
+
/** Async version of mutate that throws errors */
|
|
92
|
+
mutateAsync: (variables: TVariables) => Promise<TData>;
|
|
93
|
+
/** Mutation result data */
|
|
94
|
+
data: TData | null;
|
|
95
|
+
/** Loading state */
|
|
96
|
+
isLoading: boolean;
|
|
97
|
+
/** Error state */
|
|
98
|
+
error: Error | null;
|
|
99
|
+
/** Reset mutation state */
|
|
100
|
+
reset: () => void;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Hook for creating, updating, or deleting ObjectStack data
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* function CreateTaskForm() {
|
|
108
|
+
* const { mutate, isLoading, error } = useMutation('todo_task', 'create', {
|
|
109
|
+
* onSuccess: (data) => {
|
|
110
|
+
* console.log('Task created:', data);
|
|
111
|
+
* }
|
|
112
|
+
* });
|
|
113
|
+
*
|
|
114
|
+
* const handleSubmit = (formData) => {
|
|
115
|
+
* mutate(formData);
|
|
116
|
+
* };
|
|
117
|
+
*
|
|
118
|
+
* return <form onSubmit={handleSubmit}>...</form>;
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function useMutation<TData = any, TVariables = any>(object: string, operation: 'create' | 'update' | 'delete' | 'createMany' | 'updateMany' | 'deleteMany', options?: UseMutationOptions<TData, TVariables>): UseMutationResult<TData, TVariables>;
|
|
123
|
+
/**
|
|
124
|
+
* Pagination options for usePagination hook
|
|
125
|
+
*/
|
|
126
|
+
export interface UsePaginationOptions<T = any> extends Omit<UseQueryOptions<T>, 'top' | 'skip'> {
|
|
127
|
+
/** Page size */
|
|
128
|
+
pageSize?: number;
|
|
129
|
+
/** Initial page (1-based) */
|
|
130
|
+
initialPage?: number;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Pagination result for usePagination hook
|
|
134
|
+
*/
|
|
135
|
+
export interface UsePaginationResult<T = any> extends UseQueryResult<T> {
|
|
136
|
+
/** Current page (1-based) */
|
|
137
|
+
page: number;
|
|
138
|
+
/** Total number of pages */
|
|
139
|
+
totalPages: number;
|
|
140
|
+
/** Total number of records */
|
|
141
|
+
totalCount: number;
|
|
142
|
+
/** Go to next page */
|
|
143
|
+
nextPage: () => void;
|
|
144
|
+
/** Go to previous page */
|
|
145
|
+
previousPage: () => void;
|
|
146
|
+
/** Go to specific page */
|
|
147
|
+
goToPage: (page: number) => void;
|
|
148
|
+
/** Whether there is a next page */
|
|
149
|
+
hasNextPage: boolean;
|
|
150
|
+
/** Whether there is a previous page */
|
|
151
|
+
hasPreviousPage: boolean;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Hook for paginated data queries
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* function PaginatedTaskList() {
|
|
159
|
+
* const {
|
|
160
|
+
* data,
|
|
161
|
+
* isLoading,
|
|
162
|
+
* page,
|
|
163
|
+
* totalPages,
|
|
164
|
+
* nextPage,
|
|
165
|
+
* previousPage,
|
|
166
|
+
* hasNextPage,
|
|
167
|
+
* hasPreviousPage
|
|
168
|
+
* } = usePagination('todo_task', {
|
|
169
|
+
* pageSize: 10,
|
|
170
|
+
* sort: ['-created_at']
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* return (
|
|
174
|
+
* <div>
|
|
175
|
+
* {data?.value.map(task => <div key={task.id}>{task.subject}</div>)}
|
|
176
|
+
* <button onClick={previousPage} disabled={!hasPreviousPage}>Previous</button>
|
|
177
|
+
* <span>Page {page} of {totalPages}</span>
|
|
178
|
+
* <button onClick={nextPage} disabled={!hasNextPage}>Next</button>
|
|
179
|
+
* </div>
|
|
180
|
+
* );
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export declare function usePagination<T = any>(object: string, options?: UsePaginationOptions<T>): UsePaginationResult<T>;
|
|
185
|
+
/**
|
|
186
|
+
* Infinite query options for useInfiniteQuery hook
|
|
187
|
+
*/
|
|
188
|
+
export interface UseInfiniteQueryOptions<T = any> extends Omit<UseQueryOptions<T>, 'skip'> {
|
|
189
|
+
/** Page size for each fetch */
|
|
190
|
+
pageSize?: number;
|
|
191
|
+
/** Get next page parameter */
|
|
192
|
+
getNextPageParam?: (lastPage: PaginatedResult<T>, allPages: PaginatedResult<T>[]) => number | undefined;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Infinite query result for useInfiniteQuery hook
|
|
196
|
+
*/
|
|
197
|
+
export interface UseInfiniteQueryResult<T = any> {
|
|
198
|
+
/** All pages of data */
|
|
199
|
+
data: PaginatedResult<T>[];
|
|
200
|
+
/** Flattened data from all pages */
|
|
201
|
+
flatData: T[];
|
|
202
|
+
/** Loading state */
|
|
203
|
+
isLoading: boolean;
|
|
204
|
+
/** Error state */
|
|
205
|
+
error: Error | null;
|
|
206
|
+
/** Load the next page */
|
|
207
|
+
fetchNextPage: () => Promise<void>;
|
|
208
|
+
/** Whether there are more pages */
|
|
209
|
+
hasNextPage: boolean;
|
|
210
|
+
/** Is currently fetching next page */
|
|
211
|
+
isFetchingNextPage: boolean;
|
|
212
|
+
/** Refetch all pages */
|
|
213
|
+
refetch: () => Promise<void>;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Hook for infinite scrolling / load more functionality
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```tsx
|
|
220
|
+
* function InfiniteTaskList() {
|
|
221
|
+
* const {
|
|
222
|
+
* flatData,
|
|
223
|
+
* isLoading,
|
|
224
|
+
* fetchNextPage,
|
|
225
|
+
* hasNextPage,
|
|
226
|
+
* isFetchingNextPage
|
|
227
|
+
* } = useInfiniteQuery('todo_task', {
|
|
228
|
+
* pageSize: 20,
|
|
229
|
+
* sort: ['-created_at']
|
|
230
|
+
* });
|
|
231
|
+
*
|
|
232
|
+
* return (
|
|
233
|
+
* <div>
|
|
234
|
+
* {flatData.map(task => <div key={task.id}>{task.subject}</div>)}
|
|
235
|
+
* {hasNextPage && (
|
|
236
|
+
* <button onClick={fetchNextPage} disabled={isFetchingNextPage}>
|
|
237
|
+
* {isFetchingNextPage ? 'Loading...' : 'Load More'}
|
|
238
|
+
* </button>
|
|
239
|
+
* )}
|
|
240
|
+
* </div>
|
|
241
|
+
* );
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export declare function useInfiniteQuery<T = any>(object: string, options?: UseInfiniteQueryOptions<T>): UseInfiniteQueryResult<T>;
|
|
246
|
+
//# sourceMappingURL=data-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-hooks.d.ts","sourceRoot":"","sources":["../src/data-hooks.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,GAAG;IACtC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,qBAAqB;IACrB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mCAAmC;IACnC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC/C,wBAAwB;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,GAAG;IACrC,wBAAwB;IACxB,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChC,oBAAoB;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,wBAAwB;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,8BAA8B;IAC9B,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,GAAG,GAAG,EAC9B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GAC/B,cAAc,CAAC,CAAC,CAAC,CA4FnB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,KAAK,GAAG,GAAG,EAAE,UAAU,GAAG,GAAG;IAC/D,sCAAsC;IACtC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,KAAK,IAAI,CAAC;IACzD,wBAAwB;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,KAAK,IAAI,CAAC;IACxD,2DAA2D;IAC3D,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,SAAS,EAAE,UAAU,KAAK,IAAI,CAAC;CAC3F;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,KAAK,GAAG,GAAG,EAAE,UAAU,GAAG,GAAG;IAC9D,2BAA2B;IAC3B,MAAM,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAClD,iDAAiD;IACjD,WAAW,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IACvD,2BAA2B;IAC3B,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IACnB,oBAAoB;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,KAAK,GAAG,GAAG,EAAE,UAAU,GAAG,GAAG,EACvD,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,YAAY,GAAG,YAAY,EACtF,OAAO,GAAE,kBAAkB,CAAC,KAAK,EAAE,UAAU,CAAM,GAClD,iBAAiB,CAAC,KAAK,EAAE,UAAU,CAAC,CAqFtC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC7F,gBAAgB;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;IACrE,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,0BAA0B;IAC1B,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,0BAA0B;IAC1B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,mCAAmC;IACnC,WAAW,EAAE,OAAO,CAAC;IACrB,uCAAuC;IACvC,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAAC,CAAC,GAAG,GAAG,EACnC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,oBAAoB,CAAC,CAAC,CAAM,GACpC,mBAAmB,CAAC,CAAC,CAAC,CA2CxB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACxF,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;CACzG;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC,GAAG,GAAG;IAC7C,wBAAwB;IACxB,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3B,oCAAoC;IACpC,QAAQ,EAAE,CAAC,EAAE,CAAC;IACd,oBAAoB;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,yBAAyB;IACzB,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,mCAAmC;IACnC,WAAW,EAAE,OAAO,CAAC;IACrB,sCAAsC;IACtC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,wBAAwB;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,GAAG,EACtC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,uBAAuB,CAAC,CAAC,CAAM,GACvC,sBAAsB,CAAC,CAAC,CAAC,CAoG3B"}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Data Query Hooks
|
|
4
|
+
*
|
|
5
|
+
* React hooks for querying and mutating ObjectStack data
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.useQuery = useQuery;
|
|
9
|
+
exports.useMutation = useMutation;
|
|
10
|
+
exports.usePagination = usePagination;
|
|
11
|
+
exports.useInfiniteQuery = useInfiniteQuery;
|
|
12
|
+
const react_1 = require("react");
|
|
13
|
+
const context_1 = require("./context");
|
|
14
|
+
/**
|
|
15
|
+
* Hook for querying ObjectStack data with automatic caching and refetching
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* function TaskList() {
|
|
20
|
+
* const { data, isLoading, error, refetch } = useQuery('todo_task', {
|
|
21
|
+
* select: ['id', 'subject', 'priority'],
|
|
22
|
+
* sort: ['-created_at'],
|
|
23
|
+
* top: 20
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
27
|
+
* if (error) return <div>Error: {error.message}</div>;
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <div>
|
|
31
|
+
* {data?.value.map(task => (
|
|
32
|
+
* <div key={task.id}>{task.subject}</div>
|
|
33
|
+
* ))}
|
|
34
|
+
* </div>
|
|
35
|
+
* );
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function useQuery(object, options = {}) {
|
|
40
|
+
const client = (0, context_1.useClient)();
|
|
41
|
+
const [data, setData] = (0, react_1.useState)(null);
|
|
42
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
|
43
|
+
const [isRefetching, setIsRefetching] = (0, react_1.useState)(false);
|
|
44
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
45
|
+
const intervalRef = (0, react_1.useRef)();
|
|
46
|
+
const { query, select, filters, sort, top, skip, enabled = true, refetchInterval, onSuccess, onError } = options;
|
|
47
|
+
const fetchData = (0, react_1.useCallback)(async (isRefetch = false) => {
|
|
48
|
+
if (!enabled)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
if (isRefetch) {
|
|
52
|
+
setIsRefetching(true);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
}
|
|
57
|
+
setError(null);
|
|
58
|
+
let result;
|
|
59
|
+
if (query) {
|
|
60
|
+
// Use advanced query API
|
|
61
|
+
result = await client.data.query(object, query);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Use simplified find API
|
|
65
|
+
result = await client.data.find(object, {
|
|
66
|
+
select,
|
|
67
|
+
filters: filters,
|
|
68
|
+
sort,
|
|
69
|
+
top,
|
|
70
|
+
skip
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
setData(result);
|
|
74
|
+
onSuccess?.(result);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const error = err instanceof Error ? err : new Error('Query failed');
|
|
78
|
+
setError(error);
|
|
79
|
+
onError?.(error);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
setIsLoading(false);
|
|
83
|
+
setIsRefetching(false);
|
|
84
|
+
}
|
|
85
|
+
}, [client, object, query, select, filters, sort, top, skip, enabled, onSuccess, onError]);
|
|
86
|
+
// Initial fetch and dependency-based refetch
|
|
87
|
+
(0, react_1.useEffect)(() => {
|
|
88
|
+
fetchData();
|
|
89
|
+
}, [fetchData]);
|
|
90
|
+
// Setup refetch interval
|
|
91
|
+
(0, react_1.useEffect)(() => {
|
|
92
|
+
if (refetchInterval && enabled) {
|
|
93
|
+
intervalRef.current = setInterval(() => {
|
|
94
|
+
fetchData(true);
|
|
95
|
+
}, refetchInterval);
|
|
96
|
+
return () => {
|
|
97
|
+
if (intervalRef.current) {
|
|
98
|
+
clearInterval(intervalRef.current);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}, [refetchInterval, enabled, fetchData]);
|
|
104
|
+
const refetch = (0, react_1.useCallback)(async () => {
|
|
105
|
+
await fetchData(true);
|
|
106
|
+
}, [fetchData]);
|
|
107
|
+
return {
|
|
108
|
+
data,
|
|
109
|
+
isLoading,
|
|
110
|
+
error,
|
|
111
|
+
refetch,
|
|
112
|
+
isRefetching
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Hook for creating, updating, or deleting ObjectStack data
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* function CreateTaskForm() {
|
|
121
|
+
* const { mutate, isLoading, error } = useMutation('todo_task', 'create', {
|
|
122
|
+
* onSuccess: (data) => {
|
|
123
|
+
* console.log('Task created:', data);
|
|
124
|
+
* }
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* const handleSubmit = (formData) => {
|
|
128
|
+
* mutate(formData);
|
|
129
|
+
* };
|
|
130
|
+
*
|
|
131
|
+
* return <form onSubmit={handleSubmit}>...</form>;
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
function useMutation(object, operation, options = {}) {
|
|
136
|
+
const client = (0, context_1.useClient)();
|
|
137
|
+
const [data, setData] = (0, react_1.useState)(null);
|
|
138
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
139
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
140
|
+
const { onSuccess, onError, onSettled } = options;
|
|
141
|
+
const mutateAsync = (0, react_1.useCallback)(async (variables) => {
|
|
142
|
+
setIsLoading(true);
|
|
143
|
+
setError(null);
|
|
144
|
+
try {
|
|
145
|
+
let result;
|
|
146
|
+
switch (operation) {
|
|
147
|
+
case 'create':
|
|
148
|
+
result = await client.data.create(object, variables);
|
|
149
|
+
break;
|
|
150
|
+
case 'update':
|
|
151
|
+
// Expect variables to be { id: string, data: Partial<T> }
|
|
152
|
+
const updateVars = variables;
|
|
153
|
+
result = await client.data.update(object, updateVars.id, updateVars.data);
|
|
154
|
+
break;
|
|
155
|
+
case 'delete':
|
|
156
|
+
// Expect variables to be { id: string }
|
|
157
|
+
const deleteVars = variables;
|
|
158
|
+
result = await client.data.delete(object, deleteVars.id);
|
|
159
|
+
break;
|
|
160
|
+
case 'createMany':
|
|
161
|
+
// createMany returns an array, which may not match TData type
|
|
162
|
+
result = await client.data.createMany(object, variables);
|
|
163
|
+
break;
|
|
164
|
+
case 'updateMany':
|
|
165
|
+
// Expect variables to be { records: Array<{ id: string, data: Partial<T> }> }
|
|
166
|
+
const updateManyVars = variables;
|
|
167
|
+
result = await client.data.updateMany(object, updateManyVars.records, updateManyVars.options);
|
|
168
|
+
break;
|
|
169
|
+
case 'deleteMany':
|
|
170
|
+
// Expect variables to be { ids: string[] }
|
|
171
|
+
const deleteManyVars = variables;
|
|
172
|
+
result = await client.data.deleteMany(object, deleteManyVars.ids, deleteManyVars.options);
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
176
|
+
}
|
|
177
|
+
setData(result);
|
|
178
|
+
onSuccess?.(result, variables);
|
|
179
|
+
onSettled?.(result, null, variables);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const error = err instanceof Error ? err : new Error('Mutation failed');
|
|
184
|
+
setError(error);
|
|
185
|
+
onError?.(error, variables);
|
|
186
|
+
onSettled?.(undefined, error, variables);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
setIsLoading(false);
|
|
191
|
+
}
|
|
192
|
+
}, [client, object, operation, onSuccess, onError, onSettled]);
|
|
193
|
+
const mutate = (0, react_1.useCallback)((variables) => {
|
|
194
|
+
return mutateAsync(variables).catch(() => {
|
|
195
|
+
// Swallow error for non-async version
|
|
196
|
+
// Error is still available in the error state
|
|
197
|
+
return null;
|
|
198
|
+
});
|
|
199
|
+
}, [mutateAsync]);
|
|
200
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
201
|
+
setData(null);
|
|
202
|
+
setError(null);
|
|
203
|
+
setIsLoading(false);
|
|
204
|
+
}, []);
|
|
205
|
+
return {
|
|
206
|
+
mutate,
|
|
207
|
+
mutateAsync,
|
|
208
|
+
data,
|
|
209
|
+
isLoading,
|
|
210
|
+
error,
|
|
211
|
+
reset
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Hook for paginated data queries
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```tsx
|
|
219
|
+
* function PaginatedTaskList() {
|
|
220
|
+
* const {
|
|
221
|
+
* data,
|
|
222
|
+
* isLoading,
|
|
223
|
+
* page,
|
|
224
|
+
* totalPages,
|
|
225
|
+
* nextPage,
|
|
226
|
+
* previousPage,
|
|
227
|
+
* hasNextPage,
|
|
228
|
+
* hasPreviousPage
|
|
229
|
+
* } = usePagination('todo_task', {
|
|
230
|
+
* pageSize: 10,
|
|
231
|
+
* sort: ['-created_at']
|
|
232
|
+
* });
|
|
233
|
+
*
|
|
234
|
+
* return (
|
|
235
|
+
* <div>
|
|
236
|
+
* {data?.value.map(task => <div key={task.id}>{task.subject}</div>)}
|
|
237
|
+
* <button onClick={previousPage} disabled={!hasPreviousPage}>Previous</button>
|
|
238
|
+
* <span>Page {page} of {totalPages}</span>
|
|
239
|
+
* <button onClick={nextPage} disabled={!hasNextPage}>Next</button>
|
|
240
|
+
* </div>
|
|
241
|
+
* );
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
function usePagination(object, options = {}) {
|
|
246
|
+
const { pageSize = 20, initialPage = 1, ...queryOptions } = options;
|
|
247
|
+
const [page, setPage] = (0, react_1.useState)(initialPage);
|
|
248
|
+
const queryResult = useQuery(object, {
|
|
249
|
+
...queryOptions,
|
|
250
|
+
top: pageSize,
|
|
251
|
+
skip: (page - 1) * pageSize
|
|
252
|
+
});
|
|
253
|
+
const totalCount = queryResult.data?.count || 0;
|
|
254
|
+
const totalPages = Math.ceil(totalCount / pageSize);
|
|
255
|
+
const hasNextPage = page < totalPages;
|
|
256
|
+
const hasPreviousPage = page > 1;
|
|
257
|
+
const nextPage = (0, react_1.useCallback)(() => {
|
|
258
|
+
if (hasNextPage) {
|
|
259
|
+
setPage(p => p + 1);
|
|
260
|
+
}
|
|
261
|
+
}, [hasNextPage]);
|
|
262
|
+
const previousPage = (0, react_1.useCallback)(() => {
|
|
263
|
+
if (hasPreviousPage) {
|
|
264
|
+
setPage(p => p - 1);
|
|
265
|
+
}
|
|
266
|
+
}, [hasPreviousPage]);
|
|
267
|
+
const goToPage = (0, react_1.useCallback)((newPage) => {
|
|
268
|
+
const clampedPage = Math.max(1, Math.min(newPage, totalPages));
|
|
269
|
+
setPage(clampedPage);
|
|
270
|
+
}, [totalPages]);
|
|
271
|
+
return {
|
|
272
|
+
...queryResult,
|
|
273
|
+
page,
|
|
274
|
+
totalPages,
|
|
275
|
+
totalCount,
|
|
276
|
+
nextPage,
|
|
277
|
+
previousPage,
|
|
278
|
+
goToPage,
|
|
279
|
+
hasNextPage,
|
|
280
|
+
hasPreviousPage
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Hook for infinite scrolling / load more functionality
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```tsx
|
|
288
|
+
* function InfiniteTaskList() {
|
|
289
|
+
* const {
|
|
290
|
+
* flatData,
|
|
291
|
+
* isLoading,
|
|
292
|
+
* fetchNextPage,
|
|
293
|
+
* hasNextPage,
|
|
294
|
+
* isFetchingNextPage
|
|
295
|
+
* } = useInfiniteQuery('todo_task', {
|
|
296
|
+
* pageSize: 20,
|
|
297
|
+
* sort: ['-created_at']
|
|
298
|
+
* });
|
|
299
|
+
*
|
|
300
|
+
* return (
|
|
301
|
+
* <div>
|
|
302
|
+
* {flatData.map(task => <div key={task.id}>{task.subject}</div>)}
|
|
303
|
+
* {hasNextPage && (
|
|
304
|
+
* <button onClick={fetchNextPage} disabled={isFetchingNextPage}>
|
|
305
|
+
* {isFetchingNextPage ? 'Loading...' : 'Load More'}
|
|
306
|
+
* </button>
|
|
307
|
+
* )}
|
|
308
|
+
* </div>
|
|
309
|
+
* );
|
|
310
|
+
* }
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
function useInfiniteQuery(object, options = {}) {
|
|
314
|
+
const client = (0, context_1.useClient)();
|
|
315
|
+
const { pageSize = 20,
|
|
316
|
+
// getNextPageParam is reserved for future use
|
|
317
|
+
query, select, filters, sort, enabled = true, onSuccess, onError } = options;
|
|
318
|
+
const [pages, setPages] = (0, react_1.useState)([]);
|
|
319
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
|
320
|
+
const [isFetchingNextPage, setIsFetchingNextPage] = (0, react_1.useState)(false);
|
|
321
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
322
|
+
const [hasNextPage, setHasNextPage] = (0, react_1.useState)(true);
|
|
323
|
+
const fetchPage = (0, react_1.useCallback)(async (skip, isNextPage = false) => {
|
|
324
|
+
try {
|
|
325
|
+
if (isNextPage) {
|
|
326
|
+
setIsFetchingNextPage(true);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
setIsLoading(true);
|
|
330
|
+
}
|
|
331
|
+
setError(null);
|
|
332
|
+
let result;
|
|
333
|
+
if (query) {
|
|
334
|
+
result = await client.data.query(object, {
|
|
335
|
+
...query,
|
|
336
|
+
limit: pageSize,
|
|
337
|
+
offset: skip
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
result = await client.data.find(object, {
|
|
342
|
+
select,
|
|
343
|
+
filters: filters,
|
|
344
|
+
sort,
|
|
345
|
+
top: pageSize,
|
|
346
|
+
skip
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (isNextPage) {
|
|
350
|
+
setPages(prev => [...prev, result]);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
setPages([result]);
|
|
354
|
+
}
|
|
355
|
+
// Determine if there's a next page
|
|
356
|
+
const fetchedCount = result.value.length;
|
|
357
|
+
const hasMore = fetchedCount === pageSize;
|
|
358
|
+
setHasNextPage(hasMore);
|
|
359
|
+
onSuccess?.(result);
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
const error = err instanceof Error ? err : new Error('Query failed');
|
|
363
|
+
setError(error);
|
|
364
|
+
onError?.(error);
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
setIsLoading(false);
|
|
368
|
+
setIsFetchingNextPage(false);
|
|
369
|
+
}
|
|
370
|
+
}, [client, object, query, select, filters, sort, pageSize, onSuccess, onError]);
|
|
371
|
+
// Initial fetch
|
|
372
|
+
(0, react_1.useEffect)(() => {
|
|
373
|
+
if (enabled) {
|
|
374
|
+
fetchPage(0);
|
|
375
|
+
}
|
|
376
|
+
}, [enabled, fetchPage]);
|
|
377
|
+
const fetchNextPage = (0, react_1.useCallback)(async () => {
|
|
378
|
+
if (!hasNextPage || isFetchingNextPage)
|
|
379
|
+
return;
|
|
380
|
+
const nextSkip = pages.length * pageSize;
|
|
381
|
+
await fetchPage(nextSkip, true);
|
|
382
|
+
}, [hasNextPage, isFetchingNextPage, pages.length, pageSize, fetchPage]);
|
|
383
|
+
const refetch = (0, react_1.useCallback)(async () => {
|
|
384
|
+
setPages([]);
|
|
385
|
+
await fetchPage(0);
|
|
386
|
+
}, [fetchPage]);
|
|
387
|
+
const flatData = pages.flatMap(page => page.value);
|
|
388
|
+
return {
|
|
389
|
+
data: pages,
|
|
390
|
+
flatData,
|
|
391
|
+
isLoading,
|
|
392
|
+
error,
|
|
393
|
+
fetchNextPage,
|
|
394
|
+
hasNextPage,
|
|
395
|
+
isFetchingNextPage,
|
|
396
|
+
refetch
|
|
397
|
+
};
|
|
398
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @objectstack/client-react
|
|
3
|
+
*
|
|
4
|
+
* React hooks for ObjectStack Client SDK
|
|
5
|
+
*
|
|
6
|
+
* Provides type-safe React hooks for:
|
|
7
|
+
* - Data queries (useQuery, useMutation, usePagination, useInfiniteQuery)
|
|
8
|
+
* - Metadata access (useObject, useView, useFields, useMetadata)
|
|
9
|
+
* - Client context (ObjectStackProvider, useClient)
|
|
10
|
+
*/
|
|
11
|
+
export { ObjectStackProvider, ObjectStackContext, useClient, type ObjectStackProviderProps } from './context';
|
|
12
|
+
export { useQuery, useMutation, usePagination, useInfiniteQuery, type UseQueryOptions, type UseQueryResult, type UseMutationOptions, type UseMutationResult, type UsePaginationOptions, type UsePaginationResult, type UseInfiniteQueryOptions, type UseInfiniteQueryResult } from './data-hooks';
|
|
13
|
+
export { useObject, useView, useFields, useMetadata, type UseMetadataOptions, type UseMetadataResult } from './metadata-hooks';
|
|
14
|
+
export { ObjectStackClient, type ClientConfig } from '@objectstack/client';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,SAAS,EACT,KAAK,wBAAwB,EAC9B,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,SAAS,EACT,OAAO,EACP,SAAS,EACT,WAAW,EACX,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
|