@aws-amplify/datastore 3.12.6-next.36 → 3.12.6-next.44

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 (132) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/authModeStrategies/multiAuthStrategy.js.map +1 -1
  3. package/lib/datastore/datastore.d.ts +59 -8
  4. package/lib/datastore/datastore.js +640 -143
  5. package/lib/datastore/datastore.js.map +1 -1
  6. package/lib/index.d.ts +3 -2
  7. package/lib/index.js +4 -0
  8. package/lib/index.js.map +1 -1
  9. package/lib/predicates/index.d.ts +16 -2
  10. package/lib/predicates/index.js +127 -6
  11. package/lib/predicates/index.js.map +1 -1
  12. package/lib/predicates/next.d.ts +342 -0
  13. package/lib/predicates/next.js +801 -0
  14. package/lib/predicates/next.js.map +1 -0
  15. package/lib/predicates/sort.js +10 -4
  16. package/lib/predicates/sort.js.map +1 -1
  17. package/lib/storage/adapter/AsyncStorageAdapter.d.ts +2 -1
  18. package/lib/storage/adapter/AsyncStorageAdapter.js +106 -293
  19. package/lib/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  20. package/lib/storage/adapter/AsyncStorageDatabase.js +6 -5
  21. package/lib/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  22. package/lib/storage/adapter/InMemoryStore.d.ts +1 -1
  23. package/lib/storage/adapter/InMemoryStore.js.map +1 -1
  24. package/lib/storage/adapter/IndexedDBAdapter.d.ts +4 -2
  25. package/lib/storage/adapter/IndexedDBAdapter.js +223 -289
  26. package/lib/storage/adapter/IndexedDBAdapter.js.map +1 -1
  27. package/lib/storage/adapter/getDefaultAdapter/index.js.map +1 -1
  28. package/lib/storage/relationship.d.ts +140 -0
  29. package/lib/storage/relationship.js +335 -0
  30. package/lib/storage/relationship.js.map +1 -0
  31. package/lib/storage/storage.d.ts +7 -6
  32. package/lib/storage/storage.js +32 -16
  33. package/lib/storage/storage.js.map +1 -1
  34. package/lib/sync/datastoreConnectivity.js.map +1 -1
  35. package/lib/sync/index.js +2 -8
  36. package/lib/sync/index.js.map +1 -1
  37. package/lib/sync/merger.js.map +1 -1
  38. package/lib/sync/outbox.js.map +1 -1
  39. package/lib/sync/processors/errorMaps.js +1 -1
  40. package/lib/sync/processors/errorMaps.js.map +1 -1
  41. package/lib/sync/processors/mutation.js +9 -6
  42. package/lib/sync/processors/mutation.js.map +1 -1
  43. package/lib/sync/processors/subscription.js +3 -0
  44. package/lib/sync/processors/subscription.js.map +1 -1
  45. package/lib/sync/processors/sync.js.map +1 -1
  46. package/lib/sync/utils.d.ts +1 -1
  47. package/lib/sync/utils.js +30 -31
  48. package/lib/sync/utils.js.map +1 -1
  49. package/lib/types.d.ts +58 -6
  50. package/lib/types.js +6 -1
  51. package/lib/types.js.map +1 -1
  52. package/lib/util.d.ts +39 -6
  53. package/lib/util.js +174 -104
  54. package/lib/util.js.map +1 -1
  55. package/lib-esm/authModeStrategies/multiAuthStrategy.js.map +1 -1
  56. package/lib-esm/datastore/datastore.d.ts +59 -8
  57. package/lib-esm/datastore/datastore.js +642 -146
  58. package/lib-esm/datastore/datastore.js.map +1 -1
  59. package/lib-esm/index.d.ts +3 -2
  60. package/lib-esm/index.js +2 -1
  61. package/lib-esm/index.js.map +1 -1
  62. package/lib-esm/predicates/index.d.ts +16 -2
  63. package/lib-esm/predicates/index.js +128 -7
  64. package/lib-esm/predicates/index.js.map +1 -1
  65. package/lib-esm/predicates/next.d.ts +342 -0
  66. package/lib-esm/predicates/next.js +797 -0
  67. package/lib-esm/predicates/next.js.map +1 -0
  68. package/lib-esm/predicates/sort.js +10 -4
  69. package/lib-esm/predicates/sort.js.map +1 -1
  70. package/lib-esm/storage/adapter/AsyncStorageAdapter.d.ts +2 -1
  71. package/lib-esm/storage/adapter/AsyncStorageAdapter.js +108 -295
  72. package/lib-esm/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  73. package/lib-esm/storage/adapter/AsyncStorageDatabase.js +6 -5
  74. package/lib-esm/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  75. package/lib-esm/storage/adapter/InMemoryStore.d.ts +1 -1
  76. package/lib-esm/storage/adapter/InMemoryStore.js.map +1 -1
  77. package/lib-esm/storage/adapter/IndexedDBAdapter.d.ts +4 -2
  78. package/lib-esm/storage/adapter/IndexedDBAdapter.js +226 -292
  79. package/lib-esm/storage/adapter/IndexedDBAdapter.js.map +1 -1
  80. package/lib-esm/storage/adapter/getDefaultAdapter/index.js.map +1 -1
  81. package/lib-esm/storage/relationship.d.ts +140 -0
  82. package/lib-esm/storage/relationship.js +333 -0
  83. package/lib-esm/storage/relationship.js.map +1 -0
  84. package/lib-esm/storage/storage.d.ts +7 -6
  85. package/lib-esm/storage/storage.js +32 -16
  86. package/lib-esm/storage/storage.js.map +1 -1
  87. package/lib-esm/sync/datastoreConnectivity.js.map +1 -1
  88. package/lib-esm/sync/index.js +3 -9
  89. package/lib-esm/sync/index.js.map +1 -1
  90. package/lib-esm/sync/merger.js.map +1 -1
  91. package/lib-esm/sync/outbox.js.map +1 -1
  92. package/lib-esm/sync/processors/errorMaps.js +1 -1
  93. package/lib-esm/sync/processors/errorMaps.js.map +1 -1
  94. package/lib-esm/sync/processors/mutation.js +10 -7
  95. package/lib-esm/sync/processors/mutation.js.map +1 -1
  96. package/lib-esm/sync/processors/subscription.js +3 -0
  97. package/lib-esm/sync/processors/subscription.js.map +1 -1
  98. package/lib-esm/sync/processors/sync.js.map +1 -1
  99. package/lib-esm/sync/utils.d.ts +1 -1
  100. package/lib-esm/sync/utils.js +31 -32
  101. package/lib-esm/sync/utils.js.map +1 -1
  102. package/lib-esm/types.d.ts +58 -6
  103. package/lib-esm/types.js +6 -2
  104. package/lib-esm/types.js.map +1 -1
  105. package/lib-esm/util.d.ts +39 -6
  106. package/lib-esm/util.js +170 -104
  107. package/lib-esm/util.js.map +1 -1
  108. package/package.json +11 -9
  109. package/src/authModeStrategies/multiAuthStrategy.ts +1 -1
  110. package/src/datastore/datastore.ts +679 -203
  111. package/src/index.ts +4 -0
  112. package/src/predicates/index.ts +143 -17
  113. package/src/predicates/next.ts +1016 -0
  114. package/src/predicates/sort.ts +8 -2
  115. package/src/storage/adapter/AsyncStorageAdapter.ts +56 -178
  116. package/src/storage/adapter/AsyncStorageDatabase.ts +16 -15
  117. package/src/storage/adapter/InMemoryStore.ts +5 -2
  118. package/src/storage/adapter/IndexedDBAdapter.ts +166 -191
  119. package/src/storage/adapter/getDefaultAdapter/index.ts +2 -2
  120. package/src/storage/relationship.ts +272 -0
  121. package/src/storage/storage.ts +56 -37
  122. package/src/sync/datastoreConnectivity.ts +4 -4
  123. package/src/sync/index.ts +22 -28
  124. package/src/sync/merger.ts +1 -1
  125. package/src/sync/outbox.ts +6 -6
  126. package/src/sync/processors/errorMaps.ts +1 -1
  127. package/src/sync/processors/mutation.ts +22 -17
  128. package/src/sync/processors/subscription.ts +18 -14
  129. package/src/sync/processors/sync.ts +16 -16
  130. package/src/sync/utils.ts +42 -48
  131. package/src/types.ts +115 -10
  132. package/src/util.ts +108 -150
@@ -0,0 +1,1016 @@
1
+ import {
2
+ Scalar,
3
+ PersistentModel,
4
+ ModelFieldType,
5
+ ModelMeta,
6
+ AllOperators,
7
+ PredicateFieldType,
8
+ ModelPredicate as StoragePredicate,
9
+ } from '../types';
10
+
11
+ import {
12
+ ModelPredicateCreator as FlatModelPredicateCreator,
13
+ comparisonKeys,
14
+ } from './index';
15
+ import { ExclusiveStorage as StorageAdapter } from '../storage/storage';
16
+ import { ModelRelationship } from '../storage/relationship';
17
+ import { asyncSome, asyncEvery, asyncFilter } from '../util';
18
+
19
+ type MatchableTypes =
20
+ | string
21
+ | string[]
22
+ | number
23
+ | number[]
24
+ | boolean
25
+ | boolean[];
26
+
27
+ type AllFieldOperators = keyof AllOperators;
28
+
29
+ const ops = [...comparisonKeys] as AllFieldOperators[];
30
+
31
+ type NonNeverKeys<T> = {
32
+ [K in keyof T]: T[K] extends never ? never : K;
33
+ }[keyof T];
34
+
35
+ type WithoutNevers<T> = Pick<T, NonNeverKeys<T>>;
36
+
37
+ /**
38
+ * A function that accepts a RecursiveModelPrecicate<T>, which it must use to
39
+ * return a final condition.
40
+ *
41
+ * This is used in `DataStore.query()`, `DataStore.observe()`, and
42
+ * `DataStore.observeQuery()` as the second argument. E.g.,
43
+ *
44
+ * ```
45
+ * DataStore.query(MyModel, model => model.field.eq('some value'))
46
+ * ```
47
+ *
48
+ * More complex queries should also be supported. E.g.,
49
+ *
50
+ * ```
51
+ * DataStore.query(MyModel, model => model.and(m => [
52
+ * m.relatedEntity.or(relative => [
53
+ * relative.relativeField.eq('whatever'),
54
+ * relative.relativeField.eq('whatever else')
55
+ * ]),
56
+ * m.myModelField.ne('something')
57
+ * ]))
58
+ * ```
59
+ */
60
+ export type RecursiveModelPredicateExtender<RT extends PersistentModel> = (
61
+ lambda: RecursiveModelPredicate<RT>
62
+ ) => {
63
+ /**
64
+ * @private
65
+ *
66
+ * DataStore internal
67
+ */
68
+ __query: GroupCondition;
69
+ };
70
+
71
+ export type RecursiveModelPredicateAggregateExtender<
72
+ RT extends PersistentModel
73
+ > = (lambda: RecursiveModelPredicate<RT>) => {
74
+ /**
75
+ * @private
76
+ *
77
+ * DataStore internal
78
+ */
79
+ __query: GroupCondition;
80
+ }[];
81
+
82
+ /**
83
+ * A function that accepts a ModelPrecicate<T>, which it must use to return a
84
+ * final condition.
85
+ *
86
+ * This is used as predicates in `DataStore.save()`, `DataStore.delete()`, and
87
+ * DataStore sync expressions.
88
+ *
89
+ * ```
90
+ * DataStore.save(record, model => model.field.eq('some value'))
91
+ * ```
92
+ *
93
+ * Logical operators are supported. But, condtiions are related records are
94
+ * NOT supported. E.g.,
95
+ *
96
+ * ```
97
+ * DataStore.delete(record, model => model.or(m => [
98
+ * m.field.eq('whatever'),
99
+ * m.field.eq('whatever else')
100
+ * ]))
101
+ * ```
102
+ */
103
+ export type ModelPredicateExtender<RT extends PersistentModel> = (
104
+ lambda: ModelPredicate<RT>
105
+ ) => {
106
+ /**
107
+ * @private
108
+ *
109
+ * DataStore internal
110
+ */
111
+ __query: GroupCondition;
112
+ };
113
+
114
+ export type ModelPredicateAggregateExtender<RT extends PersistentModel> = (
115
+ lambda: ModelPredicate<RT>
116
+ ) => {
117
+ /**
118
+ * @private
119
+ *
120
+ * DataStore internal
121
+ */
122
+ __query: GroupCondition;
123
+ }[];
124
+
125
+ type ValuePredicate<RT extends PersistentModel, MT extends MatchableTypes> = {
126
+ [K in AllFieldOperators]: K extends 'between'
127
+ ? (
128
+ inclusiveLowerBound: Scalar<MT>,
129
+ inclusiveUpperBound: Scalar<MT>
130
+ ) => ModelPredicateLeaf
131
+ : (operand: Scalar<MT>) => ModelPredicateLeaf;
132
+ };
133
+
134
+ type RecursiveModelPredicateOperator<RT extends PersistentModel> = (
135
+ ...predicates:
136
+ | [RecursiveModelPredicateAggregateExtender<RT>]
137
+ | ModelPredicateLeaf[]
138
+ ) => ModelPredicateLeaf;
139
+
140
+ type RecursiveModelPredicateNegation<RT extends PersistentModel> = (
141
+ predicate: RecursiveModelPredicateExtender<RT> | ModelPredicateLeaf
142
+ ) => ModelPredicateLeaf;
143
+
144
+ type ModelPredicateOperator<RT extends PersistentModel> = (
145
+ ...predicates: [ModelPredicateAggregateExtender<RT>] | ModelPredicateLeaf[]
146
+ ) => ModelPredicateLeaf;
147
+
148
+ type ModelPredicateNegation<RT extends PersistentModel> = (
149
+ predicate: ModelPredicateExtender<RT> | ModelPredicateLeaf
150
+ ) => ModelPredicateLeaf;
151
+
152
+ export type RecursiveModelPredicate<RT extends PersistentModel> = {
153
+ [K in keyof RT]-?: PredicateFieldType<RT[K]> extends PersistentModel
154
+ ? RecursiveModelPredicate<PredicateFieldType<RT[K]>>
155
+ : ValuePredicate<RT, RT[K]>;
156
+ } & {
157
+ or: RecursiveModelPredicateOperator<RT>;
158
+ and: RecursiveModelPredicateOperator<RT>;
159
+ not: RecursiveModelPredicateNegation<RT>;
160
+ /**
161
+ * @private
162
+ *
163
+ * DataStore internal
164
+ */
165
+ __copy: () => RecursiveModelPredicate<RT>;
166
+ } & ModelPredicateLeaf;
167
+
168
+ export type ModelPredicate<RT extends PersistentModel> = WithoutNevers<{
169
+ [K in keyof RT]-?: PredicateFieldType<RT[K]> extends PersistentModel
170
+ ? never
171
+ : ValuePredicate<RT, RT[K]>;
172
+ }> & {
173
+ or: ModelPredicateOperator<RT>;
174
+ and: ModelPredicateOperator<RT>;
175
+ not: ModelPredicateNegation<RT>;
176
+
177
+ /**
178
+ * @private
179
+ *
180
+ * DataStore internal
181
+ */
182
+ __copy: () => ModelPredicate<RT>;
183
+ } & ModelPredicateLeaf;
184
+
185
+ export type ModelPredicateLeaf = {
186
+ /**
187
+ * @private
188
+ *
189
+ * DataStore internal
190
+ */
191
+ __query: GroupCondition;
192
+
193
+ /**
194
+ * @private
195
+ *
196
+ * DataStore internal
197
+ */
198
+ __tail: GroupCondition;
199
+
200
+ /**
201
+ * @private
202
+ *
203
+ * DataStore internal
204
+ */
205
+ filter: <T>(items: T[]) => Promise<T[]>;
206
+ };
207
+
208
+ type GroupOperator = 'and' | 'or' | 'not';
209
+
210
+ type UntypedCondition = {
211
+ fetch: (storage: StorageAdapter) => Promise<Record<string, any>[]>;
212
+ matches: (item: Record<string, any>) => Promise<boolean>;
213
+ copy(extract: GroupCondition): [UntypedCondition, GroupCondition | undefined];
214
+ toAST(): any;
215
+ };
216
+
217
+ /**
218
+ * Maps operators to negated operators.
219
+ * Used to facilitate propagation of negation down a tree of conditions.
220
+ */
221
+ const negations = {
222
+ and: 'or',
223
+ or: 'and',
224
+ not: 'and',
225
+ eq: 'ne',
226
+ ne: 'eq',
227
+ gt: 'le',
228
+ ge: 'lt',
229
+ lt: 'ge',
230
+ le: 'gt',
231
+ contains: 'notContains',
232
+ notContains: 'contains',
233
+ };
234
+
235
+ /**
236
+ * Given a V1 predicate "seed", applies a list of V2 field-level conditions
237
+ * to the predicate, returning a new/final V1 predicate chain link.
238
+ * @param predicate The base/seed V1 predicate to build on
239
+ * @param conditions The V2 conditions to add to the predicate chain.
240
+ * @param negateChildren Whether the conditions should be negated first.
241
+ * @returns A V1 predicate, with conditions incorporated.
242
+ */
243
+ function applyConditionsToV1Predicate<T>(
244
+ predicate: T,
245
+ conditions: FieldCondition[],
246
+ negateChildren: boolean
247
+ ): T {
248
+ let p = predicate;
249
+ const finalConditions: FieldCondition[] = [];
250
+
251
+ for (const c of conditions) {
252
+ if (negateChildren) {
253
+ if (c.operator === 'between') {
254
+ finalConditions.push(
255
+ new FieldCondition(c.field, 'lt', [c.operands[0]]),
256
+ new FieldCondition(c.field, 'gt', [c.operands[1]])
257
+ );
258
+ } else {
259
+ finalConditions.push(
260
+ new FieldCondition(c.field, negations[c.operator], c.operands)
261
+ );
262
+ }
263
+ } else {
264
+ finalConditions.push(c);
265
+ }
266
+ }
267
+
268
+ for (const c of finalConditions) {
269
+ p = p[c.field](
270
+ c.operator as never,
271
+ (c.operator === 'between' ? c.operands : c.operands[0]) as never
272
+ );
273
+ }
274
+ return p;
275
+ }
276
+
277
+ /**
278
+ * A condition that can operate against a single "primitive" field of a model or item.
279
+ * @member field The field of *some record* to test against.
280
+ * @member operator The equality or comparison operator to use.
281
+ * @member operands The operands for the equality/comparison check.
282
+ */
283
+ export class FieldCondition {
284
+ constructor(
285
+ public field: string,
286
+ public operator: string,
287
+ public operands: string[]
288
+ ) {
289
+ this.validate();
290
+ }
291
+
292
+ /**
293
+ * Creates a copy of self.
294
+ * @param extract Not used. Present only to fulfill the `UntypedCondition` interface.
295
+ * @returns A new, identitical `FieldCondition`.
296
+ */
297
+ copy(extract: GroupCondition): [FieldCondition, GroupCondition | undefined] {
298
+ return [
299
+ new FieldCondition(this.field, this.operator, [...this.operands]),
300
+ undefined,
301
+ ];
302
+ }
303
+
304
+ toAST() {
305
+ return {
306
+ [this.field]: {
307
+ [this.operator]:
308
+ this.operator === 'between'
309
+ ? [this.operands[0], this.operands[1]]
310
+ : this.operands[0],
311
+ },
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Not implemented. Not needed. GroupCondition instead consumes FieldConditions and
317
+ * transforms them into legacy predicates. (*For now.*)
318
+ * @param storage N/A. If ever implemented, the storage adapter to query.
319
+ * @returns N/A. If ever implemented, return items from `storage` that match.
320
+ */
321
+ async fetch(storage: StorageAdapter): Promise<Record<string, any>[]> {
322
+ return Promise.reject('No implementation needed [yet].');
323
+ }
324
+
325
+ /**
326
+ * Determins whether a given item matches the expressed condition.
327
+ * @param item The item to test.
328
+ * @returns `Promise<boolean>`, `true` if matches; `false` otherwise.
329
+ */
330
+ async matches(item: Record<string, any>): Promise<boolean> {
331
+ const v = String(item[this.field]);
332
+ const operations = {
333
+ eq: () => v === this.operands[0],
334
+ ne: () => v !== this.operands[0],
335
+ gt: () => v > this.operands[0],
336
+ ge: () => v >= this.operands[0],
337
+ lt: () => v < this.operands[0],
338
+ le: () => v <= this.operands[0],
339
+ contains: () => v.indexOf(this.operands[0]) > -1,
340
+ notContains: () => v.indexOf(this.operands[0]) === -1,
341
+ beginsWith: () => v.startsWith(this.operands[0]),
342
+ between: () => v >= this.operands[0] && v <= this.operands[1],
343
+ };
344
+ const operation = operations[this.operator as keyof typeof operations];
345
+ if (operation) {
346
+ return operation();
347
+ } else {
348
+ throw new Error(`Invalid operator given: ${this.operator}`);
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Checks `this.operands` for compatibility with `this.operator`.
354
+ */
355
+ validate(): void {
356
+ /**
357
+ * Creates a validator that checks for a particular `operands` count.
358
+ * Throws an exception if the `count` disagrees with `operands.length`.
359
+ * @param count The number of `operands` expected.
360
+ */
361
+ const argumentCount = count => {
362
+ const argsClause = count === 1 ? 'argument is' : 'arguments are';
363
+ return () => {
364
+ if (this.operands.length !== count) {
365
+ return `Exactly ${count} ${argsClause} required.`;
366
+ }
367
+ };
368
+ };
369
+
370
+ // NOTE: validations should return a message on failure.
371
+ // hence, they should be "joined" together with logical OR's
372
+ // as seen in the `between:` entry.
373
+ const validations = {
374
+ eq: argumentCount(1),
375
+ ne: argumentCount(1),
376
+ gt: argumentCount(1),
377
+ ge: argumentCount(1),
378
+ lt: argumentCount(1),
379
+ le: argumentCount(1),
380
+ contains: argumentCount(1),
381
+ notContains: argumentCount(1),
382
+ beginsWith: argumentCount(1),
383
+ between: () =>
384
+ argumentCount(2)() ||
385
+ (this.operands[0] > this.operands[1]
386
+ ? 'The first argument must be less than or equal to the second argument.'
387
+ : null),
388
+ };
389
+ const validate = validations[this.operator as keyof typeof validations];
390
+ if (validate) {
391
+ const e = validate();
392
+ if (typeof e === 'string')
393
+ throw new Error(`Incorrect usage of \`${this.operator}()\`: ${e}`);
394
+ } else {
395
+ throw new Error(`Non-existent operator: \`${this.operator}()\``);
396
+ }
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Small utility function to generate a monotonically increasing ID.
402
+ * Used by GroupCondition to help keep track of which group is doing what,
403
+ * when, and where during troubleshooting.
404
+ */
405
+ const getGroupId = (() => {
406
+ let seed = 1;
407
+ return () => `group_${seed++}`;
408
+ })();
409
+
410
+ /**
411
+ * A set of sub-conditions to operate against a model, optionally scoped to
412
+ * a specific field, combined with the given operator (one of `and`, `or`, or `not`).
413
+ * @member groupId Used to distinguish between GroupCondition instances for
414
+ * debugging and troublehsooting.
415
+ * @member model A metadata object that tells GroupCondition what to query and how.
416
+ * @member field The field on the model that the sub-conditions apply to.
417
+ * @member operator How to group child conditions together.
418
+ * @member operands The child conditions.
419
+ */
420
+ export class GroupCondition {
421
+ // `groupId` was used for development/debugging.
422
+ // Should we leave this in for future troubleshooting?
423
+ public groupId = getGroupId();
424
+
425
+ constructor(
426
+ /**
427
+ * The `ModelMeta` of the model to query and/or filter against.
428
+ * Expected to contain:
429
+ *
430
+ * ```js
431
+ * {
432
+ * builder: ModelConstructor,
433
+ * schema: SchemaModel,
434
+ * pkField: string[]
435
+ * }
436
+ * ```
437
+ */
438
+ public model: ModelMeta<any>,
439
+
440
+ /**
441
+ * If populated, this group specifices a condition on a relationship.
442
+ *
443
+ * If `field` does *not* point to a related model, that's an error. It
444
+ * could indicate that the `GroupCondition` was instantiated with bad
445
+ * data, or that the model metadata is incorrect.
446
+ */
447
+ public field: string | undefined,
448
+
449
+ /**
450
+ * If a `field` is given, whether the relationship is a `HAS_ONE`,
451
+ * 'HAS_MANY`, or `BELONGS_TO`.
452
+ *
453
+ * TODO: Remove this and replace with derivation using
454
+ * `ModelRelationship.from(this.model, this.field).relationship`;
455
+ */
456
+ public relationshipType: string | undefined,
457
+
458
+ /**
459
+ *
460
+ */
461
+ public operator: GroupOperator,
462
+
463
+ /**
464
+ *
465
+ */
466
+ public operands: UntypedCondition[]
467
+ ) {}
468
+
469
+ /**
470
+ * Returns a copy of a GroupCondition, which also returns the copy of a
471
+ * given reference node to "extract".
472
+ * @param extract A node of interest. Its copy will *also* be returned if the node exists.
473
+ * @returns [The full copy, the copy of `extract` | undefined]
474
+ */
475
+ copy(extract: GroupCondition): [GroupCondition, GroupCondition | undefined] {
476
+ const copied = new GroupCondition(
477
+ this.model,
478
+ this.field,
479
+ this.relationshipType,
480
+ this.operator,
481
+ []
482
+ );
483
+
484
+ let extractedCopy: GroupCondition | undefined =
485
+ extract === this ? copied : undefined;
486
+
487
+ this.operands.forEach(o => {
488
+ const [operandCopy, extractedFromOperand] = o.copy(extract);
489
+ copied.operands.push(operandCopy);
490
+ extractedCopy = extractedCopy || extractedFromOperand;
491
+ });
492
+
493
+ return [copied, extractedCopy];
494
+ }
495
+
496
+ /**
497
+ * Fetches matching records from a given storage adapter using legacy predicates (for now).
498
+ * @param storage The storage adapter this predicate will query against.
499
+ * @param breadcrumb For debugging/troubleshooting. A list of the `groupId`'s this
500
+ * GroupdCondition.fetch is nested within.
501
+ * @param negate Whether to match on the `NOT` of `this`.
502
+ * @returns An `Promise` of `any[]` from `storage` matching the child conditions.
503
+ */
504
+ async fetch(
505
+ storage: StorageAdapter,
506
+ breadcrumb: string[] = [],
507
+ negate = false
508
+ ): Promise<Record<string, any>[]> {
509
+ const resultGroups: Array<Record<string, any>[]> = [];
510
+
511
+ const operator = (negate ? negations[this.operator] : this.operator) as
512
+ | 'or'
513
+ | 'and'
514
+ | 'not';
515
+
516
+ const negateChildren = negate !== (this.operator === 'not');
517
+
518
+ /**
519
+ * Conditions that must be branched out and used to generate a base, "candidate"
520
+ * result set.
521
+ *
522
+ * If `field` is populated, these groups select *related* records, and the base,
523
+ * candidate results are selected to match those.
524
+ */
525
+ const groups = this.operands.filter(
526
+ op => op instanceof GroupCondition
527
+ ) as GroupCondition[];
528
+
529
+ /**
530
+ * Simple conditions that must match the target model of `this`.
531
+ */
532
+ const conditions = this.operands.filter(
533
+ op => op instanceof FieldCondition
534
+ ) as FieldCondition[];
535
+
536
+ for (const g of groups) {
537
+ const relatives = await g.fetch(
538
+ storage,
539
+ [...breadcrumb, this.groupId],
540
+ negateChildren
541
+ );
542
+
543
+ // no relatives -> no need to attempt to perform a "join" query for
544
+ // candidate results:
545
+ //
546
+ // select a.* from a,b where b.id in EMPTY_SET ==> EMPTY_SET
547
+ //
548
+ // Additionally, the entire (sub)-query can be short-circuited if
549
+ // the operator is `AND`. Illustrated in SQL:
550
+ //
551
+ // select a.* from a where
552
+ // id in [a,b,c]
553
+ // AND <
554
+ // id in EMTPY_SET <<< Look!
555
+ // AND <
556
+ // id in [x,y,z]
557
+ //
558
+ // YIELDS: EMPTY_SET // <-- Easy peasy. Lemon squeezy.
559
+ //
560
+ if (relatives.length === 0) {
561
+ // aggressively short-circuit as soon as we know the group condition will fail
562
+ if (operator === 'and') {
563
+ return [];
564
+ }
565
+
566
+ // less aggressive short-circuit if we know the relatives will produce no
567
+ // candidate results; but aren't sure yet how this affects the group condition.
568
+ resultGroups.push([]);
569
+ continue;
570
+ }
571
+
572
+ if (g.field) {
573
+ // `relatives` are actual relatives. We'll skim them for FK query values.
574
+ // Use the relatives to add candidate result sets (`resultGroups`)
575
+
576
+ const relationship = ModelRelationship.from(this.model, g.field);
577
+
578
+ if (relationship) {
579
+ const relativesPredicates: ((
580
+ p: RecursiveModelPredicate<any>
581
+ ) => RecursiveModelPredicate<any>)[] = [];
582
+ for (const relative of relatives) {
583
+ const individualRowJoinConditions: FieldCondition[] = [];
584
+
585
+ for (let i = 0; i < relationship.localJoinFields.length; i++) {
586
+ // rightHandValue
587
+ individualRowJoinConditions.push(
588
+ new FieldCondition(relationship.localJoinFields[i], 'eq', [
589
+ relative[relationship.remoteJoinFields[i]],
590
+ ])
591
+ );
592
+ }
593
+
594
+ const predicate = p =>
595
+ applyConditionsToV1Predicate(
596
+ p,
597
+ individualRowJoinConditions,
598
+ negateChildren
599
+ );
600
+ relativesPredicates.push(predicate as any);
601
+ }
602
+
603
+ const predicate = FlatModelPredicateCreator.createGroupFromExisting(
604
+ this.model.schema,
605
+ 'or',
606
+ relativesPredicates as any
607
+ );
608
+
609
+ resultGroups.push(
610
+ await storage.query(this.model.builder, predicate as any)
611
+ );
612
+ } else {
613
+ throw new Error('Missing field metadata.');
614
+ }
615
+ } else {
616
+ // relatives are not actually relatives. they're candidate results.
617
+ resultGroups.push(relatives);
618
+ }
619
+ }
620
+
621
+ // if conditions is empty at this point, child predicates found no matches.
622
+ // i.e., we can stop looking and return empty.
623
+ if (conditions.length > 0) {
624
+ const predicate = FlatModelPredicateCreator.createFromExisting(
625
+ this.model.schema,
626
+ p =>
627
+ p[operator](c =>
628
+ applyConditionsToV1Predicate(c, conditions, negateChildren)
629
+ )
630
+ );
631
+ resultGroups.push(
632
+ await storage.query(this.model.builder, predicate as any)
633
+ );
634
+ } else if (conditions.length === 0 && resultGroups.length === 0) {
635
+ resultGroups.push(await storage.query(this.model.builder));
636
+ }
637
+
638
+ // PK might be a single field, like `id`, or it might be several fields.
639
+ // so, we'll need to extract the list of PK fields from an object
640
+ // and stringify the list for easy comparison / merging.
641
+ const getPKValue = item =>
642
+ JSON.stringify(this.model.pkField.map(name => item[name]));
643
+
644
+ // will be used for intersecting or unioning results
645
+ let resultIndex: Map<string, Record<string, any>> | undefined;
646
+
647
+ if (operator === 'and') {
648
+ if (resultGroups.length === 0) {
649
+ return [];
650
+ }
651
+
652
+ // for each group, we intersect, removing items from the result index
653
+ // that aren't present in each subsequent group.
654
+ for (const group of resultGroups) {
655
+ if (resultIndex === undefined) {
656
+ resultIndex = new Map(group.map(item => [getPKValue(item), item]));
657
+ } else {
658
+ const intersectWith = new Map<string, Record<string, any>>(
659
+ group.map(item => [getPKValue(item), item])
660
+ );
661
+ for (const k of resultIndex.keys()) {
662
+ if (!intersectWith.has(k)) {
663
+ resultIndex.delete(k);
664
+ }
665
+ }
666
+ }
667
+ }
668
+ } else if (operator === 'or' || operator === 'not') {
669
+ // it's OK to handle NOT here, because NOT must always only negate
670
+ // a single child predicate. NOT logic will have been distributed down
671
+ // to the leaf conditions already.
672
+
673
+ resultIndex = new Map();
674
+
675
+ // just merge the groups, performing DISTINCT-ification by ID.
676
+ for (const group of resultGroups) {
677
+ for (const item of group) {
678
+ resultIndex.set(getPKValue(item), item);
679
+ }
680
+ }
681
+ }
682
+
683
+ return Array.from(resultIndex?.values() || []);
684
+ }
685
+
686
+ /**
687
+ * Determines whether a single item matches the conditions of `this`.
688
+ * When checking the target `item`'s properties, each property will be `await`'d
689
+ * to ensure lazy-loading is respected where applicable.
690
+ * @param item The item to match against.
691
+ * @param ignoreFieldName Tells `match()` that the field name has already been dereferenced.
692
+ * (Used for iterating over children on HAS_MANY checks.)
693
+ * @returns A boolean (promise): `true` if matched, `false` otherwise.
694
+ */
695
+ async matches(
696
+ item: Record<string, any>,
697
+ ignoreFieldName: boolean = false
698
+ ): Promise<boolean> {
699
+ const itemToCheck =
700
+ this.field && !ignoreFieldName ? await item[this.field] : item;
701
+
702
+ // if there is no item to check, we can stop recursing immediately.
703
+ // a condition cannot match against an item that does not exist. this
704
+ // can occur when `item.field` is optional in the schema.
705
+ if (!itemToCheck) {
706
+ return false;
707
+ }
708
+
709
+ if (
710
+ this.relationshipType === 'HAS_MANY' &&
711
+ typeof itemToCheck[Symbol.asyncIterator] === 'function'
712
+ ) {
713
+ for await (const singleItem of itemToCheck) {
714
+ if (await this.matches(singleItem, true)) {
715
+ return true;
716
+ }
717
+ }
718
+ return false;
719
+ }
720
+
721
+ if (this.operator === 'or') {
722
+ return asyncSome(this.operands, c => c.matches(itemToCheck));
723
+ } else if (this.operator === 'and') {
724
+ return asyncEvery(this.operands, c => c.matches(itemToCheck));
725
+ } else if (this.operator === 'not') {
726
+ if (this.operands.length !== 1) {
727
+ throw new Error(
728
+ 'Invalid arguments! `not()` accepts exactly one predicate expression.'
729
+ );
730
+ }
731
+ return !(await this.operands[0].matches(itemToCheck));
732
+ } else {
733
+ throw new Error('Invalid group operator!');
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Tranfsorm to a AppSync GraphQL compatible AST.
739
+ * (Does not support filtering in nested types.)
740
+ */
741
+ toAST() {
742
+ if (this.field)
743
+ throw new Error('Nested type conditions are not supported!');
744
+
745
+ return {
746
+ [this.operator]: this.operands.map(operand => operand.toAST()),
747
+ };
748
+ }
749
+
750
+ toStoragePredicate<T>(
751
+ baseCondition?: StoragePredicate<T>
752
+ ): StoragePredicate<T> {
753
+ return FlatModelPredicateCreator.createFromAST(
754
+ this.model.schema,
755
+ this.toAST()
756
+ ) as unknown as StoragePredicate<T>;
757
+ }
758
+ }
759
+
760
+ /**
761
+ * Creates a "seed" predicate that can be used to build an executable condition.
762
+ * This is used in `query()`, for example, to seed customer- E.g.,
763
+ *
764
+ * ```
765
+ * const p = predicateFor({builder: modelConstructor, schema: modelSchema, pkField: string[]});
766
+ * p.and(child => [
767
+ * child.field.eq('whatever'),
768
+ * child.childModel.childField.eq('whatever else'),
769
+ * child.childModel.or(child => [
770
+ * child.otherField.contains('x'),
771
+ * child.otherField.contains('y'),
772
+ * child.otherField.contains('z'),
773
+ * ])
774
+ * ])
775
+ * ```
776
+ *
777
+ * `predicateFor()` returns objecst with recursive getters. To facilitate this,
778
+ * a `query` and `tail` can be provided to "accumulate" nested conditions.
779
+ *
780
+ * TODO: the sortof-immutable algorithm was originally done to support legacy style
781
+ * predicate branching (`p => p.x.eq(value).y.eq(value)`). i'm not sure this is
782
+ * necessary or beneficial at this point, since we decided that each field condition
783
+ * must flly terminate a branch. is the strong mutation barrier between chain links
784
+ * still necessary or helpful?
785
+ *
786
+ * @param ModelType The ModelMeta used to build child properties.
787
+ * @param field Scopes the query branch to a field.
788
+ * @param query A base query to build on. Omit to start a new query.
789
+ * @param tail The point in an existing `query` to attach new conditions to.
790
+ * @returns A ModelPredicate (builder) that customers can create queries with.
791
+ * (As shown in function description.)
792
+ */
793
+ export function recursivePredicateFor<T extends PersistentModel>(
794
+ ModelType: ModelMeta<T>,
795
+ allowRecursion: boolean = true,
796
+ field?: string,
797
+ query?: GroupCondition,
798
+ tail?: GroupCondition
799
+ ): RecursiveModelPredicate<T> {
800
+ let starter: GroupCondition | undefined;
801
+ // if we don't have an existing query + tail to build onto,
802
+ // we need to start a new query chain.
803
+ if (!query || !tail) {
804
+ starter = new GroupCondition(ModelType, field, undefined, 'and', []);
805
+ }
806
+
807
+ // our eventual return object, which can be built upon.
808
+ // next steps will be to add or(), and(), not(), and field.op() methods.
809
+ const link = {
810
+ __query: starter || query,
811
+ __tail: starter || tail,
812
+ __copy: () => {
813
+ const [query, newtail] = link.__query.copy(link.__tail);
814
+ return recursivePredicateFor(
815
+ ModelType,
816
+ allowRecursion,
817
+ undefined,
818
+ query,
819
+ newtail
820
+ );
821
+ },
822
+ filter: items => {
823
+ return asyncFilter(items, i => link.__query.matches(i));
824
+ },
825
+ } as RecursiveModelPredicate<T>;
826
+
827
+ // Adds .or() and .and() methods to the link.
828
+ // TODO: If revisiting this code, consider writing a Proxy instead.
829
+ ['and', 'or'].forEach(op => {
830
+ (link as any)[op] = (
831
+ ...builderOrPredicates:
832
+ | [RecursiveModelPredicateAggregateExtender<T>]
833
+ | ModelPredicateLeaf[]
834
+ ): ModelPredicateLeaf => {
835
+ // or() and and() will return a copy of the original link
836
+ // to head off mutability concerns.
837
+ const newlink = link.__copy();
838
+
839
+ // the customer will supply a child predicate, which apply to the `model.field`
840
+ // of the tail GroupCondition.
841
+ newlink.__tail.operands.push(
842
+ new GroupCondition(
843
+ ModelType,
844
+ field,
845
+ undefined,
846
+ op as 'and' | 'or',
847
+ typeof builderOrPredicates[0] === 'function'
848
+ ? // handle the the `c => [c.field.eq(v)]` form
849
+ builderOrPredicates[0](
850
+ recursivePredicateFor(ModelType, allowRecursion)
851
+ ).map(p => p.__query)
852
+ : // handle the `[MyModel.field.eq(v)]` form (not yet available)
853
+ (builderOrPredicates as ModelPredicateLeaf[]).map(p => p.__query)
854
+ )
855
+ );
856
+
857
+ // FinalPredicate
858
+ return {
859
+ __query: newlink.__query,
860
+ __tail: newlink.__tail,
861
+ filter: items => {
862
+ return asyncFilter(items, i => newlink.__query.matches(i));
863
+ },
864
+ };
865
+ };
866
+ });
867
+
868
+ // TODO: If revisiting this code, consider proxy.
869
+ link.not = (
870
+ builderOrPredicate: RecursiveModelPredicateExtender<T> | ModelPredicateLeaf
871
+ ): ModelPredicateLeaf => {
872
+ // not() will return a copy of the original link
873
+ // to head off mutability concerns.
874
+ const newlink = link.__copy();
875
+
876
+ // unlike and() and or(), the customer will supply a "singular" child predicate.
877
+ // the difference being: not() does not accept an array of predicate-like objects.
878
+ // it negates only a *single* predicate subtree.
879
+ newlink.__tail.operands.push(
880
+ new GroupCondition(
881
+ ModelType,
882
+ field,
883
+ undefined,
884
+ 'not',
885
+ typeof builderOrPredicate === 'function'
886
+ ? // handle the the `c => c.field.eq(v)` form
887
+ [
888
+ builderOrPredicate(
889
+ recursivePredicateFor(ModelType, allowRecursion)
890
+ ).__query,
891
+ ]
892
+ : // handle the `MyModel.field.eq(v)` form (not yet available)
893
+ [builderOrPredicate.__query]
894
+ )
895
+ );
896
+
897
+ // A `FinalModelPredicate`.
898
+ // Return a thing that can no longer be extended, but instead used to `async filter(items)`
899
+ // or query storage: `.__query.fetch(storage)`.
900
+ return {
901
+ __query: newlink.__query,
902
+ __tail: newlink.__tail,
903
+ filter: items => {
904
+ return asyncFilter(items, i => newlink.__query.matches(i));
905
+ },
906
+ };
907
+ };
908
+
909
+ // For each field on the model schema, we want to add a getter
910
+ // that creates the appropriate new `link` in the query chain.
911
+ // TODO: If revisiting, consider a proxy.
912
+ for (const fieldName in ModelType.schema.fields) {
913
+ Object.defineProperty(link, fieldName, {
914
+ enumerable: true,
915
+ get: () => {
916
+ const def = ModelType.schema.fields[fieldName];
917
+
918
+ if (!def.association) {
919
+ // we're looking at a value field. we need to return a
920
+ // "field matcher object", which contains all of the comparison
921
+ // functions ('eq', 'ne', 'gt', etc.), scoped to operate
922
+ // against the target field (fieldName).
923
+ return ops.reduce((fieldMatcher, operator) => {
924
+ return {
925
+ ...fieldMatcher,
926
+
927
+ // each operator on the fieldMatcher objcect is a function.
928
+ // when the customer calls the function, it returns a new link
929
+ // in the chain -- for now -- this is the "leaf" link that
930
+ // cannot be further extended.
931
+ [operator]: (...operands: any[]) => {
932
+ // build off a fresh copy of the existing `link`, just in case
933
+ // the same link is being used elsewhere by the customer.
934
+ const newlink = link.__copy();
935
+
936
+ // add the given condition to the link's TAIL node.
937
+ // remember: the base link might go N nodes deep! e.g.,
938
+ newlink.__tail.operands.push(
939
+ new FieldCondition(fieldName, operator, operands)
940
+ );
941
+
942
+ // A `FinalModelPredicate`.
943
+ // Return a thing that can no longer be extended, but instead used to `async filter(items)`
944
+ // or query storage: `.__query.fetch(storage)`.
945
+ return {
946
+ __query: newlink.__query,
947
+ __tail: newlink.__tail,
948
+ filter: (items: any[]) => {
949
+ return asyncFilter(items, i => newlink.__query.matches(i));
950
+ },
951
+ };
952
+ },
953
+ };
954
+ }, {});
955
+ } else {
956
+ if (!allowRecursion) {
957
+ throw new Error(
958
+ 'Predication on releated models is not supported in this context.'
959
+ );
960
+ } else if (
961
+ def.association.connectionType === 'BELONGS_TO' ||
962
+ def.association.connectionType === 'HAS_ONE' ||
963
+ def.association.connectionType === 'HAS_MANY'
964
+ ) {
965
+ // the use has just typed '.someRelatedModel'. we need to given them
966
+ // back a predicate chain.
967
+
968
+ const relatedMeta = (def.type as ModelFieldType).modelConstructor;
969
+ if (!relatedMeta) {
970
+ throw new Error(
971
+ 'Related model metadata is missing. This is a bug! Please report it.'
972
+ );
973
+ }
974
+
975
+ // `Model.reletedModelField` returns a copy of the original link,
976
+ // and will contains copies of internal GroupConditions
977
+ // to head off mutability concerns.
978
+ const [newquery, oldtail] = link.__query.copy(link.__tail);
979
+ const newtail = new GroupCondition(
980
+ relatedMeta,
981
+ fieldName,
982
+ def.association.connectionType,
983
+ 'and',
984
+ []
985
+ );
986
+
987
+ // `oldtail` here refers to the *copy* of the old tail.
988
+ // so, it's safe to modify at this point. and we need to modify
989
+ // it to push the *new* tail onto the end of it.
990
+ (oldtail as GroupCondition).operands.push(newtail);
991
+ const newlink = recursivePredicateFor(
992
+ relatedMeta,
993
+ allowRecursion,
994
+ undefined,
995
+ newquery,
996
+ newtail
997
+ );
998
+ return newlink;
999
+ } else {
1000
+ throw new Error(
1001
+ "Related model definition doesn't have a typedef. This is a bug! Please report it."
1002
+ );
1003
+ }
1004
+ }
1005
+ },
1006
+ });
1007
+ }
1008
+
1009
+ return link;
1010
+ }
1011
+
1012
+ export function predicateFor<T extends PersistentModel>(
1013
+ ModelType: ModelMeta<T>
1014
+ ): ModelPredicate<T> {
1015
+ return recursivePredicateFor(ModelType, false) as any as ModelPredicate<T>;
1016
+ }