@vaiftech/react 1.0.3 → 1.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.
- package/README.md +563 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,12 +32,23 @@ function App() {
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
## Hooks
|
|
35
|
+
## Hooks Overview
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
| Category | Hooks |
|
|
38
|
+
|----------|-------|
|
|
39
|
+
| **Auth** | `useAuth`, `useUser`, `useToken`, `usePasswordReset`, `useEmailVerification`, `useMagicLink`, `useOAuth`, `useMFA` |
|
|
40
|
+
| **Query** | `useQuery`, `useQueryById`, `useQueryFirst`, `usePaginatedQuery`, `useInfiniteQuery`, `useCount` |
|
|
41
|
+
| **Mutation** | `useMutation`, `useCreate`, `useUpdate`, `useDelete`, `useUpsert`, `useBatchCreate`, `useBatchUpdate`, `useBatchDelete`, `useOptimisticMutation` |
|
|
42
|
+
| **Realtime** | `useSubscription`, `useChannel`, `usePresence`, `useRealtimeConnection`, `useBroadcast` |
|
|
43
|
+
| **Storage** | `useUpload`, `useDownload`, `useFile`, `useFiles`, `useDropzone`, `usePublicUrl` |
|
|
44
|
+
| **Functions** | `useFunction`, `useRpc`, `useFunctionList`, `useBatchInvoke`, `useScheduledFunction` |
|
|
45
|
+
| **MongoDB** | `useMongoFind`, `useMongoFindOne`, `useMongoAggregate`, `useMongoInsertOne`, `useMongoInsertMany`, `useMongoUpdateOne`, `useMongoUpdateMany`, `useMongoDeleteOne`, `useMongoDeleteMany`, `useMongoInfiniteFind`, `useMongoCount`, `useMongoDistinct`, `useMongoCollection` |
|
|
46
|
+
| **Observability** | `useMetrics`, `useAuditLogs`, `useIncidents`, `useSystemHealth`, `useRealtimeStats`, `useErrorTracking` |
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
38
49
|
|
|
39
50
|
```tsx
|
|
40
|
-
import { useAuth, useUser, useSession } from '@vaiftech/react';
|
|
51
|
+
import { useAuth, useUser, useSession, useMFA, useOAuth } from '@vaiftech/react';
|
|
41
52
|
|
|
42
53
|
function AuthComponent() {
|
|
43
54
|
const { user, isLoading, signIn, signUp, signOut } = useAuth();
|
|
@@ -66,17 +77,79 @@ function Profile() {
|
|
|
66
77
|
return user ? <p>{user.email}</p> : null;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
//
|
|
70
|
-
function
|
|
71
|
-
const {
|
|
72
|
-
|
|
80
|
+
// OAuth login
|
|
81
|
+
function OAuthLogin() {
|
|
82
|
+
const { signInWithProvider, isLoading } = useOAuth();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
<button onClick={() => signInWithProvider('google')}>
|
|
87
|
+
Sign in with Google
|
|
88
|
+
</button>
|
|
89
|
+
<button onClick={() => signInWithProvider('github')}>
|
|
90
|
+
Sign in with GitHub
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MFA setup
|
|
97
|
+
function MFASetup() {
|
|
98
|
+
const { enroll, verify, isEnrolling, qrCode } = useMFA();
|
|
99
|
+
|
|
100
|
+
const handleSetup = async () => {
|
|
101
|
+
await enroll({ type: 'totp' });
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div>
|
|
106
|
+
{qrCode && <img src={qrCode} alt="Scan with authenticator" />}
|
|
107
|
+
<button onClick={handleSetup}>Enable MFA</button>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
73
110
|
}
|
|
74
111
|
```
|
|
75
112
|
|
|
76
|
-
###
|
|
113
|
+
### Standalone Auth Provider
|
|
114
|
+
|
|
115
|
+
For auth-only applications, use the standalone auth provider from `@vaiftech/auth`:
|
|
77
116
|
|
|
78
117
|
```tsx
|
|
79
|
-
import {
|
|
118
|
+
import { AuthProvider, useAuth, useSession } from '@vaiftech/react';
|
|
119
|
+
|
|
120
|
+
function App() {
|
|
121
|
+
return (
|
|
122
|
+
<AuthProvider config={{ url: 'https://api.vaif.studio', apiKey: 'your-key' }}>
|
|
123
|
+
<YourApp />
|
|
124
|
+
</AuthProvider>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function LoginPage() {
|
|
129
|
+
const { signInWithPassword, signInWithOAuth, isLoading } = useAuth();
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div>
|
|
133
|
+
<form onSubmit={(e) => {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
signInWithPassword({ email, password });
|
|
136
|
+
}}>
|
|
137
|
+
<input type="email" />
|
|
138
|
+
<input type="password" />
|
|
139
|
+
<button type="submit">Sign In</button>
|
|
140
|
+
</form>
|
|
141
|
+
<button onClick={() => signInWithOAuth({ provider: 'google' })}>
|
|
142
|
+
Google
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Data Fetching
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { useQuery, useQueryById, usePaginatedQuery, useInfiniteQuery } from '@vaiftech/react';
|
|
80
153
|
|
|
81
154
|
interface Post {
|
|
82
155
|
id: string;
|
|
@@ -84,6 +157,7 @@ interface Post {
|
|
|
84
157
|
content: string;
|
|
85
158
|
}
|
|
86
159
|
|
|
160
|
+
// Basic query
|
|
87
161
|
function PostList() {
|
|
88
162
|
const { data: posts, isLoading, error, refetch } = useQuery<Post>('posts', {
|
|
89
163
|
filters: [{ field: 'published', operator: 'eq', value: true }],
|
|
@@ -103,8 +177,67 @@ function PostList() {
|
|
|
103
177
|
);
|
|
104
178
|
}
|
|
105
179
|
|
|
180
|
+
// Query by ID
|
|
181
|
+
function PostDetail({ postId }: { postId: string }) {
|
|
182
|
+
const { data: post, isLoading } = useQueryById<Post>('posts', postId);
|
|
183
|
+
|
|
184
|
+
if (isLoading) return <div>Loading...</div>;
|
|
185
|
+
return <h1>{post?.title}</h1>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Paginated query
|
|
189
|
+
function PaginatedPosts() {
|
|
190
|
+
const {
|
|
191
|
+
data,
|
|
192
|
+
page,
|
|
193
|
+
totalPages,
|
|
194
|
+
nextPage,
|
|
195
|
+
prevPage,
|
|
196
|
+
isLoading
|
|
197
|
+
} = usePaginatedQuery<Post>('posts', {
|
|
198
|
+
pageSize: 20,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
{data?.map(post => <div key={post.id}>{post.title}</div>)}
|
|
204
|
+
<button onClick={prevPage} disabled={page === 1}>Prev</button>
|
|
205
|
+
<span>{page} / {totalPages}</span>
|
|
206
|
+
<button onClick={nextPage} disabled={page === totalPages}>Next</button>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Infinite scroll
|
|
212
|
+
function InfinitePosts() {
|
|
213
|
+
const {
|
|
214
|
+
data,
|
|
215
|
+
fetchNextPage,
|
|
216
|
+
hasNextPage,
|
|
217
|
+
isFetchingNextPage
|
|
218
|
+
} = useInfiniteQuery<Post>('posts', { limit: 20 });
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div>
|
|
222
|
+
{data?.map(post => <div key={post.id}>{post.title}</div>)}
|
|
223
|
+
{hasNextPage && (
|
|
224
|
+
<button onClick={fetchNextPage} disabled={isFetchingNextPage}>
|
|
225
|
+
{isFetchingNextPage ? 'Loading...' : 'Load More'}
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Mutations
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
import { useCreate, useUpdate, useDelete, useMutation, useOptimisticMutation } from '@vaiftech/react';
|
|
237
|
+
|
|
238
|
+
// Create
|
|
106
239
|
function CreatePost() {
|
|
107
|
-
const { create, isCreating } =
|
|
240
|
+
const { create, isCreating, error } = useCreate<Post>('posts');
|
|
108
241
|
|
|
109
242
|
const handleSubmit = async (data: Omit<Post, 'id'>) => {
|
|
110
243
|
const newPost = await create(data);
|
|
@@ -117,47 +250,131 @@ function CreatePost() {
|
|
|
117
250
|
</button>
|
|
118
251
|
);
|
|
119
252
|
}
|
|
253
|
+
|
|
254
|
+
// Update
|
|
255
|
+
function UpdatePost({ postId }: { postId: string }) {
|
|
256
|
+
const { update, isUpdating } = useUpdate<Post>('posts');
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<button
|
|
260
|
+
onClick={() => update(postId, { title: 'Updated Title' })}
|
|
261
|
+
disabled={isUpdating}
|
|
262
|
+
>
|
|
263
|
+
Update
|
|
264
|
+
</button>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Delete
|
|
269
|
+
function DeletePost({ postId }: { postId: string }) {
|
|
270
|
+
const { remove, isDeleting } = useDelete('posts');
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<button onClick={() => remove(postId)} disabled={isDeleting}>
|
|
274
|
+
Delete
|
|
275
|
+
</button>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Full mutation hook
|
|
280
|
+
function PostActions() {
|
|
281
|
+
const { create, update, remove, isLoading } = useMutation<Post>('posts');
|
|
282
|
+
|
|
283
|
+
// Use create, update, remove as needed
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Optimistic updates
|
|
287
|
+
function OptimisticLike({ postId }: { postId: string }) {
|
|
288
|
+
const { mutate } = useOptimisticMutation<Post>('posts', {
|
|
289
|
+
optimisticUpdate: (cache, postId) => {
|
|
290
|
+
return { ...cache[postId], likes: cache[postId].likes + 1 };
|
|
291
|
+
},
|
|
292
|
+
rollbackOnError: true,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return <button onClick={() => mutate(postId, { likes: '+1' })}>Like</button>;
|
|
296
|
+
}
|
|
120
297
|
```
|
|
121
298
|
|
|
122
|
-
|
|
299
|
+
## Realtime
|
|
123
300
|
|
|
124
301
|
```tsx
|
|
125
|
-
import {
|
|
302
|
+
import { useSubscription, useChannel, usePresence, useBroadcast } from '@vaiftech/react';
|
|
126
303
|
|
|
127
|
-
// Subscribe to changes
|
|
304
|
+
// Subscribe to table changes
|
|
128
305
|
function MessageListener() {
|
|
129
|
-
|
|
306
|
+
useSubscription<Message>('messages', {
|
|
130
307
|
event: 'INSERT',
|
|
131
|
-
onInsert: (message) =>
|
|
132
|
-
|
|
133
|
-
|
|
308
|
+
onInsert: (message) => console.log('New message:', message),
|
|
309
|
+
onUpdate: (message) => console.log('Updated:', message),
|
|
310
|
+
onDelete: (message) => console.log('Deleted:', message.id),
|
|
134
311
|
});
|
|
135
312
|
|
|
136
313
|
return null;
|
|
137
314
|
}
|
|
138
315
|
|
|
139
|
-
//
|
|
140
|
-
function
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
316
|
+
// Channel communication
|
|
317
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
318
|
+
const channel = useChannel(`room-${roomId}`);
|
|
319
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
320
|
+
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
channel.on('message', (msg) => {
|
|
323
|
+
setMessages(prev => [...prev, msg]);
|
|
324
|
+
});
|
|
325
|
+
return () => channel.off('message');
|
|
326
|
+
}, [channel]);
|
|
327
|
+
|
|
328
|
+
const sendMessage = (text: string) => {
|
|
329
|
+
channel.send('message', { text, timestamp: Date.now() });
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return <div>{/* Chat UI */}</div>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Presence tracking
|
|
336
|
+
function OnlineUsers({ roomId }: { roomId: string }) {
|
|
337
|
+
const { users, track, leave } = usePresence(`room-${roomId}`);
|
|
338
|
+
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
track({ status: 'online', name: currentUser.name });
|
|
341
|
+
return () => leave();
|
|
342
|
+
}, []);
|
|
145
343
|
|
|
146
344
|
return (
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
</ul>
|
|
345
|
+
<div>
|
|
346
|
+
<h4>Online ({users.length})</h4>
|
|
347
|
+
{users.map(u => <span key={u.id}>{u.name}</span>)}
|
|
348
|
+
</div>
|
|
152
349
|
);
|
|
153
350
|
}
|
|
351
|
+
|
|
352
|
+
// Broadcast events
|
|
353
|
+
function TypingIndicator({ roomId }: { roomId: string }) {
|
|
354
|
+
const { send, subscribe } = useBroadcast(`room-${roomId}`);
|
|
355
|
+
const [typing, setTyping] = useState<string[]>([]);
|
|
356
|
+
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
subscribe('typing', (event) => {
|
|
359
|
+
setTyping(prev => [...prev, event.userId]);
|
|
360
|
+
setTimeout(() => {
|
|
361
|
+
setTyping(prev => prev.filter(id => id !== event.userId));
|
|
362
|
+
}, 3000);
|
|
363
|
+
});
|
|
364
|
+
}, []);
|
|
365
|
+
|
|
366
|
+
const onType = () => send('typing', { userId: currentUser.id });
|
|
367
|
+
|
|
368
|
+
return <div>{typing.length > 0 && `${typing.join(', ')} typing...`}</div>;
|
|
369
|
+
}
|
|
154
370
|
```
|
|
155
371
|
|
|
156
|
-
|
|
372
|
+
## Storage
|
|
157
373
|
|
|
158
374
|
```tsx
|
|
159
|
-
import { useUpload, useDownload, useFile, usePublicUrl } from '@vaiftech/react';
|
|
375
|
+
import { useUpload, useDownload, useFile, useFiles, useDropzone, usePublicUrl } from '@vaiftech/react';
|
|
160
376
|
|
|
377
|
+
// File upload with progress
|
|
161
378
|
function FileUpload() {
|
|
162
379
|
const { upload, progress, isUploading, error } = useUpload();
|
|
163
380
|
|
|
@@ -175,17 +392,50 @@ function FileUpload() {
|
|
|
175
392
|
);
|
|
176
393
|
}
|
|
177
394
|
|
|
395
|
+
// Drag and drop zone
|
|
396
|
+
function DropzoneUpload() {
|
|
397
|
+
const { getRootProps, getInputProps, isDragActive, files } = useDropzone({
|
|
398
|
+
accept: { 'image/*': ['.png', '.jpg', '.gif'] },
|
|
399
|
+
maxFiles: 5,
|
|
400
|
+
onDrop: async (files) => {
|
|
401
|
+
// Handle uploaded files
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<div {...getRootProps()}>
|
|
407
|
+
<input {...getInputProps()} />
|
|
408
|
+
{isDragActive ? 'Drop files here' : 'Drag files or click to upload'}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Display files
|
|
178
414
|
function FileDisplay({ path }: { path: string }) {
|
|
179
415
|
const url = usePublicUrl(path);
|
|
180
416
|
return url ? <img src={url} alt="" /> : null;
|
|
181
417
|
}
|
|
418
|
+
|
|
419
|
+
// List files
|
|
420
|
+
function FileList({ prefix }: { prefix: string }) {
|
|
421
|
+
const { files, isLoading, refetch } = useFiles({ prefix });
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<ul>
|
|
425
|
+
{files?.map(file => (
|
|
426
|
+
<li key={file.path}>{file.name} ({file.size} bytes)</li>
|
|
427
|
+
))}
|
|
428
|
+
</ul>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
182
431
|
```
|
|
183
432
|
|
|
184
|
-
|
|
433
|
+
## Edge Functions
|
|
185
434
|
|
|
186
435
|
```tsx
|
|
187
|
-
import { useFunction } from '@vaiftech/react';
|
|
436
|
+
import { useFunction, useRpc, useBatchInvoke } from '@vaiftech/react';
|
|
188
437
|
|
|
438
|
+
// Invoke function
|
|
189
439
|
function SendEmail() {
|
|
190
440
|
const { invoke, isInvoking, error, result } = useFunction('send-email');
|
|
191
441
|
|
|
@@ -202,6 +452,283 @@ function SendEmail() {
|
|
|
202
452
|
</button>
|
|
203
453
|
);
|
|
204
454
|
}
|
|
455
|
+
|
|
456
|
+
// RPC-style calls
|
|
457
|
+
function DataProcessor() {
|
|
458
|
+
const { call, isLoading, data } = useRpc<ProcessResult>('process-data');
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
<button onClick={() => call({ input: rawData })}>
|
|
462
|
+
Process
|
|
463
|
+
</button>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Batch invocations
|
|
468
|
+
function BatchProcessor() {
|
|
469
|
+
const { invoke, isInvoking, results } = useBatchInvoke();
|
|
470
|
+
|
|
471
|
+
const processAll = async () => {
|
|
472
|
+
await invoke([
|
|
473
|
+
{ name: 'resize-image', payload: { url: 'img1.jpg' } },
|
|
474
|
+
{ name: 'resize-image', payload: { url: 'img2.jpg' } },
|
|
475
|
+
{ name: 'resize-image', payload: { url: 'img3.jpg' } },
|
|
476
|
+
]);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
return <button onClick={processAll}>Process All</button>;
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## MongoDB Hooks
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import {
|
|
487
|
+
useMongoFind,
|
|
488
|
+
useMongoFindOne,
|
|
489
|
+
useMongoInsertOne,
|
|
490
|
+
useMongoUpdateOne,
|
|
491
|
+
useMongoDeleteOne,
|
|
492
|
+
useMongoAggregate,
|
|
493
|
+
useMongoCollection,
|
|
494
|
+
useMongoInfiniteFind,
|
|
495
|
+
} from '@vaiftech/react';
|
|
496
|
+
|
|
497
|
+
// Find documents
|
|
498
|
+
function UserList() {
|
|
499
|
+
const { data: users, isLoading, error, refetch } = useMongoFind<User>('users', {
|
|
500
|
+
filter: { status: 'active' },
|
|
501
|
+
sort: { createdAt: -1 },
|
|
502
|
+
limit: 20,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
if (isLoading) return <div>Loading...</div>;
|
|
506
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<ul>
|
|
510
|
+
{users?.map(user => <li key={user._id}>{user.name}</li>)}
|
|
511
|
+
</ul>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Find single document
|
|
516
|
+
function UserProfile({ id }: { id: string }) {
|
|
517
|
+
const { data: user, isLoading } = useMongoFindOne<User>('users', { _id: id });
|
|
518
|
+
return user ? <div>{user.name}</div> : null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Insert document
|
|
522
|
+
function CreateUser() {
|
|
523
|
+
const { insertOne, isInserting } = useMongoInsertOne<User>('users');
|
|
524
|
+
|
|
525
|
+
const handleCreate = async () => {
|
|
526
|
+
const result = await insertOne({
|
|
527
|
+
name: 'New User',
|
|
528
|
+
email: 'new@example.com',
|
|
529
|
+
createdAt: new Date(),
|
|
530
|
+
});
|
|
531
|
+
console.log('Created:', result.insertedId);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<button onClick={handleCreate} disabled={isInserting}>
|
|
536
|
+
Create User
|
|
537
|
+
</button>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Update document
|
|
542
|
+
function UpdateUser({ id }: { id: string }) {
|
|
543
|
+
const { updateOne, isUpdating } = useMongoUpdateOne('users');
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<button
|
|
547
|
+
onClick={() => updateOne({ _id: id }, { $set: { updatedAt: new Date() } })}
|
|
548
|
+
disabled={isUpdating}
|
|
549
|
+
>
|
|
550
|
+
Update
|
|
551
|
+
</button>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Delete document
|
|
556
|
+
function DeleteUser({ id }: { id: string }) {
|
|
557
|
+
const { deleteOne, isDeleting } = useMongoDeleteOne('users');
|
|
558
|
+
return (
|
|
559
|
+
<button onClick={() => deleteOne({ _id: id })} disabled={isDeleting}>
|
|
560
|
+
Delete
|
|
561
|
+
</button>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Aggregation pipeline
|
|
566
|
+
function UserStats() {
|
|
567
|
+
const { data: stats } = useMongoAggregate<{ _id: string; count: number }>('users', [
|
|
568
|
+
{ $match: { status: 'active' } },
|
|
569
|
+
{ $group: { _id: '$country', count: { $sum: 1 } } },
|
|
570
|
+
{ $sort: { count: -1 } },
|
|
571
|
+
]);
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<ul>
|
|
575
|
+
{stats?.map(stat => (
|
|
576
|
+
<li key={stat._id}>{stat._id}: {stat.count}</li>
|
|
577
|
+
))}
|
|
578
|
+
</ul>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Infinite scroll with MongoDB
|
|
583
|
+
function InfiniteUserList() {
|
|
584
|
+
const { data, fetchNextPage, hasNextPage } = useMongoInfiniteFind<User>('users', {
|
|
585
|
+
filter: { status: 'active' },
|
|
586
|
+
sort: { createdAt: -1 },
|
|
587
|
+
limit: 20,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
<div>
|
|
592
|
+
{data?.map(user => <div key={user._id}>{user.name}</div>)}
|
|
593
|
+
{hasNextPage && <button onClick={fetchNextPage}>Load More</button>}
|
|
594
|
+
</div>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Full collection access
|
|
599
|
+
function AdvancedOps() {
|
|
600
|
+
const collection = useMongoCollection<User>('users');
|
|
601
|
+
|
|
602
|
+
const handleBulk = async () => {
|
|
603
|
+
const count = await collection.count({ status: 'active' });
|
|
604
|
+
const countries = await collection.distinct('country');
|
|
605
|
+
await collection.bulkWrite([
|
|
606
|
+
{ insertOne: { document: { name: 'Bulk User' } } },
|
|
607
|
+
]);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
return <button onClick={handleBulk}>Run Operations</button>;
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
## Observability Hooks
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
import {
|
|
618
|
+
useMetrics,
|
|
619
|
+
useAuditLogs,
|
|
620
|
+
useIncidents,
|
|
621
|
+
useSystemHealth,
|
|
622
|
+
useRealtimeStats,
|
|
623
|
+
useErrorTracking,
|
|
624
|
+
} from '@vaiftech/react';
|
|
625
|
+
|
|
626
|
+
// Metrics dashboard
|
|
627
|
+
function MetricsDashboard() {
|
|
628
|
+
const { metrics, isLoading, timeRange, setTimeRange, refresh } = useMetrics({
|
|
629
|
+
sources: ['api', 'database', 'storage'],
|
|
630
|
+
interval: '1h',
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<div>
|
|
635
|
+
<select onChange={(e) => setTimeRange(e.target.value)} value={timeRange}>
|
|
636
|
+
<option value="1h">Last Hour</option>
|
|
637
|
+
<option value="24h">Last 24 Hours</option>
|
|
638
|
+
<option value="7d">Last 7 Days</option>
|
|
639
|
+
</select>
|
|
640
|
+
{metrics?.map(m => (
|
|
641
|
+
<div key={m.name}>{m.name}: {m.value}</div>
|
|
642
|
+
))}
|
|
643
|
+
</div>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Audit logs
|
|
648
|
+
function AuditLogViewer() {
|
|
649
|
+
const { logs, hasMore, loadMore, setFilters } = useAuditLogs({ limit: 50 });
|
|
650
|
+
|
|
651
|
+
return (
|
|
652
|
+
<div>
|
|
653
|
+
<select onChange={(e) => setFilters({ action: e.target.value })}>
|
|
654
|
+
<option value="">All Actions</option>
|
|
655
|
+
<option value="create">Create</option>
|
|
656
|
+
<option value="update">Update</option>
|
|
657
|
+
<option value="delete">Delete</option>
|
|
658
|
+
</select>
|
|
659
|
+
<ul>
|
|
660
|
+
{logs?.map(log => (
|
|
661
|
+
<li key={log.id}>{log.timestamp}: {log.action} by {log.userId}</li>
|
|
662
|
+
))}
|
|
663
|
+
</ul>
|
|
664
|
+
{hasMore && <button onClick={loadMore}>Load More</button>}
|
|
665
|
+
</div>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Incident management
|
|
670
|
+
function IncidentList() {
|
|
671
|
+
const { incidents, activeCount, acknowledge, resolve } = useIncidents();
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<div>
|
|
675
|
+
<h3>Active Incidents: {activeCount}</h3>
|
|
676
|
+
{incidents?.map(incident => (
|
|
677
|
+
<div key={incident.id}>
|
|
678
|
+
<span>{incident.title} - {incident.severity}</span>
|
|
679
|
+
{incident.status === 'open' && (
|
|
680
|
+
<button onClick={() => acknowledge(incident.id)}>Ack</button>
|
|
681
|
+
)}
|
|
682
|
+
{incident.status === 'acknowledged' && (
|
|
683
|
+
<button onClick={() => resolve(incident.id)}>Resolve</button>
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
))}
|
|
687
|
+
</div>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// System health
|
|
692
|
+
function SystemHealth() {
|
|
693
|
+
const { isHealthy, services, lastCheck } = useSystemHealth({ pollInterval: 30000 });
|
|
694
|
+
|
|
695
|
+
return (
|
|
696
|
+
<div>
|
|
697
|
+
<div>Status: {isHealthy ? 'Healthy' : 'Degraded'}</div>
|
|
698
|
+
<div>Last Check: {lastCheck?.toLocaleString()}</div>
|
|
699
|
+
{services?.map(svc => (
|
|
700
|
+
<div key={svc.name}>{svc.name}: {svc.status} ({svc.latency}ms)</div>
|
|
701
|
+
))}
|
|
702
|
+
</div>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Realtime stats
|
|
707
|
+
function RealtimeStats() {
|
|
708
|
+
const { connections, requestsPerSecond, activeUsers } = useRealtimeStats();
|
|
709
|
+
|
|
710
|
+
return (
|
|
711
|
+
<div>
|
|
712
|
+
<div>Connections: {connections}</div>
|
|
713
|
+
<div>Requests/sec: {requestsPerSecond}</div>
|
|
714
|
+
<div>Active Users: {activeUsers}</div>
|
|
715
|
+
</div>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Error tracking
|
|
720
|
+
function ErrorTracker() {
|
|
721
|
+
const { errorRate, topErrors, trackError } = useErrorTracking({ timeRange: '24h' });
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<div>
|
|
725
|
+
<div>Error Rate: {errorRate}%</div>
|
|
726
|
+
{topErrors?.map(err => (
|
|
727
|
+
<div key={err.message}>{err.message} ({err.count})</div>
|
|
728
|
+
))}
|
|
729
|
+
</div>
|
|
730
|
+
);
|
|
731
|
+
}
|
|
205
732
|
```
|
|
206
733
|
|
|
207
734
|
## TypeScript Support
|
|
@@ -217,11 +744,15 @@ interface User {
|
|
|
217
744
|
|
|
218
745
|
const { data } = useQuery<User>('users');
|
|
219
746
|
// data is User[] | undefined
|
|
747
|
+
|
|
748
|
+
const { data: user } = useMongoFindOne<User>('users', { email: 'test@example.com' });
|
|
749
|
+
// user is User | null | undefined
|
|
220
750
|
```
|
|
221
751
|
|
|
222
752
|
## Related Packages
|
|
223
753
|
|
|
224
754
|
- [@vaiftech/client](https://www.npmjs.com/package/@vaiftech/client) - Core client SDK
|
|
755
|
+
- [@vaiftech/auth](https://www.npmjs.com/package/@vaiftech/auth) - Standalone auth client
|
|
225
756
|
- [@vaiftech/sdk-expo](https://www.npmjs.com/package/@vaiftech/sdk-expo) - React Native/Expo SDK
|
|
226
757
|
- [@vaiftech/cli](https://www.npmjs.com/package/@vaiftech/cli) - CLI tools
|
|
227
758
|
|