@vaiftech/sdk-expo 1.0.5 → 1.0.7
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 +595 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,19 @@ export default function App() {
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Hooks Overview
|
|
37
|
+
|
|
38
|
+
| Category | Hooks |
|
|
39
|
+
|----------|-------|
|
|
40
|
+
| **Auth** | `useAuth`, `useUser`, `useToken`, `usePasswordReset`, `useEmailVerification`, `useMagicLink`, `useOAuth`, `useMFA` |
|
|
41
|
+
| **Query** | `useQuery`, `useQueryById`, `useQueryFirst`, `usePaginatedQuery`, `useInfiniteQuery`, `useCount` |
|
|
42
|
+
| **Mutation** | `useCreate`, `useUpdate`, `useDelete`, `useUpsert`, `useBatchCreate`, `useBatchUpdate`, `useBatchDelete`, `useOptimisticMutation` |
|
|
43
|
+
| **Realtime** | `useSubscription`, `useChannel`, `usePresence`, `useRealtimeConnection`, `useBroadcast` |
|
|
44
|
+
| **Storage** | `useUpload`, `useDownload`, `useFile`, `useFiles`, `usePublicUrl` |
|
|
45
|
+
| **Functions** | `useFunction`, `useRpc`, `useFunctionQuery`, `useFunctionList`, `useBatchInvoke`, `useScheduledFunction` |
|
|
46
|
+
| **MongoDB** | `useMongoFind`, `useMongoFindOne`, `useMongoAggregate`, `useMongoInsertOne`, `useMongoInsertMany`, `useMongoUpdateOne`, `useMongoUpdateMany`, `useMongoDeleteOne`, `useMongoDeleteMany`, `useMongoInfiniteFind`, `useMongoCount`, `useMongoDistinct`, `useMongoCollection` |
|
|
47
|
+
| **Observability** | `useMetrics`, `useAuditLogs`, `useIncidents`, `useSystemHealth`, `useRealtimeStats`, `useErrorTracking` |
|
|
48
|
+
|
|
36
49
|
## Features
|
|
37
50
|
|
|
38
51
|
### Persistent Sessions
|
|
@@ -67,7 +80,7 @@ import { useUpload } from '@vaiftech/sdk-expo';
|
|
|
67
80
|
import * as ImagePicker from 'expo-image-picker';
|
|
68
81
|
|
|
69
82
|
function ImageUpload() {
|
|
70
|
-
const {
|
|
83
|
+
const { upload, isUploading, progress } = useUpload();
|
|
71
84
|
|
|
72
85
|
const pickAndUpload = async () => {
|
|
73
86
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
@@ -77,7 +90,10 @@ function ImageUpload() {
|
|
|
77
90
|
|
|
78
91
|
if (!result.canceled) {
|
|
79
92
|
const { uri } = result.assets[0];
|
|
80
|
-
|
|
93
|
+
// Convert URI to blob and upload
|
|
94
|
+
const response = await fetch(uri);
|
|
95
|
+
const blob = await response.blob();
|
|
96
|
+
const uploaded = await upload(blob, `photos/${Date.now()}.jpg`);
|
|
81
97
|
console.log('Uploaded:', uploaded?.url);
|
|
82
98
|
}
|
|
83
99
|
};
|
|
@@ -94,122 +110,625 @@ function ImageUpload() {
|
|
|
94
110
|
}
|
|
95
111
|
```
|
|
96
112
|
|
|
97
|
-
|
|
113
|
+
## Authentication
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { useAuth, useUser, useOAuth, useMFA } from '@vaiftech/sdk-expo';
|
|
117
|
+
|
|
118
|
+
function AuthScreen() {
|
|
119
|
+
const { user, isLoading, signIn, signUp, signOut } = useAuth();
|
|
120
|
+
|
|
121
|
+
if (isLoading) return <ActivityIndicator />;
|
|
122
|
+
|
|
123
|
+
if (!user) {
|
|
124
|
+
return (
|
|
125
|
+
<View>
|
|
126
|
+
<Button title="Sign In" onPress={() => signIn({ email, password })} />
|
|
127
|
+
<Button title="Sign Up" onPress={() => signUp({ email, password, name })} />
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<View>
|
|
134
|
+
<Text>Welcome, {user.email}</Text>
|
|
135
|
+
<Button title="Sign Out" onPress={signOut} />
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// OAuth login
|
|
141
|
+
function OAuthLogin() {
|
|
142
|
+
const { signInWithProvider, isLoading } = useOAuth();
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<View>
|
|
146
|
+
<Button
|
|
147
|
+
title="Sign in with Google"
|
|
148
|
+
onPress={() => signInWithProvider('google')}
|
|
149
|
+
disabled={isLoading}
|
|
150
|
+
/>
|
|
151
|
+
<Button
|
|
152
|
+
title="Sign in with Apple"
|
|
153
|
+
onPress={() => signInWithProvider('apple')}
|
|
154
|
+
disabled={isLoading}
|
|
155
|
+
/>
|
|
156
|
+
</View>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// MFA setup
|
|
161
|
+
function MFASetup() {
|
|
162
|
+
const { enroll, verify, qrCode } = useMFA();
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<View>
|
|
166
|
+
{qrCode && <Image source={{ uri: qrCode }} style={{ width: 200, height: 200 }} />}
|
|
167
|
+
<Button title="Enable MFA" onPress={() => enroll({ type: 'totp' })} />
|
|
168
|
+
</View>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Standalone Auth Provider
|
|
174
|
+
|
|
175
|
+
For auth-only applications:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { AuthProvider, useAuth, useSession } from '@vaiftech/sdk-expo';
|
|
179
|
+
|
|
180
|
+
function App() {
|
|
181
|
+
return (
|
|
182
|
+
<AuthProvider config={{ url: 'https://api.vaif.studio', apiKey: 'your-key' }}>
|
|
183
|
+
<Navigation />
|
|
184
|
+
</AuthProvider>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Data Fetching
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { useQuery, useQueryById, usePaginatedQuery, useInfiniteQuery } from '@vaiftech/sdk-expo';
|
|
193
|
+
|
|
194
|
+
interface Task {
|
|
195
|
+
id: string;
|
|
196
|
+
title: string;
|
|
197
|
+
completed: boolean;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Basic query
|
|
201
|
+
function TaskList() {
|
|
202
|
+
const { data: tasks, isLoading, error, refetch } = useQuery<Task>('tasks', {
|
|
203
|
+
filters: [{ field: 'completed', operator: 'eq', value: false }],
|
|
204
|
+
orderBy: [{ field: 'createdAt', direction: 'desc' }],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (isLoading) return <ActivityIndicator />;
|
|
208
|
+
if (error) return <Text>Error: {error.message}</Text>;
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<FlatList
|
|
212
|
+
data={tasks}
|
|
213
|
+
keyExtractor={(item) => item.id}
|
|
214
|
+
renderItem={({ item }) => <Text>{item.title}</Text>}
|
|
215
|
+
refreshing={isLoading}
|
|
216
|
+
onRefresh={refetch}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Query by ID
|
|
222
|
+
function TaskDetail({ taskId }: { taskId: string }) {
|
|
223
|
+
const { data: task, isLoading } = useQueryById<Task>('tasks', taskId);
|
|
224
|
+
|
|
225
|
+
if (isLoading) return <ActivityIndicator />;
|
|
226
|
+
return <Text>{task?.title}</Text>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Paginated query
|
|
230
|
+
function PaginatedTasks() {
|
|
231
|
+
const { data, page, totalPages, nextPage, prevPage } = usePaginatedQuery<Task>('tasks', {
|
|
232
|
+
pageSize: 20,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<View>
|
|
237
|
+
<FlatList data={data} renderItem={({ item }) => <Text>{item.title}</Text>} />
|
|
238
|
+
<View style={{ flexDirection: 'row' }}>
|
|
239
|
+
<Button title="Prev" onPress={prevPage} disabled={page === 1} />
|
|
240
|
+
<Text>{page} / {totalPages}</Text>
|
|
241
|
+
<Button title="Next" onPress={nextPage} disabled={page === totalPages} />
|
|
242
|
+
</View>
|
|
243
|
+
</View>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Infinite scroll
|
|
248
|
+
function InfiniteTasks() {
|
|
249
|
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery<Task>('tasks');
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<FlatList
|
|
253
|
+
data={data}
|
|
254
|
+
keyExtractor={(item) => item.id}
|
|
255
|
+
renderItem={({ item }) => <Text>{item.title}</Text>}
|
|
256
|
+
onEndReached={() => hasNextPage && fetchNextPage()}
|
|
257
|
+
ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Mutations
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { useCreate, useUpdate, useDelete } from '@vaiftech/sdk-expo';
|
|
267
|
+
|
|
268
|
+
// Create
|
|
269
|
+
function CreateTask() {
|
|
270
|
+
const { create, isCreating } = useCreate<Task>('tasks');
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Button
|
|
274
|
+
title={isCreating ? 'Creating...' : 'Add Task'}
|
|
275
|
+
onPress={() => create({ title: 'New Task', completed: false })}
|
|
276
|
+
disabled={isCreating}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update
|
|
282
|
+
function ToggleTask({ task }: { task: Task }) {
|
|
283
|
+
const { update, isUpdating } = useUpdate<Task>('tasks');
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<Switch
|
|
287
|
+
value={task.completed}
|
|
288
|
+
onValueChange={(completed) => update(task.id, { completed })}
|
|
289
|
+
disabled={isUpdating}
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Delete
|
|
295
|
+
function DeleteTask({ taskId }: { taskId: string }) {
|
|
296
|
+
const { remove, isDeleting } = useDelete('tasks');
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<Button
|
|
300
|
+
title="Delete"
|
|
301
|
+
onPress={() => remove(taskId)}
|
|
302
|
+
disabled={isDeleting}
|
|
303
|
+
color="red"
|
|
304
|
+
/>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Realtime
|
|
98
310
|
|
|
99
311
|
```tsx
|
|
100
|
-
import {
|
|
312
|
+
import { useSubscription, usePresence, useBroadcast, useChannel } from '@vaiftech/sdk-expo';
|
|
101
313
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
314
|
+
// Listen for new messages
|
|
315
|
+
function MessageListener() {
|
|
316
|
+
useSubscription<Message>('messages', {
|
|
105
317
|
filters: [{ field: 'room_id', operator: 'eq', value: roomId }],
|
|
106
|
-
onInsert: (message) =>
|
|
107
|
-
|
|
108
|
-
|
|
318
|
+
onInsert: (message) => console.log('New:', message),
|
|
319
|
+
onUpdate: (message) => console.log('Updated:', message),
|
|
320
|
+
onDelete: (message) => console.log('Deleted:', message.id),
|
|
109
321
|
});
|
|
110
322
|
|
|
111
|
-
|
|
112
|
-
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Track online users
|
|
327
|
+
function OnlineUsers({ roomId }: { roomId: string }) {
|
|
328
|
+
const { users, track, leave } = usePresence(`room-${roomId}`);
|
|
113
329
|
|
|
114
330
|
useEffect(() => {
|
|
115
|
-
track({ status: 'online' });
|
|
331
|
+
track({ status: 'online', name: currentUser.name });
|
|
116
332
|
return () => leave();
|
|
117
333
|
}, []);
|
|
118
334
|
|
|
119
335
|
return (
|
|
120
336
|
<View>
|
|
121
337
|
<Text>Online: {users.length}</Text>
|
|
338
|
+
{users.map(u => <Text key={u.id}>{u.name}</Text>)}
|
|
122
339
|
</View>
|
|
123
340
|
);
|
|
124
341
|
}
|
|
125
|
-
```
|
|
126
342
|
|
|
127
|
-
|
|
343
|
+
// Broadcast typing indicator
|
|
344
|
+
function TypingIndicator({ roomId }: { roomId: string }) {
|
|
345
|
+
const { send, subscribe } = useBroadcast(`room-${roomId}`);
|
|
346
|
+
const [typing, setTyping] = useState<string[]>([]);
|
|
128
347
|
|
|
129
|
-
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
subscribe('typing', (event) => {
|
|
350
|
+
setTyping(prev => [...prev, event.userName]);
|
|
351
|
+
setTimeout(() => {
|
|
352
|
+
setTyping(prev => prev.filter(n => n !== event.userName));
|
|
353
|
+
}, 3000);
|
|
354
|
+
});
|
|
355
|
+
}, []);
|
|
130
356
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
357
|
+
return (
|
|
358
|
+
<Text>{typing.length > 0 ? `${typing.join(', ')} typing...` : ''}</Text>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Channel messaging
|
|
363
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
364
|
+
const channel = useChannel(`room-${roomId}`);
|
|
365
|
+
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
channel.on('message', (msg) => {
|
|
368
|
+
// Handle incoming message
|
|
369
|
+
});
|
|
370
|
+
return () => channel.off('message');
|
|
371
|
+
}, [channel]);
|
|
372
|
+
|
|
373
|
+
const sendMessage = (text: string) => {
|
|
374
|
+
channel.send('message', { text, userId: currentUser.id });
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
return <View>{/* Chat UI */}</View>;
|
|
378
|
+
}
|
|
152
379
|
```
|
|
153
380
|
|
|
154
|
-
|
|
381
|
+
## Storage
|
|
155
382
|
|
|
156
383
|
```tsx
|
|
157
|
-
import {
|
|
384
|
+
import { useUpload, useDownload, useFile, useFiles, usePublicUrl } from '@vaiftech/sdk-expo';
|
|
158
385
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
386
|
+
// Upload with progress
|
|
387
|
+
function FileUpload() {
|
|
388
|
+
const { upload, isUploading, progress } = useUpload();
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<View>
|
|
392
|
+
<Button
|
|
393
|
+
title={isUploading ? `${progress}%` : 'Upload'}
|
|
394
|
+
onPress={async () => {
|
|
395
|
+
const result = await upload(fileBlob, 'uploads/file.pdf');
|
|
396
|
+
Alert.alert('Uploaded!', result?.url);
|
|
397
|
+
}}
|
|
398
|
+
disabled={isUploading}
|
|
399
|
+
/>
|
|
400
|
+
{isUploading && <ProgressBar progress={progress / 100} />}
|
|
401
|
+
</View>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
164
404
|
|
|
165
|
-
//
|
|
166
|
-
|
|
405
|
+
// Download file
|
|
406
|
+
function DownloadFile({ path }: { path: string }) {
|
|
407
|
+
const { download, isDownloading } = useDownload();
|
|
167
408
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
409
|
+
return (
|
|
410
|
+
<Button
|
|
411
|
+
title={isDownloading ? 'Downloading...' : 'Download'}
|
|
412
|
+
onPress={async () => {
|
|
413
|
+
const blob = await download(path);
|
|
414
|
+
// Save to device or open
|
|
415
|
+
}}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Display image
|
|
421
|
+
function ProfileImage({ path }: { path: string }) {
|
|
422
|
+
const url = usePublicUrl(path);
|
|
423
|
+
return url ? <Image source={{ uri: url }} style={styles.avatar} /> : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// List files
|
|
427
|
+
function FileList({ folder }: { folder: string }) {
|
|
428
|
+
const { files, isLoading } = useFiles({ prefix: folder });
|
|
429
|
+
|
|
430
|
+
if (isLoading) return <ActivityIndicator />;
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<FlatList
|
|
434
|
+
data={files}
|
|
435
|
+
keyExtractor={(f) => f.path}
|
|
436
|
+
renderItem={({ item }) => (
|
|
437
|
+
<Text>{item.name} ({item.size} bytes)</Text>
|
|
438
|
+
)}
|
|
439
|
+
/>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
174
442
|
```
|
|
175
443
|
|
|
176
|
-
|
|
444
|
+
## Edge Functions
|
|
177
445
|
|
|
178
446
|
```tsx
|
|
179
|
-
import {
|
|
447
|
+
import { useFunction, useRpc, useBatchInvoke } from '@vaiftech/sdk-expo';
|
|
448
|
+
|
|
449
|
+
// Invoke function
|
|
450
|
+
function ProcessImage() {
|
|
451
|
+
const { invoke, isInvoking, result, error } = useFunction('process-image');
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<Button
|
|
455
|
+
title={isInvoking ? 'Processing...' : 'Process'}
|
|
456
|
+
onPress={() => invoke({ imageUrl: 'https://...' })}
|
|
457
|
+
disabled={isInvoking}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// RPC-style call
|
|
463
|
+
function Calculator() {
|
|
464
|
+
const { call, isLoading, data } = useRpc<{ result: number }>('calculate');
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<View>
|
|
468
|
+
<Button title="Calculate" onPress={() => call({ a: 5, b: 3, op: 'add' })} />
|
|
469
|
+
{data && <Text>Result: {data.result}</Text>}
|
|
470
|
+
</View>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
180
473
|
|
|
181
|
-
//
|
|
182
|
-
|
|
474
|
+
// Batch operations
|
|
475
|
+
function BatchProcessor() {
|
|
476
|
+
const { invoke, isInvoking, results } = useBatchInvoke();
|
|
183
477
|
|
|
184
|
-
|
|
185
|
-
|
|
478
|
+
const processAll = async () => {
|
|
479
|
+
await invoke([
|
|
480
|
+
{ name: 'resize', payload: { url: 'img1.jpg' } },
|
|
481
|
+
{ name: 'resize', payload: { url: 'img2.jpg' } },
|
|
482
|
+
]);
|
|
483
|
+
};
|
|
186
484
|
|
|
187
|
-
|
|
188
|
-
|
|
485
|
+
return <Button title="Process All" onPress={processAll} disabled={isInvoking} />;
|
|
486
|
+
}
|
|
189
487
|
```
|
|
190
488
|
|
|
191
|
-
|
|
489
|
+
## MongoDB Hooks
|
|
192
490
|
|
|
193
491
|
```tsx
|
|
194
|
-
import {
|
|
492
|
+
import {
|
|
493
|
+
useMongoFind,
|
|
494
|
+
useMongoFindOne,
|
|
495
|
+
useMongoInsertOne,
|
|
496
|
+
useMongoUpdateOne,
|
|
497
|
+
useMongoDeleteOne,
|
|
498
|
+
useMongoAggregate,
|
|
499
|
+
useMongoInfiniteFind,
|
|
500
|
+
useMongoCollection,
|
|
501
|
+
} from '@vaiftech/sdk-expo';
|
|
502
|
+
|
|
503
|
+
// Find documents
|
|
504
|
+
function UserList() {
|
|
505
|
+
const { data: users, isLoading, refetch } = useMongoFind<User>('users', {
|
|
506
|
+
filter: { status: 'active' },
|
|
507
|
+
sort: { createdAt: -1 },
|
|
508
|
+
limit: 20,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (isLoading) return <ActivityIndicator />;
|
|
512
|
+
|
|
513
|
+
return (
|
|
514
|
+
<FlatList
|
|
515
|
+
data={users}
|
|
516
|
+
keyExtractor={(item) => item._id}
|
|
517
|
+
renderItem={({ item }) => <Text>{item.name}</Text>}
|
|
518
|
+
refreshing={isLoading}
|
|
519
|
+
onRefresh={refetch}
|
|
520
|
+
/>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Find single document
|
|
525
|
+
function UserProfile({ id }: { id: string }) {
|
|
526
|
+
const { data: user, isLoading } = useMongoFindOne<User>('users', { _id: id });
|
|
527
|
+
return user ? <Text>{user.name}</Text> : null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Insert document
|
|
531
|
+
function CreateUser() {
|
|
532
|
+
const { insertOne, isInserting } = useMongoInsertOne<User>('users');
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<Button
|
|
536
|
+
title={isInserting ? 'Creating...' : 'Create User'}
|
|
537
|
+
onPress={() => insertOne({ name: 'New User', email: 'new@example.com' })}
|
|
538
|
+
disabled={isInserting}
|
|
539
|
+
/>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Update document
|
|
544
|
+
function UpdateUser({ id }: { id: string }) {
|
|
545
|
+
const { updateOne, isUpdating } = useMongoUpdateOne('users');
|
|
546
|
+
|
|
547
|
+
return (
|
|
548
|
+
<Button
|
|
549
|
+
title="Update"
|
|
550
|
+
onPress={() => updateOne({ _id: id }, { $set: { updatedAt: new Date() } })}
|
|
551
|
+
disabled={isUpdating}
|
|
552
|
+
/>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Delete document
|
|
557
|
+
function DeleteUser({ id }: { id: string }) {
|
|
558
|
+
const { deleteOne, isDeleting } = useMongoDeleteOne('users');
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<Button
|
|
562
|
+
title="Delete"
|
|
563
|
+
onPress={() => deleteOne({ _id: id })}
|
|
564
|
+
disabled={isDeleting}
|
|
565
|
+
color="red"
|
|
566
|
+
/>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Aggregation
|
|
571
|
+
function UserStats() {
|
|
572
|
+
const { data: stats } = useMongoAggregate<{ _id: string; count: number }>('users', [
|
|
573
|
+
{ $match: { status: 'active' } },
|
|
574
|
+
{ $group: { _id: '$country', count: { $sum: 1 } } },
|
|
575
|
+
{ $sort: { count: -1 } },
|
|
576
|
+
]);
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<View>
|
|
580
|
+
{stats?.map(stat => (
|
|
581
|
+
<Text key={stat._id}>{stat._id}: {stat.count}</Text>
|
|
582
|
+
))}
|
|
583
|
+
</View>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
195
586
|
|
|
196
|
-
|
|
587
|
+
// Infinite scroll
|
|
588
|
+
function InfiniteUsers() {
|
|
589
|
+
const { data, fetchNextPage, hasNextPage } = useMongoInfiniteFind<User>('users', {
|
|
590
|
+
filter: { status: 'active' },
|
|
591
|
+
limit: 20,
|
|
592
|
+
});
|
|
197
593
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
594
|
+
return (
|
|
595
|
+
<FlatList
|
|
596
|
+
data={data}
|
|
597
|
+
keyExtractor={(item) => item._id}
|
|
598
|
+
renderItem={({ item }) => <Text>{item.name}</Text>}
|
|
599
|
+
onEndReached={() => hasNextPage && fetchNextPage()}
|
|
600
|
+
/>
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Full collection access
|
|
605
|
+
function BulkOps() {
|
|
606
|
+
const collection = useMongoCollection<User>('users');
|
|
607
|
+
|
|
608
|
+
const handleBulk = async () => {
|
|
609
|
+
const count = await collection.count({ status: 'active' });
|
|
610
|
+
const countries = await collection.distinct('country');
|
|
611
|
+
await collection.bulkWrite([
|
|
612
|
+
{ insertOne: { document: { name: 'Bulk User' } } },
|
|
613
|
+
]);
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
return <Button title="Run Bulk Ops" onPress={handleBulk} />;
|
|
617
|
+
}
|
|
201
618
|
```
|
|
202
619
|
|
|
203
|
-
|
|
620
|
+
## Observability Hooks
|
|
204
621
|
|
|
205
622
|
```tsx
|
|
206
|
-
import {
|
|
623
|
+
import {
|
|
624
|
+
useMetrics,
|
|
625
|
+
useAuditLogs,
|
|
626
|
+
useIncidents,
|
|
627
|
+
useSystemHealth,
|
|
628
|
+
useRealtimeStats,
|
|
629
|
+
useErrorTracking,
|
|
630
|
+
} from '@vaiftech/sdk-expo';
|
|
631
|
+
|
|
632
|
+
// Metrics dashboard
|
|
633
|
+
function MetricsDashboard() {
|
|
634
|
+
const { metrics, timeRange, setTimeRange, refresh } = useMetrics({
|
|
635
|
+
sources: ['api', 'database'],
|
|
636
|
+
interval: '1h',
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<View>
|
|
641
|
+
<Picker selectedValue={timeRange} onValueChange={setTimeRange}>
|
|
642
|
+
<Picker.Item label="Last Hour" value="1h" />
|
|
643
|
+
<Picker.Item label="Last 24 Hours" value="24h" />
|
|
644
|
+
<Picker.Item label="Last 7 Days" value="7d" />
|
|
645
|
+
</Picker>
|
|
646
|
+
{metrics?.map(m => (
|
|
647
|
+
<Text key={m.name}>{m.name}: {m.value}</Text>
|
|
648
|
+
))}
|
|
649
|
+
</View>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Audit logs
|
|
654
|
+
function AuditLogs() {
|
|
655
|
+
const { logs, hasMore, loadMore } = useAuditLogs({ limit: 50 });
|
|
656
|
+
|
|
657
|
+
return (
|
|
658
|
+
<FlatList
|
|
659
|
+
data={logs}
|
|
660
|
+
keyExtractor={(item) => item.id}
|
|
661
|
+
renderItem={({ item }) => (
|
|
662
|
+
<Text>{item.timestamp}: {item.action} by {item.userId}</Text>
|
|
663
|
+
)}
|
|
664
|
+
onEndReached={() => hasMore && loadMore()}
|
|
665
|
+
/>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Incidents
|
|
670
|
+
function IncidentList() {
|
|
671
|
+
const { incidents, activeCount, acknowledge, resolve } = useIncidents();
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<View>
|
|
675
|
+
<Text>Active: {activeCount}</Text>
|
|
676
|
+
{incidents?.map(incident => (
|
|
677
|
+
<View key={incident.id}>
|
|
678
|
+
<Text>{incident.title} - {incident.severity}</Text>
|
|
679
|
+
{incident.status === 'open' && (
|
|
680
|
+
<Button title="Ack" onPress={() => acknowledge(incident.id)} />
|
|
681
|
+
)}
|
|
682
|
+
{incident.status === 'acknowledged' && (
|
|
683
|
+
<Button title="Resolve" onPress={() => resolve(incident.id)} />
|
|
684
|
+
)}
|
|
685
|
+
</View>
|
|
686
|
+
))}
|
|
687
|
+
</View>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
207
690
|
|
|
208
|
-
//
|
|
209
|
-
|
|
691
|
+
// System health
|
|
692
|
+
function SystemHealth() {
|
|
693
|
+
const { isHealthy, services, lastCheck } = useSystemHealth({ pollInterval: 30000 });
|
|
210
694
|
|
|
211
|
-
|
|
212
|
-
|
|
695
|
+
return (
|
|
696
|
+
<View>
|
|
697
|
+
<Text>Status: {isHealthy ? 'Healthy' : 'Degraded'}</Text>
|
|
698
|
+
<Text>Last: {lastCheck?.toLocaleString()}</Text>
|
|
699
|
+
{services?.map(svc => (
|
|
700
|
+
<Text key={svc.name}>{svc.name}: {svc.status} ({svc.latency}ms)</Text>
|
|
701
|
+
))}
|
|
702
|
+
</View>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Realtime stats
|
|
707
|
+
function RealtimeStats() {
|
|
708
|
+
const { connections, requestsPerSecond, activeUsers } = useRealtimeStats();
|
|
709
|
+
|
|
710
|
+
return (
|
|
711
|
+
<View>
|
|
712
|
+
<Text>Connections: {connections}</Text>
|
|
713
|
+
<Text>Requests/sec: {requestsPerSecond}</Text>
|
|
714
|
+
<Text>Active Users: {activeUsers}</Text>
|
|
715
|
+
</View>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Error tracking
|
|
720
|
+
function ErrorTracker() {
|
|
721
|
+
const { errorRate, topErrors, trackError } = useErrorTracking({ timeRange: '24h' });
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<View>
|
|
725
|
+
<Text>Error Rate: {errorRate}%</Text>
|
|
726
|
+
{topErrors?.map(err => (
|
|
727
|
+
<Text key={err.message}>{err.message} ({err.count})</Text>
|
|
728
|
+
))}
|
|
729
|
+
</View>
|
|
730
|
+
);
|
|
731
|
+
}
|
|
213
732
|
```
|
|
214
733
|
|
|
215
734
|
## TypeScript Support
|
|
@@ -225,11 +744,15 @@ interface User {
|
|
|
225
744
|
|
|
226
745
|
const { data } = useQuery<User>('users');
|
|
227
746
|
// data is User[] | undefined
|
|
747
|
+
|
|
748
|
+
const { data: user } = useMongoFindOne<User>('users', { _id: id });
|
|
749
|
+
// user is User | null | undefined
|
|
228
750
|
```
|
|
229
751
|
|
|
230
752
|
## Related Packages
|
|
231
753
|
|
|
232
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
|
|
233
756
|
- [@vaiftech/react](https://www.npmjs.com/package/@vaiftech/react) - React (web) hooks
|
|
234
757
|
- [@vaiftech/cli](https://www.npmjs.com/package/@vaiftech/cli) - CLI tools
|
|
235
758
|
|