amplifyquery 1.0.17 β 1.0.19
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 +192 -2
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/service.js +132 -12
- package/dist/types.d.ts +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,11 @@ A library that combines AWS Amplify and React Query, making it easier to manage
|
|
|
8
8
|
- π **React Query Integration**: Leverage all React Query features like caching, retries, background updates, etc.
|
|
9
9
|
- π± **Offline Support**: Persistent query caching via MMKV for fast data loading even offline.
|
|
10
10
|
- πͺ **Convenient Hooks API**: Abstract complex data synchronization into simple Hooks.
|
|
11
|
+
- π΄ **Realtime Subscriptions**: Built-in support for AWS Amplify realtime updates with automatic cache synchronization.
|
|
11
12
|
- π‘ **Auth Mode Support**: Supports various AWS Amplify authentication modes (API Key, IAM, Cognito, etc.).
|
|
12
13
|
- βοΈ **Global Configuration**: Set model mappings and auth modes once - no more repetitive configuration.
|
|
13
14
|
- β‘ **Performance Optimized**: Maximize performance with request batching and intelligent caching.
|
|
15
|
+
- π **Automatic Cache Sync**: Mutations automatically update the cache for consistent UI state.
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
@@ -192,6 +194,8 @@ await TodoService.delete("some-id");
|
|
|
192
194
|
|
|
193
195
|
### 5. Using React Hooks
|
|
194
196
|
|
|
197
|
+
#### Basic Usage
|
|
198
|
+
|
|
195
199
|
```tsx
|
|
196
200
|
import React from "react";
|
|
197
201
|
import { View, Text, Button } from "react-native";
|
|
@@ -206,11 +210,16 @@ function TodoScreen() {
|
|
|
206
210
|
create,
|
|
207
211
|
update,
|
|
208
212
|
delete: deleteTodo,
|
|
213
|
+
getItem,
|
|
209
214
|
} = TodoService.useHook();
|
|
210
215
|
|
|
211
216
|
// Hook for managing a single item
|
|
212
|
-
const {
|
|
213
|
-
|
|
217
|
+
const {
|
|
218
|
+
item: settings,
|
|
219
|
+
isLoading: isSettingsLoading,
|
|
220
|
+
update: updateSettings,
|
|
221
|
+
refresh: refreshSettings,
|
|
222
|
+
} = UserSettingsService.useItemHook("settings-id");
|
|
214
223
|
|
|
215
224
|
if (isLoading) return <Text>Loading...</Text>;
|
|
216
225
|
if (error) return <Text>Error: {error.message}</Text>;
|
|
@@ -237,8 +246,118 @@ function TodoScreen() {
|
|
|
237
246
|
}
|
|
238
247
|
```
|
|
239
248
|
|
|
249
|
+
#### Realtime Subscriptions
|
|
250
|
+
|
|
251
|
+
Enable real-time updates using AWS Amplify's `observeQuery` feature:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
function TodoScreen() {
|
|
255
|
+
// Enable realtime for list updates
|
|
256
|
+
const {
|
|
257
|
+
items: todos,
|
|
258
|
+
isLoading,
|
|
259
|
+
isSynced, // true when realtime subscription is active
|
|
260
|
+
create,
|
|
261
|
+
update,
|
|
262
|
+
delete: deleteTodo,
|
|
263
|
+
} = TodoService.useHook({
|
|
264
|
+
realtime: {
|
|
265
|
+
enabled: true,
|
|
266
|
+
// Optional: filter events to subscribe to
|
|
267
|
+
events: ["create", "update", "delete"],
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Enable realtime for single item updates
|
|
272
|
+
const {
|
|
273
|
+
item: todo,
|
|
274
|
+
isSynced: isItemSynced,
|
|
275
|
+
update: updateTodo,
|
|
276
|
+
} = TodoService.useItemHook(todoId, {
|
|
277
|
+
realtime: {
|
|
278
|
+
enabled: true,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<View>
|
|
284
|
+
{isSynced && <Text>π’ Live updates active</Text>}
|
|
285
|
+
{todos.map((todo) => (
|
|
286
|
+
<Text key={todo.id}>{todo.name}</Text>
|
|
287
|
+
))}
|
|
288
|
+
</View>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Realtime Features:**
|
|
294
|
+
- β
Automatic cache synchronization when data changes
|
|
295
|
+
- β
Works across multiple devices/sessions
|
|
296
|
+
- β
`isSynced` flag indicates subscription status
|
|
297
|
+
- β
Optimistic updates are immediately reflected
|
|
298
|
+
- β
No manual refresh needed for real-time changes
|
|
299
|
+
|
|
300
|
+
**Note:** When `realtime.enabled` is `true`, the initial fetch is skipped to avoid duplicate data. The subscription provides the initial data set.
|
|
301
|
+
|
|
240
302
|
## Advanced Features
|
|
241
303
|
|
|
304
|
+
### Realtime Subscriptions
|
|
305
|
+
|
|
306
|
+
AmplifyQuery supports real-time data synchronization using AWS Amplify's `observeQuery` API. When enabled, your UI automatically updates when data changes on the server or other devices.
|
|
307
|
+
|
|
308
|
+
#### useHook Realtime Options
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const {
|
|
312
|
+
items,
|
|
313
|
+
isSynced, // true when subscription is active
|
|
314
|
+
create,
|
|
315
|
+
update,
|
|
316
|
+
delete: deleteItem,
|
|
317
|
+
} = TodoService.useHook({
|
|
318
|
+
realtime: {
|
|
319
|
+
enabled: true, // Enable realtime subscription
|
|
320
|
+
events: ["create", "update", "delete"], // Optional: filter events
|
|
321
|
+
observeOptions: {
|
|
322
|
+
// Optional: additional observeQuery options
|
|
323
|
+
filter: { completed: { eq: false } },
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### useItemHook Realtime Options
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const {
|
|
333
|
+
item,
|
|
334
|
+
isSynced, // true when subscription is active
|
|
335
|
+
update,
|
|
336
|
+
delete: deleteItem,
|
|
337
|
+
} = TodoService.useItemHook(todoId, {
|
|
338
|
+
realtime: {
|
|
339
|
+
enabled: true,
|
|
340
|
+
observeOptions: {
|
|
341
|
+
// Optional: additional observeQuery options
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### How It Works
|
|
348
|
+
|
|
349
|
+
1. **Initial Load**: When `realtime.enabled` is `true`, the hook subscribes to `observeQuery` instead of making a one-time fetch
|
|
350
|
+
2. **Cache Updates**: All changes (create/update/delete) are automatically synchronized to the React Query cache
|
|
351
|
+
3. **Cross-Device**: Changes made on other devices or sessions are immediately reflected
|
|
352
|
+
4. **Optimistic Updates**: Local mutations are immediately reflected, then confirmed via realtime events
|
|
353
|
+
|
|
354
|
+
#### Best Practices
|
|
355
|
+
|
|
356
|
+
- Use realtime for collaborative features or when data changes frequently
|
|
357
|
+
- Monitor `isSynced` to show connection status to users
|
|
358
|
+
- Combine with optimistic updates for the best user experience
|
|
359
|
+
- Consider using `events` filter to reduce unnecessary updates
|
|
360
|
+
|
|
242
361
|
### Global Configuration
|
|
243
362
|
|
|
244
363
|
AmplifyQuery supports global configuration to reduce code duplication and simplify service creation.
|
|
@@ -338,6 +457,25 @@ TodoService.resetCache();
|
|
|
338
457
|
const todos = await TodoService.list({ forceRefresh: true });
|
|
339
458
|
```
|
|
340
459
|
|
|
460
|
+
#### Automatic Cache Synchronization
|
|
461
|
+
|
|
462
|
+
AmplifyQuery automatically synchronizes the cache when you use hook methods (`create`, `update`, `delete`):
|
|
463
|
+
|
|
464
|
+
- **Immediate Updates**: Changes are reflected in the UI immediately after successful operations
|
|
465
|
+
- **Consistent State**: The cache stays in sync across all hook instances
|
|
466
|
+
- **Optimistic Updates**: UI updates before server confirmation for better UX
|
|
467
|
+
- **Realtime Integration**: Works seamlessly with realtime subscriptions
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
// Create, update, delete automatically update the cache
|
|
471
|
+
const { create, update, delete: deleteItem } = TodoService.useHook();
|
|
472
|
+
|
|
473
|
+
// These operations immediately update the hook's items array
|
|
474
|
+
await create({ name: "New Todo" });
|
|
475
|
+
await update({ id: todoId, completed: true });
|
|
476
|
+
await deleteItem(todoId);
|
|
477
|
+
```
|
|
478
|
+
|
|
341
479
|
### Authentication Modes
|
|
342
480
|
|
|
343
481
|
Access data with various authentication methods.
|
|
@@ -363,10 +501,62 @@ await TodoService.list({ authMode: "iam" });
|
|
|
363
501
|
const adminTodoService = TodoService.withAuthMode("iam");
|
|
364
502
|
```
|
|
365
503
|
|
|
504
|
+
### Hook API Reference
|
|
505
|
+
|
|
506
|
+
#### useHook(options?)
|
|
507
|
+
|
|
508
|
+
Returns a hook for managing a list of items.
|
|
509
|
+
|
|
510
|
+
**Options:**
|
|
511
|
+
- `initialFetchOptions?: { fetch?: boolean, filter?: Record<string, any> }` - Control initial data fetch
|
|
512
|
+
- `customList?: { queryName: string, args: Record<string, any>, forceRefresh?: boolean }` - Use custom query for list
|
|
513
|
+
- `realtime?: { enabled?: boolean, observeOptions?: Record<string, any>, events?: Array<"create" | "update" | "delete"> }` - Enable realtime subscriptions
|
|
514
|
+
|
|
515
|
+
**Returns:**
|
|
516
|
+
```typescript
|
|
517
|
+
{
|
|
518
|
+
items: T[]; // Array of items
|
|
519
|
+
isLoading: boolean; // Loading state
|
|
520
|
+
error: Error | null; // Error state
|
|
521
|
+
isSynced?: boolean; // Realtime sync status (if realtime enabled)
|
|
522
|
+
getItem: (id: string) => T | undefined; // Get item by ID from cache
|
|
523
|
+
refresh: (options?: { filter?: Record<string, any> }) => Promise<T[]>; // Refresh list
|
|
524
|
+
create: (data: Partial<T>) => Promise<T | null>; // Create new item
|
|
525
|
+
update: (data: Partial<T> & { id: string }) => Promise<T | null>; // Update item
|
|
526
|
+
delete: (id: string) => Promise<boolean>; // Delete item
|
|
527
|
+
customList?: (queryName: string, args: Record<string, any>, options?: { forceRefresh?: boolean }) => Promise<T[]>; // Custom list query
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### useItemHook(id, options?)
|
|
532
|
+
|
|
533
|
+
Returns a hook for managing a single item.
|
|
534
|
+
|
|
535
|
+
**Parameters:**
|
|
536
|
+
- `id: string` - The ID of the item to manage
|
|
537
|
+
|
|
538
|
+
**Options:**
|
|
539
|
+
- `realtime?: { enabled?: boolean, observeOptions?: Record<string, any> }` - Enable realtime subscription
|
|
540
|
+
|
|
541
|
+
**Returns:**
|
|
542
|
+
```typescript
|
|
543
|
+
{
|
|
544
|
+
item: T | null; // The item (null if not found or not loaded)
|
|
545
|
+
isLoading: boolean; // Loading state
|
|
546
|
+
error: Error | null; // Error state
|
|
547
|
+
isSynced?: boolean; // Realtime sync status (if realtime enabled)
|
|
548
|
+
refresh: () => Promise<T | null>; // Refresh item
|
|
549
|
+
update: (data: Partial<T>) => Promise<T | null>; // Update item (id not needed)
|
|
550
|
+
delete: () => Promise<boolean>; // Delete item (id not needed)
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
366
554
|
## Important Notes
|
|
367
555
|
|
|
368
556
|
- This library is designed to be used with AWS Amplify v6 or higher.
|
|
369
557
|
- Requires React Query v5 or higher.
|
|
558
|
+
- When `realtime.enabled` is `true`, the initial fetch is skipped to avoid duplicate data.
|
|
559
|
+
- All mutations (`create`, `update`, `delete`) automatically synchronize the cache.
|
|
370
560
|
- Test thoroughly before using in a production project.
|
|
371
561
|
|
|
372
562
|
## License
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export declare function setModelOwnerQueryMap(queryMap: Record<string, string>):
|
|
|
11
11
|
* @returns The global model owner query mapping or undefined if not set
|
|
12
12
|
*/
|
|
13
13
|
export declare function getModelOwnerQueryMap(): Record<string, string> | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Set the global selection set mapping
|
|
16
|
+
* @param selectionSetMap Mapping of model names to selection sets
|
|
17
|
+
*/
|
|
14
18
|
/**
|
|
15
19
|
* Get owner query name for a specific model
|
|
16
20
|
* @param modelName The model name
|
package/dist/config.js
CHANGED
|
@@ -26,6 +26,10 @@ function setModelOwnerQueryMap(queryMap) {
|
|
|
26
26
|
function getModelOwnerQueryMap() {
|
|
27
27
|
return globalConfig.modelOwnerQueryMap;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Set the global selection set mapping
|
|
31
|
+
* @param selectionSetMap Mapping of model names to selection sets
|
|
32
|
+
*/
|
|
29
33
|
/**
|
|
30
34
|
* Get owner query name for a specific model
|
|
31
35
|
* @param modelName The model name
|
package/dist/service.js
CHANGED
|
@@ -1122,7 +1122,7 @@ function createAmplifyService(modelName, defaultAuthMode) {
|
|
|
1122
1122
|
},
|
|
1123
1123
|
// React Hook returning method - Reimplemented based on TanStack Query
|
|
1124
1124
|
useHook: (options) => {
|
|
1125
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
1125
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
1126
1126
|
const hookQueryClient = (0, react_query_1.useQueryClient)();
|
|
1127
1127
|
// Determine query key
|
|
1128
1128
|
const queryKey = (0, react_1.useMemo)(() => {
|
|
@@ -1171,10 +1171,11 @@ function createAmplifyService(modelName, defaultAuthMode) {
|
|
|
1171
1171
|
service,
|
|
1172
1172
|
]);
|
|
1173
1173
|
const realtimeEnabled = ((_h = options === null || options === void 0 ? void 0 : options.realtime) === null || _h === void 0 ? void 0 : _h.enabled) === true;
|
|
1174
|
+
const realtimeEvents = (_k = (_j = options === null || options === void 0 ? void 0 : options.realtime) === null || _j === void 0 ? void 0 : _j.events) !== null && _k !== void 0 ? _k : ["create", "update", "delete"];
|
|
1174
1175
|
const queryOptions = {
|
|
1175
1176
|
queryKey,
|
|
1176
1177
|
queryFn,
|
|
1177
|
-
enabled: ((
|
|
1178
|
+
enabled: ((_l = options === null || options === void 0 ? void 0 : options.initialFetchOptions) === null || _l === void 0 ? void 0 : _l.fetch) !== false && !realtimeEnabled,
|
|
1178
1179
|
staleTime: 1000 * 30, // Keep fresh for 30 seconds (refresh more frequently)
|
|
1179
1180
|
refetchOnMount: true, // Refetch on component mount
|
|
1180
1181
|
refetchOnWindowFocus: false, // Don't auto-refetch on window focus
|
|
@@ -1183,22 +1184,63 @@ function createAmplifyService(modelName, defaultAuthMode) {
|
|
|
1183
1184
|
const [isSynced, setIsSynced] = (0, react_1.useState)(undefined);
|
|
1184
1185
|
const { data: items = [], isLoading, error, refetch, } = (0, react_query_1.useQuery)(queryOptions);
|
|
1185
1186
|
(0, react_1.useEffect)(() => {
|
|
1186
|
-
var _a, _b, _c;
|
|
1187
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1187
1188
|
if (!realtimeEnabled)
|
|
1188
1189
|
return;
|
|
1189
1190
|
if (options === null || options === void 0 ? void 0 : options.customList) {
|
|
1190
|
-
|
|
1191
|
-
|
|
1191
|
+
const client = (0, client_1.getClient)();
|
|
1192
|
+
const model = (_a = client.models) === null || _a === void 0 ? void 0 : _a[modelName];
|
|
1193
|
+
if (!model) {
|
|
1194
|
+
console.warn(`π¬ ${modelName} useHook realtime: model not available.`);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const observeOptions = Object.assign({}, (((_b = options === null || options === void 0 ? void 0 : options.realtime) === null || _b === void 0 ? void 0 : _b.observeOptions) || {}));
|
|
1198
|
+
if (observeOptions.filter === undefined &&
|
|
1199
|
+
((_c = options === null || options === void 0 ? void 0 : options.initialFetchOptions) === null || _c === void 0 ? void 0 : _c.filter)) {
|
|
1200
|
+
observeOptions.filter = options.initialFetchOptions.filter;
|
|
1201
|
+
}
|
|
1202
|
+
const subscriptions = [
|
|
1203
|
+
realtimeEvents.includes("create") && model.onCreate
|
|
1204
|
+
? model.onCreate(observeOptions).subscribe({
|
|
1205
|
+
next: () => hookQueryClient.invalidateQueries({
|
|
1206
|
+
queryKey,
|
|
1207
|
+
refetchType: "active",
|
|
1208
|
+
}),
|
|
1209
|
+
error: (err) => console.error(`π¬ ${modelName} useHook realtime onCreate error:`, err),
|
|
1210
|
+
})
|
|
1211
|
+
: null,
|
|
1212
|
+
realtimeEvents.includes("update") && model.onUpdate
|
|
1213
|
+
? model.onUpdate(observeOptions).subscribe({
|
|
1214
|
+
next: () => hookQueryClient.invalidateQueries({
|
|
1215
|
+
queryKey,
|
|
1216
|
+
refetchType: "active",
|
|
1217
|
+
}),
|
|
1218
|
+
error: (err) => console.error(`π¬ ${modelName} useHook realtime onUpdate error:`, err),
|
|
1219
|
+
})
|
|
1220
|
+
: null,
|
|
1221
|
+
realtimeEvents.includes("delete") && model.onDelete
|
|
1222
|
+
? model.onDelete(observeOptions).subscribe({
|
|
1223
|
+
next: () => hookQueryClient.invalidateQueries({
|
|
1224
|
+
queryKey,
|
|
1225
|
+
refetchType: "active",
|
|
1226
|
+
}),
|
|
1227
|
+
error: (err) => console.error(`π¬ ${modelName} useHook realtime onDelete error:`, err),
|
|
1228
|
+
})
|
|
1229
|
+
: null,
|
|
1230
|
+
].filter(Boolean);
|
|
1231
|
+
return () => {
|
|
1232
|
+
subscriptions.forEach((sub) => { var _a; return (_a = sub === null || sub === void 0 ? void 0 : sub.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(sub); });
|
|
1233
|
+
};
|
|
1192
1234
|
}
|
|
1193
1235
|
const client = (0, client_1.getClient)();
|
|
1194
|
-
const model = (
|
|
1236
|
+
const model = (_d = client.models) === null || _d === void 0 ? void 0 : _d[modelName];
|
|
1195
1237
|
if (!(model === null || model === void 0 ? void 0 : model.observeQuery)) {
|
|
1196
1238
|
console.warn(`π¬ ${modelName} useHook realtime: observeQuery not available.`);
|
|
1197
1239
|
return;
|
|
1198
1240
|
}
|
|
1199
|
-
const observeOptions = Object.assign({}, (((
|
|
1241
|
+
const observeOptions = Object.assign({}, (((_e = options === null || options === void 0 ? void 0 : options.realtime) === null || _e === void 0 ? void 0 : _e.observeOptions) || {}));
|
|
1200
1242
|
if (observeOptions.filter === undefined &&
|
|
1201
|
-
((
|
|
1243
|
+
((_f = options === null || options === void 0 ? void 0 : options.initialFetchOptions) === null || _f === void 0 ? void 0 : _f.filter)) {
|
|
1202
1244
|
observeOptions.filter = options.initialFetchOptions.filter;
|
|
1203
1245
|
}
|
|
1204
1246
|
let isMounted = true;
|
|
@@ -1243,8 +1285,8 @@ function createAmplifyService(modelName, defaultAuthMode) {
|
|
|
1243
1285
|
queryKey,
|
|
1244
1286
|
hookQueryClient,
|
|
1245
1287
|
options === null || options === void 0 ? void 0 : options.customList,
|
|
1246
|
-
(
|
|
1247
|
-
(
|
|
1288
|
+
(_m = options === null || options === void 0 ? void 0 : options.initialFetchOptions) === null || _m === void 0 ? void 0 : _m.filter,
|
|
1289
|
+
(_o = options === null || options === void 0 ? void 0 : options.realtime) === null || _o === void 0 ? void 0 : _o.observeOptions,
|
|
1248
1290
|
]);
|
|
1249
1291
|
// Interface functions implementation
|
|
1250
1292
|
const getItem = (0, react_1.useCallback)((id) => {
|
|
@@ -1358,9 +1400,11 @@ function createAmplifyService(modelName, defaultAuthMode) {
|
|
|
1358
1400
|
};
|
|
1359
1401
|
},
|
|
1360
1402
|
// Hook for managing single item - Reimplemented based on TanStack Query
|
|
1361
|
-
useItemHook: (id) => {
|
|
1403
|
+
useItemHook: (id, options) => {
|
|
1404
|
+
var _a, _b;
|
|
1362
1405
|
const hookQueryClient = (0, react_query_1.useQueryClient)();
|
|
1363
1406
|
const singleItemQueryKey = itemKey(modelName, id);
|
|
1407
|
+
const realtimeEnabled = ((_a = options === null || options === void 0 ? void 0 : options.realtime) === null || _a === void 0 ? void 0 : _a.enabled) === true;
|
|
1364
1408
|
// First check data from cache
|
|
1365
1409
|
const rawCachedData = hookQueryClient.getQueryData(singleItemQueryKey);
|
|
1366
1410
|
// π§ λ²κ·Έ μμ : λ°°μ΄μ΄ μΊμλμ΄ μλ κ²½μ° μ²λ¦¬
|
|
@@ -1392,8 +1436,83 @@ function createAmplifyService(modelName, defaultAuthMode) {
|
|
|
1392
1436
|
staleTime: 1000 * 60, // Keep data "fresh" for 1 minute
|
|
1393
1437
|
refetchOnMount: cachedData ? false : true, // Only refetch if no cached data
|
|
1394
1438
|
refetchOnWindowFocus: false, // Disable window focus refetch to prevent loops
|
|
1395
|
-
enabled: !!id, // Only enable query when id exists
|
|
1439
|
+
enabled: !!id && !realtimeEnabled, // Only enable query when id exists
|
|
1396
1440
|
});
|
|
1441
|
+
const [isSynced, setIsSynced] = (0, react_1.useState)(undefined);
|
|
1442
|
+
(0, react_1.useEffect)(() => {
|
|
1443
|
+
var _a, _b;
|
|
1444
|
+
if (!realtimeEnabled || !id)
|
|
1445
|
+
return;
|
|
1446
|
+
const client = (0, client_1.getClient)();
|
|
1447
|
+
const model = (_a = client.models) === null || _a === void 0 ? void 0 : _a[modelName];
|
|
1448
|
+
if (!(model === null || model === void 0 ? void 0 : model.observeQuery)) {
|
|
1449
|
+
console.warn(`π¬ ${modelName} useItemHook realtime: observeQuery not available.`);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
const observeOptions = Object.assign({}, (((_b = options === null || options === void 0 ? void 0 : options.realtime) === null || _b === void 0 ? void 0 : _b.observeOptions) || {}));
|
|
1453
|
+
if (!observeOptions.filter) {
|
|
1454
|
+
observeOptions.filter = { id: { eq: id } };
|
|
1455
|
+
}
|
|
1456
|
+
let isMounted = true;
|
|
1457
|
+
const subscription = model.observeQuery(observeOptions).subscribe({
|
|
1458
|
+
next: ({ items: nextItems, isSynced: synced }) => {
|
|
1459
|
+
if (!isMounted)
|
|
1460
|
+
return;
|
|
1461
|
+
const safeItems = Array.isArray(nextItems)
|
|
1462
|
+
? nextItems.filter(Boolean)
|
|
1463
|
+
: [];
|
|
1464
|
+
const nextItem = safeItems.find((entry) => (entry === null || entry === void 0 ? void 0 : entry.id) === id) || null;
|
|
1465
|
+
const relatedQueryKeys = findRelatedQueryKeys(modelName, hookQueryClient);
|
|
1466
|
+
if (nextItem) {
|
|
1467
|
+
hookQueryClient.setQueryData(singleItemQueryKey, nextItem);
|
|
1468
|
+
relatedQueryKeys.forEach((queryKey) => {
|
|
1469
|
+
if (isItemKeyForModel(modelName, queryKey)) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
hookQueryClient.setQueryData(queryKey, (oldData) => {
|
|
1473
|
+
const oldItems = Array.isArray(oldData) ? oldData : [];
|
|
1474
|
+
const hasItem = oldItems.some((entry) => (entry === null || entry === void 0 ? void 0 : entry.id) === id);
|
|
1475
|
+
if (hasItem) {
|
|
1476
|
+
return oldItems.map((entry) => (entry === null || entry === void 0 ? void 0 : entry.id) === id ? nextItem : entry);
|
|
1477
|
+
}
|
|
1478
|
+
if (queryKey.length === 1) {
|
|
1479
|
+
return [...oldItems, nextItem];
|
|
1480
|
+
}
|
|
1481
|
+
return oldItems;
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
hookQueryClient.setQueryData(singleItemQueryKey, null);
|
|
1487
|
+
relatedQueryKeys.forEach((queryKey) => {
|
|
1488
|
+
if (isItemKeyForModel(modelName, queryKey)) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
hookQueryClient.setQueryData(queryKey, (oldData) => {
|
|
1492
|
+
const oldItems = Array.isArray(oldData) ? oldData : [];
|
|
1493
|
+
return oldItems.filter((entry) => (entry === null || entry === void 0 ? void 0 : entry.id) !== id);
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
setIsSynced(Boolean(synced));
|
|
1498
|
+
},
|
|
1499
|
+
error: (err) => {
|
|
1500
|
+
console.error(`π¬ ${modelName} useItemHook realtime subscribe error:`, err);
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
return () => {
|
|
1504
|
+
var _a;
|
|
1505
|
+
isMounted = false;
|
|
1506
|
+
(_a = subscription === null || subscription === void 0 ? void 0 : subscription.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(subscription);
|
|
1507
|
+
};
|
|
1508
|
+
}, [
|
|
1509
|
+
realtimeEnabled,
|
|
1510
|
+
id,
|
|
1511
|
+
modelName,
|
|
1512
|
+
hookQueryClient,
|
|
1513
|
+
singleItemQueryKey,
|
|
1514
|
+
(_b = options === null || options === void 0 ? void 0 : options.realtime) === null || _b === void 0 ? void 0 : _b.observeOptions,
|
|
1515
|
+
]);
|
|
1397
1516
|
// useMutation hooks call service methods,
|
|
1398
1517
|
// Service methods handle optimistic updates and cache updates/rollbacks internally.
|
|
1399
1518
|
// Mutations inside useHook only serve to call service methods.
|
|
@@ -1463,6 +1582,7 @@ function createAmplifyService(modelName, defaultAuthMode) {
|
|
|
1463
1582
|
item: item || null,
|
|
1464
1583
|
isLoading: effectiveLoading, // Not loading if cached data exists
|
|
1465
1584
|
error: error, // Explicitly specify error type
|
|
1585
|
+
isSynced,
|
|
1466
1586
|
refresh: refreshItem,
|
|
1467
1587
|
update: updateItem,
|
|
1468
1588
|
delete: deleteItem,
|
package/dist/types.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export type ModelHook<T> = {
|
|
|
41
41
|
export type ItemHook<T> = {
|
|
42
42
|
item: T | null;
|
|
43
43
|
isLoading: boolean;
|
|
44
|
+
isSynced?: boolean;
|
|
44
45
|
error: Error | null;
|
|
45
46
|
refresh: () => Promise<T | null>;
|
|
46
47
|
update: (data: Partial<T>) => Promise<T | null>;
|
|
@@ -96,9 +97,15 @@ export interface AmplifyDataService<T> {
|
|
|
96
97
|
realtime?: {
|
|
97
98
|
enabled?: boolean;
|
|
98
99
|
observeOptions?: Record<string, any>;
|
|
100
|
+
events?: Array<"create" | "update" | "delete">;
|
|
99
101
|
};
|
|
100
102
|
}) => ModelHook<T>;
|
|
101
|
-
useItemHook: (id: string
|
|
103
|
+
useItemHook: (id: string, options?: {
|
|
104
|
+
realtime?: {
|
|
105
|
+
enabled?: boolean;
|
|
106
|
+
observeOptions?: Record<string, any>;
|
|
107
|
+
};
|
|
108
|
+
}) => ItemHook<T>;
|
|
102
109
|
modelName: string;
|
|
103
110
|
withExtensions: <E>(extensions: E) => AmplifyDataService<T> & E;
|
|
104
111
|
setAuthMode: (authMode: AuthMode) => void;
|