@aws-amplify/datastore 4.1.1 → 4.1.2

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 (26) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/storage/adapter/AsyncStorageAdapter.d.ts +52 -28
  3. package/lib/storage/adapter/AsyncStorageAdapter.js +212 -476
  4. package/lib/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  5. package/lib/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  6. package/lib/storage/adapter/IndexedDBAdapter.d.ts +53 -28
  7. package/lib/storage/adapter/IndexedDBAdapter.js +589 -892
  8. package/lib/storage/adapter/IndexedDBAdapter.js.map +1 -1
  9. package/lib/storage/adapter/StorageAdapterBase.d.ts +146 -0
  10. package/lib/storage/adapter/StorageAdapterBase.js +479 -0
  11. package/lib/storage/adapter/StorageAdapterBase.js.map +1 -0
  12. package/lib-esm/storage/adapter/AsyncStorageAdapter.d.ts +52 -28
  13. package/lib-esm/storage/adapter/AsyncStorageAdapter.js +215 -479
  14. package/lib-esm/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  15. package/lib-esm/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  16. package/lib-esm/storage/adapter/IndexedDBAdapter.d.ts +53 -28
  17. package/lib-esm/storage/adapter/IndexedDBAdapter.js +588 -891
  18. package/lib-esm/storage/adapter/IndexedDBAdapter.js.map +1 -1
  19. package/lib-esm/storage/adapter/StorageAdapterBase.d.ts +146 -0
  20. package/lib-esm/storage/adapter/StorageAdapterBase.js +477 -0
  21. package/lib-esm/storage/adapter/StorageAdapterBase.js.map +1 -0
  22. package/package.json +6 -6
  23. package/src/storage/adapter/AsyncStorageAdapter.ts +239 -543
  24. package/src/storage/adapter/AsyncStorageDatabase.ts +2 -2
  25. package/src/storage/adapter/IndexedDBAdapter.ts +423 -786
  26. package/src/storage/adapter/StorageAdapterBase.ts +639 -0
@@ -0,0 +1,639 @@
1
+ import { ConsoleLogger as Logger } from '@aws-amplify/core';
2
+ import { Adapter } from './index';
3
+ import { ModelInstanceCreator } from '../../datastore/datastore';
4
+ import { ModelPredicateCreator } from '../../predicates';
5
+ import {
6
+ InternalSchema,
7
+ isPredicateObj,
8
+ ModelInstanceMetadata,
9
+ ModelPredicate,
10
+ NamespaceResolver,
11
+ OpType,
12
+ PaginationInput,
13
+ PersistentModel,
14
+ PersistentModelConstructor,
15
+ PredicateObject,
16
+ PredicatesGroup,
17
+ QueryOne,
18
+ RelationType,
19
+ } from '../../types';
20
+ import {
21
+ NAMESPACES,
22
+ getStorename,
23
+ getIndexKeys,
24
+ extractPrimaryKeyValues,
25
+ traverseModel,
26
+ validatePredicate,
27
+ getIndex,
28
+ getIndexFromAssociation,
29
+ isModelConstructor,
30
+ } from '../../util';
31
+ import type { IDBPDatabase, IDBPObjectStore } from 'idb';
32
+ import type AsyncStorageDatabase from './AsyncStorageDatabase';
33
+
34
+ const logger = new Logger('DataStore');
35
+ const DB_NAME = 'amplify-datastore';
36
+
37
+ export abstract class StorageAdapterBase implements Adapter {
38
+ // Non-null assertions (bang operators) added to most properties to make TS happy.
39
+ // For now, we can be reasonably sure they're available when they're needed, because
40
+ // the adapter is not used directly outside the library boundary.
41
+ protected schema!: InternalSchema;
42
+ protected namespaceResolver!: NamespaceResolver;
43
+ protected modelInstanceCreator!: ModelInstanceCreator;
44
+ protected getModelConstructorByModelName!: (
45
+ namsespaceName: NAMESPACES,
46
+ modelName: string
47
+ ) => PersistentModelConstructor<any>;
48
+ protected initPromise!: Promise<void>;
49
+ protected resolve!: (value?: any) => void;
50
+ protected reject!: (value?: any) => void;
51
+ protected dbName: string = DB_NAME;
52
+ protected abstract db: IDBPDatabase | AsyncStorageDatabase;
53
+
54
+ protected abstract preSetUpChecks(): Promise<void>;
55
+ protected abstract preOpCheck(): Promise<void>;
56
+ protected abstract initDb(): Promise<IDBPDatabase | AsyncStorageDatabase>;
57
+
58
+ /**
59
+ * Initializes local DB
60
+ *
61
+ * @param theSchema
62
+ * @param namespaceResolver
63
+ * @param modelInstanceCreator
64
+ * @param getModelConstructorByModelName
65
+ * @param sessionId
66
+ */
67
+ public async setUp(
68
+ theSchema: InternalSchema,
69
+ namespaceResolver: NamespaceResolver,
70
+ modelInstanceCreator: ModelInstanceCreator,
71
+ getModelConstructorByModelName: (
72
+ namsespaceName: NAMESPACES,
73
+ modelName: string
74
+ ) => PersistentModelConstructor<any>,
75
+ sessionId?: string
76
+ ): Promise<void> {
77
+ await this.preSetUpChecks();
78
+
79
+ if (!this.initPromise) {
80
+ this.initPromise = new Promise((res, rej) => {
81
+ this.resolve = res;
82
+ this.reject = rej;
83
+ });
84
+ } else {
85
+ await this.initPromise;
86
+ return;
87
+ }
88
+ if (sessionId) {
89
+ this.dbName = `${DB_NAME}-${sessionId}`;
90
+ }
91
+ this.schema = theSchema;
92
+ this.namespaceResolver = namespaceResolver;
93
+ this.modelInstanceCreator = modelInstanceCreator;
94
+ this.getModelConstructorByModelName = getModelConstructorByModelName;
95
+
96
+ try {
97
+ if (!this.db) {
98
+ this.db = await this.initDb();
99
+ this.resolve();
100
+ }
101
+ } catch (error) {
102
+ this.reject(error);
103
+ }
104
+ }
105
+
106
+ /*
107
+ * Abstract Methods for Adapter interface
108
+ * Not enough implementation similarities between the adapters
109
+ * to consolidate in the base class
110
+ */
111
+ public abstract clear(): Promise<void>;
112
+
113
+ public abstract save<T extends PersistentModel>(
114
+ model: T,
115
+ condition?: ModelPredicate<T>
116
+ );
117
+
118
+ public abstract query<T extends PersistentModel>(
119
+ modelConstructor: PersistentModelConstructor<T>,
120
+ predicate?: ModelPredicate<T>,
121
+ pagination?: PaginationInput<T>
122
+ ): Promise<T[]>;
123
+
124
+ public abstract queryOne<T extends PersistentModel>(
125
+ modelConstructor: PersistentModelConstructor<T>,
126
+ firstOrLast: QueryOne
127
+ ): Promise<T | undefined>;
128
+
129
+ public abstract batchSave<T extends PersistentModel>(
130
+ modelConstructor: PersistentModelConstructor<any>,
131
+ items: ModelInstanceMetadata[]
132
+ ): Promise<[T, OpType][]>;
133
+
134
+ /**
135
+ * @param modelConstructor
136
+ * @returns local DB table name
137
+ */
138
+ protected getStorenameForModel(
139
+ modelConstructor: PersistentModelConstructor<any>
140
+ ): string {
141
+ const namespace = this.namespaceResolver(modelConstructor);
142
+ const { name: modelName } = modelConstructor;
143
+
144
+ return getStorename(namespace, modelName);
145
+ }
146
+
147
+ /**
148
+ *
149
+ * @param model - instantiated model record
150
+ * @returns the record's primary key values
151
+ */
152
+ protected getIndexKeyValuesFromModel<T extends PersistentModel>(
153
+ model: T
154
+ ): string[] {
155
+ const modelConstructor = Object.getPrototypeOf(model)
156
+ .constructor as PersistentModelConstructor<T>;
157
+ const namespaceName = this.namespaceResolver(modelConstructor);
158
+
159
+ const keys = getIndexKeys(
160
+ this.schema.namespaces[namespaceName],
161
+ modelConstructor.name
162
+ );
163
+
164
+ return extractPrimaryKeyValues(model, keys);
165
+ }
166
+
167
+ /**
168
+ * Common metadata for `save` operation
169
+ * used by individual storage adapters
170
+ *
171
+ * @param model
172
+ */
173
+ protected saveMetadata<T extends PersistentModel>(
174
+ model: T
175
+ ): {
176
+ storeName: string;
177
+ set: Set<string>;
178
+ connectionStoreNames;
179
+ modelKeyValues: string[];
180
+ } {
181
+ const modelConstructor = Object.getPrototypeOf(model)
182
+ .constructor as PersistentModelConstructor<T>;
183
+ const storeName = this.getStorenameForModel(modelConstructor);
184
+ const namespaceName = this.namespaceResolver(modelConstructor);
185
+
186
+ const connectedModels = traverseModel(
187
+ modelConstructor.name,
188
+ model,
189
+ this.schema.namespaces[namespaceName],
190
+ this.modelInstanceCreator,
191
+ this.getModelConstructorByModelName!
192
+ );
193
+
194
+ const set = new Set<string>();
195
+ const connectionStoreNames = Object.values(connectedModels).map(
196
+ ({ modelName, item, instance }) => {
197
+ const storeName = getStorename(namespaceName, modelName);
198
+ set.add(storeName);
199
+ const keys = getIndexKeys(
200
+ this.schema.namespaces[namespaceName],
201
+ modelName
202
+ );
203
+ return { storeName, item, instance, keys };
204
+ }
205
+ );
206
+
207
+ const modelKeyValues = this.getIndexKeyValuesFromModel(model);
208
+
209
+ return { storeName, set, connectionStoreNames, modelKeyValues };
210
+ }
211
+
212
+ /**
213
+ * Enforces conditional save. Throws if condition is not met.
214
+ * used by individual storage adapters
215
+ *
216
+ * @param model
217
+ */
218
+ protected validateSaveCondition<T extends PersistentModel>(
219
+ condition?: ModelPredicate<T>,
220
+ fromDB?: unknown
221
+ ): void {
222
+ if (!(condition && fromDB)) {
223
+ return;
224
+ }
225
+
226
+ const predicates = ModelPredicateCreator.getPredicates(condition);
227
+ const { predicates: predicateObjs, type } = predicates!;
228
+
229
+ const isValid = validatePredicate(fromDB, type, predicateObjs);
230
+
231
+ if (!isValid) {
232
+ const msg = 'Conditional update failed';
233
+ logger.error(msg, { model: fromDB, condition: predicateObjs });
234
+
235
+ throw new Error(msg);
236
+ }
237
+ }
238
+
239
+ protected abstract _get<T>(
240
+ storeOrStoreName: IDBPObjectStore | string,
241
+ keyArr: string[]
242
+ ): Promise<T>;
243
+
244
+ /**
245
+ * Instantiate models from POJO records returned from the database
246
+ *
247
+ * @param namespaceName - string model namespace
248
+ * @param srcModelName - string model name
249
+ * @param records - array of uninstantiated records
250
+ * @returns
251
+ */
252
+ protected async load<T>(
253
+ namespaceName: NAMESPACES,
254
+ srcModelName: string,
255
+ records: T[]
256
+ ): Promise<T[]> {
257
+ const namespace = this.schema.namespaces[namespaceName];
258
+ const relations = namespace.relationships![srcModelName].relationTypes;
259
+ const connectionStoreNames = relations.map(({ modelName }) => {
260
+ return getStorename(namespaceName, modelName);
261
+ });
262
+ const modelConstructor = this.getModelConstructorByModelName!(
263
+ namespaceName,
264
+ srcModelName
265
+ );
266
+
267
+ if (connectionStoreNames.length === 0) {
268
+ return records.map(record =>
269
+ this.modelInstanceCreator(modelConstructor, record)
270
+ );
271
+ }
272
+
273
+ return records.map(record =>
274
+ this.modelInstanceCreator(modelConstructor, record)
275
+ );
276
+ }
277
+
278
+ /**
279
+ * Extracts operands from a predicate group into an array of key values
280
+ * Used in the query method
281
+ *
282
+ * @param predicates - predicate group
283
+ * @param keyPath - string array of key names ['id', 'sortKey']
284
+ * @returns string[] of key values
285
+ *
286
+ * @example
287
+ * ```js
288
+ * { and:[{ id: { eq: 'abc' }}, { sortKey: { eq: 'def' }}] }
289
+ * ```
290
+ * Becomes
291
+ * ```
292
+ * ['abc', 'def']
293
+ * ```
294
+ */
295
+ private keyValueFromPredicate<T extends PersistentModel>(
296
+ predicates: PredicatesGroup<T>,
297
+ keyPath: string[]
298
+ ): string[] | undefined {
299
+ const { predicates: predicateObjs } = predicates;
300
+
301
+ if (predicateObjs.length !== keyPath.length) {
302
+ return;
303
+ }
304
+
305
+ const keyValues = [] as any[];
306
+
307
+ for (const key of keyPath) {
308
+ const predicateObj = predicateObjs.find(
309
+ p =>
310
+ // it's a relevant predicate object only if it's an equality
311
+ // operation for a key field from the key:
312
+ isPredicateObj(p) &&
313
+ p.field === key &&
314
+ p.operator === 'eq' &&
315
+ p.operand !== null &&
316
+ p.operand !== undefined
317
+ ) as PredicateObject<T>;
318
+
319
+ predicateObj && keyValues.push(predicateObj.operand);
320
+ }
321
+
322
+ return keyValues.length === keyPath.length ? keyValues : undefined;
323
+ }
324
+
325
+ /**
326
+ * Common metadata for `query` operation
327
+ * used by individual storage adapters
328
+ *
329
+ * @param modelConstructor
330
+ * @param predicate
331
+ * @param pagination
332
+ */
333
+ protected queryMetadata<T extends PersistentModel>(
334
+ modelConstructor: PersistentModelConstructor<T>,
335
+ predicate?: ModelPredicate<T>,
336
+ pagination?: PaginationInput<T>
337
+ ) {
338
+ const storeName = this.getStorenameForModel(modelConstructor);
339
+ const namespaceName = this.namespaceResolver(
340
+ modelConstructor
341
+ ) as NAMESPACES;
342
+
343
+ const predicates =
344
+ predicate && ModelPredicateCreator.getPredicates(predicate);
345
+ const keyPath = getIndexKeys(
346
+ this.schema.namespaces[namespaceName],
347
+ modelConstructor.name
348
+ );
349
+ const queryByKey =
350
+ predicates && this.keyValueFromPredicate(predicates, keyPath);
351
+
352
+ const hasSort = pagination && pagination.sort;
353
+ const hasPagination = pagination && pagination.limit;
354
+
355
+ return {
356
+ storeName,
357
+ namespaceName,
358
+ queryByKey,
359
+ predicates,
360
+ hasSort,
361
+ hasPagination,
362
+ };
363
+ }
364
+
365
+ /**
366
+ * Delete record
367
+ * Cascades to related records (for Has One and Has Many relationships)
368
+ *
369
+ * @param modelOrModelConstructor
370
+ * @param condition
371
+ * @returns
372
+ */
373
+ public async delete<T extends PersistentModel>(
374
+ modelOrModelConstructor: T | PersistentModelConstructor<T>,
375
+ condition?: ModelPredicate<T>
376
+ ): Promise<[T[], T[]]> {
377
+ await this.preOpCheck();
378
+
379
+ const deleteQueue: { storeName: string; items: T[] }[] = [];
380
+
381
+ if (isModelConstructor(modelOrModelConstructor)) {
382
+ const modelConstructor =
383
+ modelOrModelConstructor as PersistentModelConstructor<T>;
384
+ const namespace = this.namespaceResolver(modelConstructor) as NAMESPACES;
385
+
386
+ const models = await this.query(modelConstructor, condition);
387
+ const relations =
388
+ this.schema.namespaces![namespace].relationships![modelConstructor.name]
389
+ .relationTypes;
390
+
391
+ if (condition !== undefined) {
392
+ await this.deleteTraverse(
393
+ relations,
394
+ models,
395
+ modelConstructor.name,
396
+ namespace,
397
+ deleteQueue
398
+ );
399
+
400
+ await this.deleteItem(deleteQueue);
401
+
402
+ const deletedModels = deleteQueue.reduce(
403
+ (acc, { items }) => acc.concat(items),
404
+ <T[]>[]
405
+ );
406
+
407
+ return [models, deletedModels];
408
+ } else {
409
+ await this.deleteTraverse(
410
+ relations,
411
+ models,
412
+ modelConstructor.name,
413
+ namespace,
414
+ deleteQueue
415
+ );
416
+
417
+ await this.deleteItem(deleteQueue);
418
+
419
+ const deletedModels = deleteQueue.reduce(
420
+ (acc, { items }) => acc.concat(items),
421
+ <T[]>[]
422
+ );
423
+
424
+ return [models, deletedModels];
425
+ }
426
+ } else {
427
+ const model = modelOrModelConstructor as T;
428
+
429
+ const modelConstructor = Object.getPrototypeOf(model)
430
+ .constructor as PersistentModelConstructor<T>;
431
+ const namespaceName = this.namespaceResolver(
432
+ modelConstructor
433
+ ) as NAMESPACES;
434
+
435
+ const storeName = this.getStorenameForModel(modelConstructor);
436
+
437
+ if (condition) {
438
+ const keyValues = this.getIndexKeyValuesFromModel(model);
439
+ const fromDB = await this._get(storeName, keyValues);
440
+
441
+ if (fromDB === undefined) {
442
+ const msg = 'Model instance not found in storage';
443
+ logger.warn(msg, { model });
444
+
445
+ return [[model], []];
446
+ }
447
+
448
+ const predicates = ModelPredicateCreator.getPredicates(condition);
449
+ const { predicates: predicateObjs, type } =
450
+ predicates as PredicatesGroup<T>;
451
+
452
+ const isValid = validatePredicate(fromDB as T, type, predicateObjs);
453
+ if (!isValid) {
454
+ const msg = 'Conditional update failed';
455
+ logger.error(msg, { model: fromDB, condition: predicateObjs });
456
+
457
+ throw new Error(msg);
458
+ }
459
+
460
+ const relations =
461
+ this.schema.namespaces[namespaceName].relationships![
462
+ modelConstructor.name
463
+ ].relationTypes;
464
+
465
+ await this.deleteTraverse(
466
+ relations,
467
+ [model],
468
+ modelConstructor.name,
469
+ namespaceName,
470
+ deleteQueue
471
+ );
472
+ } else {
473
+ const relations =
474
+ this.schema.namespaces[namespaceName].relationships![
475
+ modelConstructor.name
476
+ ].relationTypes;
477
+
478
+ await this.deleteTraverse(
479
+ relations,
480
+ [model],
481
+ modelConstructor.name,
482
+ namespaceName,
483
+ deleteQueue
484
+ );
485
+ }
486
+ await this.deleteItem(deleteQueue);
487
+
488
+ const deletedModels = deleteQueue.reduce(
489
+ (acc, { items }) => acc.concat(items),
490
+ <T[]>[]
491
+ );
492
+
493
+ return [[model], deletedModels];
494
+ }
495
+ }
496
+
497
+ protected abstract deleteItem<T extends PersistentModel>(
498
+ deleteQueue?: {
499
+ storeName: string;
500
+ items: T[] | IDBValidKey[];
501
+ }[]
502
+ );
503
+
504
+ protected abstract getHasOneChild<T extends PersistentModel>(
505
+ model: T,
506
+ srcModel: string,
507
+ namespace: NAMESPACES,
508
+ rel: RelationType
509
+ ): Promise<T | undefined>;
510
+
511
+ /**
512
+ * Backwards compatability for pre-CPK codegen
513
+ * TODO - deprecate this in v6; will need to re-gen MIPR for older unit
514
+ * tests that hit this path
515
+ */
516
+ protected abstract getHasOneChildLegacy<T extends PersistentModel>(
517
+ model: T,
518
+ srcModel: string,
519
+ namespace: NAMESPACES,
520
+ rel: RelationType
521
+ ): Promise<T | undefined>;
522
+
523
+ protected abstract getHasManyChildren<T extends PersistentModel>(
524
+ storeName: string,
525
+ index: string,
526
+ keyValues: string[]
527
+ ): Promise<T[] | undefined>;
528
+
529
+ /**
530
+ * Recursively traverse relationship graph and add
531
+ * all Has One and Has Many relations to `deleteQueue` param
532
+ *
533
+ * Actual deletion of records added to `deleteQueue` occurs in the `delete` method
534
+ *
535
+ * @param relations
536
+ * @param models
537
+ * @param srcModel
538
+ * @param namespace
539
+ * @param deleteQueue
540
+ */
541
+ protected async deleteTraverse<T extends PersistentModel>(
542
+ relations: RelationType[],
543
+ models: T[],
544
+ srcModel: string,
545
+ namespace: NAMESPACES,
546
+ deleteQueue: { storeName: string; items: T[] }[]
547
+ ): Promise<void> {
548
+ for await (const rel of relations) {
549
+ const { modelName, relationType, targetNames, associatedWith } = rel;
550
+
551
+ const storeName = getStorename(namespace, modelName);
552
+ const index: string =
553
+ getIndex(
554
+ this.schema.namespaces[namespace].relationships![modelName]
555
+ .relationTypes,
556
+ srcModel
557
+ ) ||
558
+ // if we were unable to find an index via relationTypes
559
+ // i.e. for keyName connections, attempt to find one by the
560
+ // associatedWith property
561
+ getIndexFromAssociation(
562
+ this.schema.namespaces[namespace].relationships![modelName].indexes,
563
+ associatedWith!
564
+ )!;
565
+
566
+ for await (const model of models) {
567
+ const childRecords: PersistentModel[] = [];
568
+
569
+ switch (relationType) {
570
+ case 'HAS_ONE':
571
+ let childRecord;
572
+ if (targetNames?.length) {
573
+ childRecord = await this.getHasOneChild(
574
+ model,
575
+ srcModel,
576
+ namespace,
577
+ rel
578
+ );
579
+ } else {
580
+ childRecord = await this.getHasOneChildLegacy(
581
+ model,
582
+ srcModel,
583
+ namespace,
584
+ rel
585
+ );
586
+ }
587
+
588
+ if (childRecord) {
589
+ childRecords.push(childRecord);
590
+ }
591
+
592
+ break;
593
+ case 'HAS_MANY':
594
+ const keyValues: string[] = this.getIndexKeyValuesFromModel(model);
595
+
596
+ const records = await this.getHasManyChildren(
597
+ storeName,
598
+ index,
599
+ keyValues
600
+ );
601
+
602
+ if (records?.length) {
603
+ childRecords.push(...records);
604
+ }
605
+
606
+ break;
607
+ case 'BELONGS_TO':
608
+ // Intentionally blank
609
+ break;
610
+ default:
611
+ throw new Error(`Invalid relation type ${relationType}`);
612
+ }
613
+
614
+ // instantiate models before passing them to next recursive call
615
+ // necessary for extracting PK metadata in `getHasOneChild` and `getHasManyChildren`
616
+ const childModels = await this.load(namespace, modelName, childRecords);
617
+
618
+ await this.deleteTraverse(
619
+ this.schema.namespaces[namespace].relationships![modelName]
620
+ .relationTypes,
621
+ childModels,
622
+ modelName,
623
+ namespace,
624
+ deleteQueue
625
+ );
626
+ }
627
+ }
628
+
629
+ deleteQueue.push({
630
+ storeName: getStorename(namespace, srcModel),
631
+ items: models.map(record =>
632
+ this.modelInstanceCreator(
633
+ this.getModelConstructorByModelName!(namespace, srcModel),
634
+ record
635
+ )
636
+ ),
637
+ });
638
+ }
639
+ }