@aws-amplify/datastore 4.0.12 → 4.0.13-push-notification-dryrun.43

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.
Files changed (111) hide show
  1. package/lib/datastore/datastore.d.ts +29 -3
  2. package/lib/datastore/datastore.js +308 -147
  3. package/lib/datastore/datastore.js.map +1 -1
  4. package/lib/predicates/index.d.ts +77 -7
  5. package/lib/predicates/index.js +142 -122
  6. package/lib/predicates/index.js.map +1 -1
  7. package/lib/predicates/next.d.ts +51 -10
  8. package/lib/predicates/next.js +111 -91
  9. package/lib/predicates/next.js.map +1 -1
  10. package/lib/storage/adapter/AsyncStorageAdapter.d.ts +28 -30
  11. package/lib/storage/adapter/AsyncStorageAdapter.js +135 -532
  12. package/lib/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  13. package/lib/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  14. package/lib/storage/adapter/IndexedDBAdapter.d.ts +28 -29
  15. package/lib/storage/adapter/IndexedDBAdapter.js +490 -885
  16. package/lib/storage/adapter/IndexedDBAdapter.js.map +1 -1
  17. package/lib/storage/adapter/StorageAdapterBase.d.ts +134 -0
  18. package/lib/storage/adapter/StorageAdapterBase.js +439 -0
  19. package/lib/storage/adapter/StorageAdapterBase.js.map +1 -0
  20. package/lib/storage/relationship.d.ts +9 -0
  21. package/lib/storage/relationship.js +9 -0
  22. package/lib/storage/relationship.js.map +1 -1
  23. package/lib/storage/storage.d.ts +1 -1
  24. package/lib/storage/storage.js +4 -3
  25. package/lib/storage/storage.js.map +1 -1
  26. package/lib/sync/index.d.ts +15 -1
  27. package/lib/sync/index.js +80 -13
  28. package/lib/sync/index.js.map +1 -1
  29. package/lib/sync/outbox.js +14 -7
  30. package/lib/sync/outbox.js.map +1 -1
  31. package/lib/sync/processors/mutation.d.ts +10 -1
  32. package/lib/sync/processors/mutation.js +33 -12
  33. package/lib/sync/processors/mutation.js.map +1 -1
  34. package/lib/sync/processors/subscription.d.ts +7 -1
  35. package/lib/sync/processors/subscription.js +196 -135
  36. package/lib/sync/processors/subscription.js.map +1 -1
  37. package/lib/sync/processors/sync.d.ts +1 -1
  38. package/lib/sync/processors/sync.js.map +1 -1
  39. package/lib/sync/utils.d.ts +66 -2
  40. package/lib/sync/utils.js +264 -16
  41. package/lib/sync/utils.js.map +1 -1
  42. package/lib/types.d.ts +9 -1
  43. package/lib/types.js.map +1 -1
  44. package/lib/util.d.ts +16 -0
  45. package/lib/util.js +31 -2
  46. package/lib/util.js.map +1 -1
  47. package/lib-esm/datastore/datastore.d.ts +29 -3
  48. package/lib-esm/datastore/datastore.js +310 -149
  49. package/lib-esm/datastore/datastore.js.map +1 -1
  50. package/lib-esm/predicates/index.d.ts +77 -7
  51. package/lib-esm/predicates/index.js +143 -123
  52. package/lib-esm/predicates/index.js.map +1 -1
  53. package/lib-esm/predicates/next.d.ts +51 -10
  54. package/lib-esm/predicates/next.js +111 -91
  55. package/lib-esm/predicates/next.js.map +1 -1
  56. package/lib-esm/storage/adapter/AsyncStorageAdapter.d.ts +28 -30
  57. package/lib-esm/storage/adapter/AsyncStorageAdapter.js +138 -535
  58. package/lib-esm/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  59. package/lib-esm/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  60. package/lib-esm/storage/adapter/IndexedDBAdapter.d.ts +28 -29
  61. package/lib-esm/storage/adapter/IndexedDBAdapter.js +489 -884
  62. package/lib-esm/storage/adapter/IndexedDBAdapter.js.map +1 -1
  63. package/lib-esm/storage/adapter/StorageAdapterBase.d.ts +134 -0
  64. package/lib-esm/storage/adapter/StorageAdapterBase.js +437 -0
  65. package/lib-esm/storage/adapter/StorageAdapterBase.js.map +1 -0
  66. package/lib-esm/storage/relationship.d.ts +9 -0
  67. package/lib-esm/storage/relationship.js +9 -0
  68. package/lib-esm/storage/relationship.js.map +1 -1
  69. package/lib-esm/storage/storage.d.ts +1 -1
  70. package/lib-esm/storage/storage.js +4 -3
  71. package/lib-esm/storage/storage.js.map +1 -1
  72. package/lib-esm/sync/index.d.ts +15 -1
  73. package/lib-esm/sync/index.js +82 -15
  74. package/lib-esm/sync/index.js.map +1 -1
  75. package/lib-esm/sync/outbox.js +14 -7
  76. package/lib-esm/sync/outbox.js.map +1 -1
  77. package/lib-esm/sync/processors/mutation.d.ts +10 -1
  78. package/lib-esm/sync/processors/mutation.js +33 -12
  79. package/lib-esm/sync/processors/mutation.js.map +1 -1
  80. package/lib-esm/sync/processors/subscription.d.ts +7 -1
  81. package/lib-esm/sync/processors/subscription.js +197 -136
  82. package/lib-esm/sync/processors/subscription.js.map +1 -1
  83. package/lib-esm/sync/processors/sync.d.ts +1 -1
  84. package/lib-esm/sync/processors/sync.js.map +1 -1
  85. package/lib-esm/sync/utils.d.ts +66 -2
  86. package/lib-esm/sync/utils.js +261 -18
  87. package/lib-esm/sync/utils.js.map +1 -1
  88. package/lib-esm/types.d.ts +9 -1
  89. package/lib-esm/types.js.map +1 -1
  90. package/lib-esm/util.d.ts +16 -0
  91. package/lib-esm/util.js +32 -3
  92. package/lib-esm/util.js.map +1 -1
  93. package/package.json +12 -11
  94. package/src/datastore/datastore.ts +288 -159
  95. package/src/predicates/index.ts +145 -175
  96. package/src/predicates/next.ts +114 -81
  97. package/src/storage/adapter/AsyncStorageAdapter.ts +97 -563
  98. package/src/storage/adapter/AsyncStorageDatabase.ts +2 -2
  99. package/src/storage/adapter/IndexedDBAdapter.ts +318 -770
  100. package/src/storage/adapter/StorageAdapterBase.ts +545 -0
  101. package/src/storage/relationship.ts +9 -0
  102. package/src/storage/storage.ts +12 -9
  103. package/src/sync/index.ts +108 -20
  104. package/src/sync/outbox.ts +17 -11
  105. package/src/sync/processors/mutation.ts +35 -4
  106. package/src/sync/processors/subscription.ts +124 -10
  107. package/src/sync/processors/sync.ts +4 -1
  108. package/src/sync/utils.ts +285 -15
  109. package/src/types.ts +15 -2
  110. package/src/util.ts +40 -1
  111. package/CHANGELOG.md +0 -904
package/src/sync/index.ts CHANGED
@@ -2,8 +2,13 @@ import {
2
2
  browserOrNode,
3
3
  ConsoleLogger as Logger,
4
4
  BackgroundProcessManager,
5
+ Hub,
5
6
  } from '@aws-amplify/core';
6
- import { CONTROL_MSG as PUBSUB_CONTROL_MSG } from '@aws-amplify/pubsub';
7
+ import {
8
+ CONTROL_MSG as PUBSUB_CONTROL_MSG,
9
+ CONNECTION_STATE_CHANGE as PUBSUB_CONNECTION_STATE_CHANGE,
10
+ ConnectionState,
11
+ } from '@aws-amplify/pubsub';
7
12
  import Observable, { ZenObservable } from 'zen-observable-ts';
8
13
  import { ModelInstanceCreator } from '../datastore/datastore';
9
14
  import { ModelPredicateCreator } from '../predicates';
@@ -116,6 +121,13 @@ export class SyncEngine {
116
121
  PersistentModelConstructor<any>,
117
122
  boolean
118
123
  > = new WeakMap();
124
+ private unsleepSyncQueriesObservable: (() => void) | null;
125
+ private waitForSleepState: Promise<void>;
126
+ private syncQueriesObservableStartSleeping: (
127
+ value?: void | PromiseLike<void>
128
+ ) => void;
129
+ private stopDisruptionListener: () => void;
130
+ private connectionDisrupted = false;
119
131
 
120
132
  private runningProcesses: BackgroundProcessManager;
121
133
 
@@ -134,13 +146,19 @@ export class SyncEngine {
134
146
  private readonly modelInstanceCreator: ModelInstanceCreator,
135
147
  conflictHandler: ConflictHandler,
136
148
  errorHandler: ErrorHandler,
137
- private readonly syncPredicates: WeakMap<SchemaModel, ModelPredicate<any>>,
149
+ private readonly syncPredicates: WeakMap<
150
+ SchemaModel,
151
+ ModelPredicate<any> | null
152
+ >,
138
153
  private readonly amplifyConfig: Record<string, any> = {},
139
154
  private readonly authModeStrategy: AuthModeStrategy,
140
155
  private readonly amplifyContext: AmplifyContext,
141
156
  private readonly connectivityMonitor?: DataStoreConnectivity
142
157
  ) {
143
158
  this.runningProcesses = new BackgroundProcessManager();
159
+ this.waitForSleepState = new Promise(resolve => {
160
+ this.syncQueriesObservableStartSleeping = resolve;
161
+ });
144
162
 
145
163
  const MutationEvent = this.modelClasses[
146
164
  'MutationEvent'
@@ -234,6 +252,8 @@ export class SyncEngine {
234
252
  'Realtime disabled when in a server-side environment'
235
253
  );
236
254
  } else {
255
+ this.stopDisruptionListener =
256
+ this.startDisruptionListener();
237
257
  //#region GraphQL Subscriptions
238
258
  [ctlSubsObservable, dataSubsObservable] =
239
259
  this.subscriptionsProcessor.start();
@@ -448,6 +468,7 @@ export class SyncEngine {
448
468
 
449
469
  await startPromise;
450
470
 
471
+ // Set by the this.datastoreConnectivity.status().subscribe() loop
451
472
  if (this.online) {
452
473
  this.mutationsProcessor.resume();
453
474
  }
@@ -540,12 +561,12 @@ export class SyncEngine {
540
561
  );
541
562
  const paginatingModels = new Set(modelLastSync.keys());
542
563
 
543
- let newestFullSyncStartedAt: number;
544
- let theInterval: number;
564
+ let lastFullSyncStartedAt: number;
565
+ let syncInterval: number;
545
566
 
546
567
  let start: number;
547
- let duration: number;
548
- let newestStartedAt: number;
568
+ let syncDuration: number;
569
+ let lastStartedAt: number;
549
570
  await new Promise((resolve, reject) => {
550
571
  if (!this.runningProcesses.isOpen) resolve();
551
572
  onTerminate.then(() => resolve());
@@ -572,10 +593,10 @@ export class SyncEngine {
572
593
  });
573
594
 
574
595
  start = getNow();
575
- newestStartedAt =
576
- newestStartedAt === undefined
596
+ lastStartedAt =
597
+ lastStartedAt === undefined
577
598
  ? startedAt
578
- : Math.max(newestStartedAt, startedAt);
599
+ : Math.max(lastStartedAt, startedAt);
579
600
  }
580
601
 
581
602
  /**
@@ -655,13 +676,13 @@ export class SyncEngine {
655
676
 
656
677
  const { lastFullSync, fullSyncInterval } = modelMetadata;
657
678
 
658
- theInterval = fullSyncInterval;
679
+ syncInterval = fullSyncInterval;
659
680
 
660
- newestFullSyncStartedAt =
661
- newestFullSyncStartedAt === undefined
681
+ lastFullSyncStartedAt =
682
+ lastFullSyncStartedAt === undefined
662
683
  ? lastFullSync!
663
684
  : Math.max(
664
- newestFullSyncStartedAt,
685
+ lastFullSyncStartedAt,
665
686
  isFullSync ? startedAt : lastFullSync!
666
687
  );
667
688
 
@@ -699,7 +720,7 @@ export class SyncEngine {
699
720
  paginatingModels.delete(modelDefinition);
700
721
 
701
722
  if (paginatingModels.size === 0) {
702
- duration = getNow() - start;
723
+ syncDuration = getNow() - start;
703
724
  resolve();
704
725
  observer.next({
705
726
  type: ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY,
@@ -721,10 +742,19 @@ export class SyncEngine {
721
742
  });
722
743
  });
723
744
 
724
- const msNextFullSync =
725
- newestFullSyncStartedAt! +
726
- theInterval! -
727
- (newestStartedAt! + duration!);
745
+ // null is cast to 0 resulting in unexpected behavior.
746
+ // undefined in arithmetic operations results in NaN also resulting in unexpected behavior.
747
+ // If lastFullSyncStartedAt is null this is the first sync.
748
+ // Assume lastStartedAt is is also newest full sync.
749
+ let msNextFullSync;
750
+ if (!lastFullSyncStartedAt!) {
751
+ msNextFullSync = syncInterval! - syncDuration!;
752
+ } else {
753
+ msNextFullSync =
754
+ lastFullSyncStartedAt! +
755
+ syncInterval! -
756
+ (lastStartedAt! + syncDuration!);
757
+ }
728
758
 
729
759
  logger.debug(
730
760
  `Next fullSync in ${msNextFullSync / 1000} seconds. (${new Date(
@@ -756,11 +786,19 @@ export class SyncEngine {
756
786
 
757
787
  onTerminate.then(() => {
758
788
  terminated = true;
789
+ this.syncQueriesObservableStartSleeping();
759
790
  unsleep();
760
791
  });
761
792
 
793
+ this.unsleepSyncQueriesObservable = unsleep;
794
+ this.syncQueriesObservableStartSleeping();
762
795
  return sleep;
763
796
  }, 'syncQueriesObservable sleep');
797
+
798
+ this.unsleepSyncQueriesObservable = null;
799
+ this.waitForSleepState = new Promise(resolve => {
800
+ this.syncQueriesObservableStartSleeping = resolve;
801
+ });
764
802
  }
765
803
  }, 'syncQueriesObservable main');
766
804
  });
@@ -795,6 +833,11 @@ export class SyncEngine {
795
833
  */
796
834
  this.unsubscribeConnectivity();
797
835
 
836
+ /**
837
+ * Stop listening for websocket connection disruption
838
+ */
839
+ this.stopDisruptionListener && this.stopDisruptionListener();
840
+
798
841
  /**
799
842
  * aggressively shut down any lingering background processes.
800
843
  * some of this might be semi-redundant with unsubscribing. however,
@@ -906,9 +949,9 @@ export class SyncEngine {
906
949
  const ModelMetadata = this.modelClasses
907
950
  .ModelMetadata as PersistentModelConstructor<ModelMetadata>;
908
951
 
909
- const predicate = ModelPredicateCreator.createFromExisting<ModelMetadata>(
952
+ const predicate = ModelPredicateCreator.createFromAST<ModelMetadata>(
910
953
  this.schema.namespaces[SYNC].models[ModelMetadata.name],
911
- c => c.namespace('eq', namespace).model('eq', model)
954
+ { and: [{ namespace: { eq: namespace } }, { model: { eq: model } }] }
912
955
  );
913
956
 
914
957
  const [modelMetadata] = await this.storage.query(ModelMetadata, predicate, {
@@ -1040,4 +1083,49 @@ export class SyncEngine {
1040
1083
  };
1041
1084
  return namespace;
1042
1085
  }
1086
+
1087
+ /**
1088
+ * listen for websocket connection disruption
1089
+ *
1090
+ * May indicate there was a period of time where messages
1091
+ * from AppSync were missed. A sync needs to be triggered to
1092
+ * retrieve the missed data.
1093
+ */
1094
+ private startDisruptionListener() {
1095
+ return Hub.listen('api', (data: any) => {
1096
+ if (
1097
+ data.source === 'PubSub' &&
1098
+ data.payload.event === PUBSUB_CONNECTION_STATE_CHANGE
1099
+ ) {
1100
+ const connectionState = data.payload.data
1101
+ .connectionState as ConnectionState;
1102
+
1103
+ switch (connectionState) {
1104
+ // Do not need to listen for ConnectionDisruptedPendingNetwork
1105
+ // Normal network reconnection logic will handle the sync
1106
+ case ConnectionState.ConnectionDisrupted:
1107
+ this.connectionDisrupted = true;
1108
+ break;
1109
+
1110
+ case ConnectionState.Connected:
1111
+ if (this.connectionDisrupted) {
1112
+ this.scheduleSync();
1113
+ }
1114
+ this.connectionDisrupted = false;
1115
+ break;
1116
+ }
1117
+ }
1118
+ });
1119
+ }
1120
+
1121
+ /*
1122
+ * Schedule a sync to start when syncQueriesObservable enters sleep state
1123
+ * Start sync immediately if syncQueriesObservable is already in sleep state
1124
+ */
1125
+ private scheduleSync(): Promise<void> {
1126
+ return this.waitForSleepState.then(() => {
1127
+ // unsleepSyncQueriesObservable will be set if waitForSleepState has resolved
1128
+ this.unsleepSyncQueriesObservable!();
1129
+ });
1130
+ }
1043
1131
  }
@@ -38,12 +38,14 @@ class MutationEventOutbox {
38
38
 
39
39
  // `id` is the key for the record in the mutationEvent;
40
40
  // `modelId` is the key for the actual record that was mutated
41
- const predicate = ModelPredicateCreator.createFromExisting<MutationEvent>(
41
+ const predicate = ModelPredicateCreator.createFromAST<MutationEvent>(
42
42
  mutationEventModelDefinition,
43
- c =>
44
- c
45
- .modelId('eq', mutationEvent.modelId)
46
- .id('ne', this.inProgressMutationEventId)
43
+ {
44
+ and: [
45
+ { modelId: { eq: mutationEvent.modelId } },
46
+ { id: { ne: this.inProgressMutationEventId } },
47
+ ],
48
+ }
47
49
  );
48
50
 
49
51
  // Check if there are any other records with same id
@@ -138,10 +140,9 @@ class MutationEventOutbox {
138
140
 
139
141
  const mutationEvents = await storage.query(
140
142
  this.MutationEvent,
141
- ModelPredicateCreator.createFromExisting(
142
- mutationEventModelDefinition,
143
- c => c.modelId('eq', modelId)
144
- )
143
+ ModelPredicateCreator.createFromAST(mutationEventModelDefinition, {
144
+ and: { modelId: { eq: modelId } },
145
+ })
145
146
  );
146
147
 
147
148
  return mutationEvents;
@@ -201,9 +202,14 @@ class MutationEventOutbox {
201
202
 
202
203
  const recordId = getIdentifierValue(userModelDefinition, record);
203
204
 
204
- const predicate = ModelPredicateCreator.createFromExisting<MutationEvent>(
205
+ const predicate = ModelPredicateCreator.createFromAST<MutationEvent>(
205
206
  mutationEventModelDefinition,
206
- c => c.modelId('eq', recordId).id('ne', this.inProgressMutationEventId)
207
+ {
208
+ and: [
209
+ { modelId: { eq: recordId } },
210
+ { id: { ne: this.inProgressMutationEventId } },
211
+ ],
212
+ }
207
213
  );
208
214
 
209
215
  const outdatedMutations = await storage.query(
@@ -56,7 +56,15 @@ type MutationProcessorEvent = {
56
56
  };
57
57
 
58
58
  class MutationProcessor {
59
- private observer!: ZenObservable.Observer<MutationProcessorEvent>;
59
+ /**
60
+ * The observer that receives messages when mutations are successfully completed
61
+ * against cloud storage.
62
+ *
63
+ * A value of `undefined` signals that the sync has either been stopped or has not
64
+ * yet started. In this case, `isReady()` will be `false` and `resume()` will exit
65
+ * early.
66
+ */
67
+ private observer?: ZenObservable.Observer<MutationProcessorEvent>;
60
68
  private readonly typeQuery = new WeakMap<
61
69
  SchemaModel,
62
70
  [TransformerMutationType, string, string][]
@@ -130,6 +138,8 @@ class MutationProcessor {
130
138
  }
131
139
 
132
140
  return this.runningProcesses.addCleaner(async () => {
141
+ // The observer has unsubscribed and/or `stop()` has been called.
142
+ this.removeObserver();
133
143
  this.pause();
134
144
  });
135
145
  });
@@ -138,10 +148,16 @@ class MutationProcessor {
138
148
  }
139
149
 
140
150
  public async stop() {
151
+ this.removeObserver();
141
152
  await this.runningProcesses.close();
142
153
  await this.runningProcesses.open();
143
154
  }
144
155
 
156
+ public removeObserver() {
157
+ this.observer?.complete?.();
158
+ this.observer = undefined;
159
+ }
160
+
145
161
  public async resume(): Promise<void> {
146
162
  await (this.runningProcesses.isOpen &&
147
163
  this.runningProcesses.add(async onTerminate => {
@@ -195,7 +211,7 @@ class MutationProcessor {
195
211
  operation,
196
212
  data,
197
213
  condition,
198
- modelConstructor as any,
214
+ modelConstructor,
199
215
  this.MutationEvent,
200
216
  head,
201
217
  operationAuthModes[authModeAttempts],
@@ -256,7 +272,7 @@ class MutationProcessor {
256
272
  hasMore = (await this.outbox.peek(storage)) !== undefined;
257
273
  });
258
274
 
259
- this.observer.next!({
275
+ this.observer?.next?.({
260
276
  operation,
261
277
  modelDefinition,
262
278
  model: record,
@@ -484,6 +500,11 @@ class MutationProcessor {
484
500
  const modelDefinition = this.schema.namespaces[namespaceName].models[model];
485
501
  const { primaryKey } = this.schema.namespaces[namespaceName].keys![model];
486
502
 
503
+ const auth = modelDefinition.attributes?.find(a => a.type === 'auth');
504
+ const ownerFields: string[] = auth?.properties?.rules
505
+ .map(rule => rule.ownerField)
506
+ .filter(f => f) || ['owner'];
507
+
487
508
  const queriesTuples = this.typeQuery.get(modelDefinition);
488
509
 
489
510
  const [, opName, query] = queriesTuples!.find(
@@ -512,7 +533,17 @@ class MutationProcessor {
512
533
  mutationInput = {};
513
534
  const modelFields = Object.values(modelDefinition.fields);
514
535
 
515
- for (const { name, type, association } of modelFields) {
536
+ for (const { name, type, association, isReadOnly } of modelFields) {
537
+ // omit readonly fields. cloud storage doesn't need them and won't take them!
538
+ if (isReadOnly) {
539
+ continue;
540
+ }
541
+
542
+ // omit owner fields if it's `null`. cloud storage doesn't allow it.
543
+ if (ownerFields.includes(name) && parsedData[name] === null) {
544
+ continue;
545
+ }
546
+
516
547
  // model fields should be stripped out from the input
517
548
  if (isModelFieldType(type)) {
518
549
  // except for belongs to relations - we need to replace them with the correct foreign key(s)
@@ -28,6 +28,13 @@ import {
28
28
  getUserGroupsFromToken,
29
29
  TransformerMutationType,
30
30
  getTokenForCustomAuth,
31
+ predicateToGraphQLFilter,
32
+ dynamicAuthFields,
33
+ filterFields,
34
+ repeatedFieldInGroup,
35
+ countFilterCombinations,
36
+ RTFError,
37
+ generateRTFRemediation,
31
38
  } from '../utils';
32
39
  import { ModelPredicateCreator } from '../../predicates';
33
40
  import { validatePredicate, USER_AGENT_SUFFIX_DATASTORE } from '../../util';
@@ -65,7 +72,10 @@ class SubscriptionProcessor {
65
72
 
66
73
  constructor(
67
74
  private readonly schema: InternalSchema,
68
- private readonly syncPredicates: WeakMap<SchemaModel, ModelPredicate<any>>,
75
+ private readonly syncPredicates: WeakMap<
76
+ SchemaModel,
77
+ ModelPredicate<any> | null
78
+ >,
69
79
  private readonly amplifyConfig: Record<string, any> = {},
70
80
  private readonly authModeStrategy: AuthModeStrategy,
71
81
  private readonly errorHandler: ErrorHandler,
@@ -79,7 +89,8 @@ class SubscriptionProcessor {
79
89
  userCredentials: USER_CREDENTIALS,
80
90
  cognitoTokenPayload: { [field: string]: any } | undefined,
81
91
  oidcTokenPayload: { [field: string]: any } | undefined,
82
- authMode: GRAPHQL_AUTH_MODE
92
+ authMode: GRAPHQL_AUTH_MODE,
93
+ filterArg: boolean = false
83
94
  ): {
84
95
  opType: TransformerMutationType;
85
96
  opName: string;
@@ -105,7 +116,8 @@ class SubscriptionProcessor {
105
116
  model,
106
117
  transformerMutationType,
107
118
  isOwner,
108
- ownerField!
119
+ ownerField!,
120
+ filterArg
109
121
  );
110
122
  return { authMode, opType, opName, query, isOwner, ownerField, ownerValue };
111
123
  }
@@ -182,10 +194,18 @@ class SubscriptionProcessor {
182
194
  cognitoOwnerAuthRules.forEach(ownerAuthRule => {
183
195
  const ownerValue = cognitoTokenPayload[ownerAuthRule.identityClaim];
184
196
 
197
+ // AuthZ for "list of owners" is handled dynamically in the subscription auth request
198
+ // resolver. It doesn't rely on a subscription arg.
199
+ // Only pass a subscription arg for single owner auth
200
+ const singleOwner =
201
+ model.fields[ownerAuthRule.ownerField]?.isArray !== true;
202
+ const isOwnerArgRequired =
203
+ singleOwner && !ownerAuthRule.areSubscriptionsPublic;
204
+
185
205
  if (ownerValue) {
186
206
  ownerAuthInfo = {
187
207
  authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
188
- isOwner: ownerAuthRule.areSubscriptionsPublic ? false : true,
208
+ isOwner: isOwnerArgRequired,
189
209
  ownerField: ownerAuthRule.ownerField,
190
210
  ownerValue,
191
211
  };
@@ -209,10 +229,15 @@ class SubscriptionProcessor {
209
229
  oidcOwnerAuthRules.forEach(ownerAuthRule => {
210
230
  const ownerValue = oidcTokenPayload[ownerAuthRule.identityClaim];
211
231
 
232
+ const singleOwner =
233
+ model.fields[ownerAuthRule.ownerField]?.isArray !== true;
234
+ const isOwnerArgRequired =
235
+ singleOwner && !ownerAuthRule.areSubscriptionsPublic;
236
+
212
237
  if (ownerValue) {
213
238
  ownerAuthInfo = {
214
239
  authMode: GRAPHQL_AUTH_MODE.OPENID_CONNECT,
215
- isOwner: ownerAuthRule.areSubscriptionsPublic ? false : true,
240
+ isOwner: isOwnerArgRequired,
216
241
  ownerField: ownerAuthRule.ownerField,
217
242
  ownerValue,
218
243
  };
@@ -356,8 +381,20 @@ class SubscriptionProcessor {
356
381
  [TransformerMutationType.DELETE]: 0,
357
382
  };
358
383
 
359
- // Retry failed subscriptions with next auth mode (if available)
360
- const authModeRetry = async operation => {
384
+ const predicatesGroup = ModelPredicateCreator.getPredicates(
385
+ this.syncPredicates.get(modelDefinition)!,
386
+ false
387
+ );
388
+
389
+ const addFilterArg = predicatesGroup !== undefined;
390
+
391
+ // Retry subscriptions that failed for one of the following reasons:
392
+ // 1. unauthorized - retry with next auth mode (if available)
393
+ // 2. RTF error - retry without sending filter arg. (filtering will fall back to clientside)
394
+ const subscriptionRetry = async (
395
+ operation,
396
+ addFilter = addFilterArg
397
+ ) => {
361
398
  const {
362
399
  opType: transformerMutationType,
363
400
  opName,
@@ -373,7 +410,8 @@ class SubscriptionProcessor {
373
410
  userCredentials,
374
411
  cognitoTokenPayload,
375
412
  oidcTokenPayload,
376
- readAuthModes[operationAuthModeAttempts[operation]]
413
+ readAuthModes[operationAuthModeAttempts[operation]],
414
+ addFilter
377
415
  );
378
416
 
379
417
  const authToken = await getTokenForCustomAuth(
@@ -383,6 +421,11 @@ class SubscriptionProcessor {
383
421
 
384
422
  const variables = {};
385
423
 
424
+ if (addFilter && predicatesGroup) {
425
+ variables['filter'] =
426
+ predicateToGraphQLFilter(predicatesGroup);
427
+ }
428
+
386
429
  if (isOwner) {
387
430
  if (!ownerValue) {
388
431
  observer.error(
@@ -478,6 +521,33 @@ class SubscriptionProcessor {
478
521
  },
479
522
  } = subscriptionError;
480
523
 
524
+ const isRTFError =
525
+ // only attempt catch if a filter variable was added to the subscription query
526
+ addFilter &&
527
+ this.catchRTFError(
528
+ message,
529
+ modelDefinition,
530
+ predicatesGroup
531
+ );
532
+
533
+ // Catch RTF errors
534
+ if (isRTFError) {
535
+ // Unsubscribe and clear subscription array for model/operation
536
+ subscriptions[modelDefinition.name][
537
+ transformerMutationType
538
+ ].forEach(subscription =>
539
+ subscription.unsubscribe()
540
+ );
541
+
542
+ subscriptions[modelDefinition.name][
543
+ transformerMutationType
544
+ ] = [];
545
+
546
+ // retry subscription connection without filter
547
+ subscriptionRetry(operation, false);
548
+ return;
549
+ }
550
+
481
551
  if (
482
552
  message.includes(
483
553
  PUBSUB_CONTROL_MSG.REALTIME_SUBSCRIPTION_INIT_ERROR
@@ -523,10 +593,11 @@ class SubscriptionProcessor {
523
593
  ]
524
594
  }`
525
595
  );
526
- authModeRetry(operation);
596
+ subscriptionRetry(operation);
527
597
  return;
528
598
  }
529
599
  }
600
+
530
601
  logger.warn('subscriptionError', message);
531
602
 
532
603
  try {
@@ -586,7 +657,7 @@ class SubscriptionProcessor {
586
657
  );
587
658
  };
588
659
 
589
- operations.forEach(op => authModeRetry(op));
660
+ operations.forEach(op => subscriptionRetry(op));
590
661
  })
591
662
  );
592
663
  });
@@ -660,6 +731,49 @@ class SubscriptionProcessor {
660
731
  this.buffer = [];
661
732
  }
662
733
  }
734
+
735
+ /**
736
+ * @returns true if the service returned an RTF subscription error
737
+ * @remarks logs a warning with remediation instructions
738
+ *
739
+ */
740
+ private catchRTFError(
741
+ message: string,
742
+ modelDefinition: SchemaModel,
743
+ predicatesGroup: PredicatesGroup<any> | undefined
744
+ ): boolean {
745
+ const header =
746
+ 'Backend subscriptions filtering error.\n' +
747
+ 'Subscriptions filtering will be applied clientside.\n';
748
+
749
+ const messageErrorTypeMap = {
750
+ 'UnknownArgument: Unknown field argument filter': RTFError.UnknownField,
751
+ 'Filters exceed maximum attributes limit': RTFError.MaxAttributes,
752
+ 'Filters combination exceed maximum limit': RTFError.MaxCombinations,
753
+ 'filter uses same fieldName multiple time': RTFError.RepeatedFieldname,
754
+ "The variables input contains a field name 'not'": RTFError.NotGroup,
755
+ 'The variables input contains a field that is not defined for input object type':
756
+ RTFError.FieldNotInType,
757
+ };
758
+
759
+ const [_errorMsg, errorType] =
760
+ Object.entries(messageErrorTypeMap).find(([errorMsg]) =>
761
+ message.includes(errorMsg)
762
+ ) || [];
763
+
764
+ if (errorType !== undefined) {
765
+ const remediationMessage = generateRTFRemediation(
766
+ errorType,
767
+ modelDefinition,
768
+ predicatesGroup
769
+ );
770
+
771
+ logger.warn(`${header}\n${message}\n${remediationMessage}`);
772
+ return true;
773
+ }
774
+
775
+ return false;
776
+ }
663
777
  }
664
778
 
665
779
  export { SubscriptionProcessor };
@@ -45,7 +45,10 @@ class SyncProcessor {
45
45
 
46
46
  constructor(
47
47
  private readonly schema: InternalSchema,
48
- private readonly syncPredicates: WeakMap<SchemaModel, ModelPredicate<any>>,
48
+ private readonly syncPredicates: WeakMap<
49
+ SchemaModel,
50
+ ModelPredicate<any> | null
51
+ >,
49
52
  private readonly amplifyConfig: Record<string, any> = {},
50
53
  private readonly authModeStrategy: AuthModeStrategy,
51
54
  private readonly errorHandler: ErrorHandler,