@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
@@ -1,14 +1,10 @@
1
1
  import { ConsoleLogger as Logger } from '@aws-amplify/core';
2
2
  import * as idb from 'idb';
3
- import { ModelInstanceCreator } from '../../datastore/datastore';
4
- import { ModelPredicateCreator } from '../../predicates';
5
3
  import {
6
- InternalSchema,
7
4
  isPredicateObj,
8
5
  isPredicateGroup,
9
6
  ModelInstanceMetadata,
10
7
  ModelPredicate,
11
- NamespaceResolver,
12
8
  OpType,
13
9
  PaginationInput,
14
10
  PersistentModel,
@@ -20,8 +16,6 @@ import {
20
16
  } from '../../types';
21
17
  import {
22
18
  getIndex,
23
- getIndexFromAssociation,
24
- isModelConstructor,
25
19
  isPrivateMode,
26
20
  traverseModel,
27
21
  validatePredicate,
@@ -29,11 +23,9 @@ import {
29
23
  NAMESPACES,
30
24
  keysEqual,
31
25
  getStorename,
32
- getIndexKeys,
33
- extractPrimaryKeyValues,
34
26
  isSafariCompatabilityMode,
35
27
  } from '../../util';
36
- import { Adapter } from './index';
28
+ import { StorageAdapterBase } from './StorageAdapterBase';
37
29
 
38
30
  const logger = new Logger('DataStore');
39
31
 
@@ -59,229 +51,135 @@ const logger = new Logger('DataStore');
59
51
  *
60
52
  */
61
53
  const MULTI_OR_CONDITION_SCAN_BREAKPOINT = 7;
54
+ //
55
+ const DB_VERSION = 3;
62
56
 
63
- const DB_NAME = 'amplify-datastore';
64
- class IndexedDBAdapter implements Adapter {
65
- private schema!: InternalSchema;
66
- private namespaceResolver!: NamespaceResolver;
67
- private modelInstanceCreator!: ModelInstanceCreator;
68
- private getModelConstructorByModelName?: (
69
- namsespaceName: NAMESPACES,
70
- modelName: string
71
- ) => PersistentModelConstructor<any>;
72
- private db!: idb.IDBPDatabase;
73
- private initPromise!: Promise<void>;
74
- private resolve!: (value?: any) => void;
75
- private reject!: (value?: any) => void;
76
- private dbName: string = DB_NAME;
57
+ class IndexedDBAdapter extends StorageAdapterBase {
58
+ protected db!: idb.IDBPDatabase;
77
59
  private safariCompatabilityMode: boolean = false;
78
60
 
79
- private getStorenameForModel(
80
- modelConstructor: PersistentModelConstructor<any>
81
- ) {
82
- const namespace = this.namespaceResolver(modelConstructor);
83
- const { name: modelName } = modelConstructor;
84
-
85
- return getStorename(namespace, modelName);
86
- }
87
-
88
- // Retrieves primary key values from a model
89
- private getIndexKeyValuesFromModel<T extends PersistentModel>(
90
- model: T
91
- ): string[] {
92
- const modelConstructor = Object.getPrototypeOf(model)
93
- .constructor as PersistentModelConstructor<T>;
94
- const namespaceName = this.namespaceResolver(modelConstructor);
95
-
96
- const keys = getIndexKeys(
97
- this.schema.namespaces[namespaceName],
98
- modelConstructor.name
99
- );
100
-
101
- return extractPrimaryKeyValues(model, keys);
61
+ // checks are called by StorageAdapterBase class
62
+ protected async preSetUpChecks() {
63
+ await this.checkPrivate();
64
+ await this.setSafariCompatabilityMode();
102
65
  }
103
66
 
104
- private async checkPrivate() {
105
- const isPrivate = await isPrivateMode().then(isPrivate => {
106
- return isPrivate;
107
- });
108
- if (isPrivate) {
109
- logger.error("IndexedDB not supported in this browser's private mode");
110
- return Promise.reject(
111
- "IndexedDB not supported in this browser's private mode"
112
- );
113
- } else {
114
- return Promise.resolve();
115
- }
67
+ protected async preOpCheck() {
68
+ await this.checkPrivate();
116
69
  }
117
70
 
118
71
  /**
119
- * Whether the browser's implementation of IndexedDB is coercing single-field
120
- * indexes to a scalar key.
72
+ * Initialize IndexedDB database
73
+ * Create new DB if one doesn't exist
74
+ * Upgrade outdated DB
121
75
  *
122
- * If this returns `true`, we need to treat indexes containing a single field
123
- * as scalars.
76
+ * Called by `StorageAdapterBase.setUp()`
124
77
  *
125
- * See PR description for reference:
126
- * https://github.com/aws-amplify/amplify-js/pull/10527
78
+ * @returns IDB Database instance
127
79
  */
128
- private async setSafariCompatabilityMode() {
129
- this.safariCompatabilityMode = await isSafariCompatabilityMode();
80
+ protected async initDb(): Promise<idb.IDBPDatabase> {
81
+ return await idb.openDB(this.dbName, DB_VERSION, {
82
+ upgrade: async (db, oldVersion, newVersion, txn) => {
83
+ // create new database
84
+ if (oldVersion === 0) {
85
+ Object.keys(this.schema.namespaces).forEach(namespaceName => {
86
+ const namespace = this.schema.namespaces[namespaceName];
87
+
88
+ Object.keys(namespace.models).forEach(modelName => {
89
+ const storeName = getStorename(namespaceName, modelName);
90
+ this.createObjectStoreForModel(
91
+ db,
92
+ namespaceName,
93
+ storeName,
94
+ modelName
95
+ );
96
+ });
97
+ });
130
98
 
131
- if (this.safariCompatabilityMode === true) {
132
- logger.debug('IndexedDB Adapter is running in Safari Compatability Mode');
133
- }
134
- }
99
+ return;
100
+ }
135
101
 
136
- private getNamespaceAndModelFromStorename(storeName: string) {
137
- const [namespaceName, ...modelNameArr] = storeName.split('_');
138
- return {
139
- namespaceName,
140
- modelName: modelNameArr.join('_'),
141
- };
142
- }
102
+ // migrate existing database to latest schema
103
+ if ((oldVersion === 1 || oldVersion === 2) && newVersion === 3) {
104
+ try {
105
+ for (const storeName of txn.objectStoreNames) {
106
+ const origStore = txn.objectStore(storeName);
143
107
 
144
- async setUp(
145
- theSchema: InternalSchema,
146
- namespaceResolver: NamespaceResolver,
147
- modelInstanceCreator: ModelInstanceCreator,
148
- getModelConstructorByModelName: (
149
- namsespaceName: NAMESPACES,
150
- modelName: string
151
- ) => PersistentModelConstructor<any>,
152
- sessionId?: string
153
- ) {
154
- await this.checkPrivate();
155
- await this.setSafariCompatabilityMode();
108
+ // rename original store
109
+ const tmpName = `tmp_${storeName}`;
110
+ origStore.name = tmpName;
156
111
 
157
- if (!this.initPromise) {
158
- this.initPromise = new Promise((res, rej) => {
159
- this.resolve = res;
160
- this.reject = rej;
161
- });
162
- } else {
163
- await this.initPromise;
164
- }
165
- if (sessionId) {
166
- this.dbName = `${DB_NAME}-${sessionId}`;
167
- }
168
- this.schema = theSchema;
169
- this.namespaceResolver = namespaceResolver;
170
- this.modelInstanceCreator = modelInstanceCreator;
171
- this.getModelConstructorByModelName = getModelConstructorByModelName;
172
-
173
- try {
174
- if (!this.db) {
175
- const VERSION = 3;
176
- this.db = await idb.openDB(this.dbName, VERSION, {
177
- upgrade: async (db, oldVersion, newVersion, txn) => {
178
- if (oldVersion === 0) {
179
- Object.keys(theSchema.namespaces).forEach(namespaceName => {
180
- const namespace = theSchema.namespaces[namespaceName];
181
-
182
- Object.keys(namespace.models).forEach(modelName => {
183
- const storeName = getStorename(namespaceName, modelName);
184
- this.createObjectStoreForModel(
185
- db,
186
- namespaceName,
187
- storeName,
188
- modelName
189
- );
190
- });
191
- });
112
+ const { namespaceName, modelName } =
113
+ this.getNamespaceAndModelFromStorename(storeName);
192
114
 
193
- return;
194
- }
115
+ const modelInCurrentSchema =
116
+ modelName in this.schema.namespaces[namespaceName].models;
117
+
118
+ if (!modelInCurrentSchema) {
119
+ // delete original
120
+ db.deleteObjectStore(tmpName);
121
+ continue;
122
+ }
195
123
 
196
- if ((oldVersion === 1 || oldVersion === 2) && newVersion === 3) {
197
- try {
198
- for (const storeName of txn.objectStoreNames) {
199
- const origStore = txn.objectStore(storeName);
124
+ const newStore = this.createObjectStoreForModel(
125
+ db,
126
+ namespaceName,
127
+ storeName,
128
+ modelName
129
+ );
200
130
 
201
- // rename original store
202
- const tmpName = `tmp_${storeName}`;
203
- origStore.name = tmpName;
131
+ let cursor = await origStore.openCursor();
132
+ let count = 0;
204
133
 
205
- const { namespaceName, modelName } =
206
- this.getNamespaceAndModelFromStorename(storeName);
134
+ // Copy data from original to new
135
+ while (cursor && cursor.value) {
136
+ // we don't pass key, since they are all new entries in the new store
137
+ await newStore.put(cursor.value);
207
138
 
208
- const modelInCurrentSchema =
209
- modelName in this.schema.namespaces[namespaceName].models;
139
+ cursor = await cursor.continue();
140
+ count++;
141
+ }
210
142
 
211
- if (!modelInCurrentSchema) {
212
- // delete original
213
- db.deleteObjectStore(tmpName);
214
- continue;
215
- }
143
+ // delete original
144
+ db.deleteObjectStore(tmpName);
145
+
146
+ logger.debug(`${count} ${storeName} records migrated`);
147
+ }
216
148
 
217
- const newStore = this.createObjectStoreForModel(
149
+ // add new models created after IndexedDB, but before migration
150
+ // this case may happen when a user has not opened an app for
151
+ // some time and a new model is added during that time
152
+ Object.keys(this.schema.namespaces).forEach(namespaceName => {
153
+ const namespace = this.schema.namespaces[namespaceName];
154
+ const objectStoreNames = new Set(txn.objectStoreNames);
155
+
156
+ Object.keys(namespace.models)
157
+ .map(modelName => {
158
+ return [modelName, getStorename(namespaceName, modelName)];
159
+ })
160
+ .filter(([, storeName]) => !objectStoreNames.has(storeName))
161
+ .forEach(([modelName, storeName]) => {
162
+ this.createObjectStoreForModel(
218
163
  db,
219
164
  namespaceName,
220
165
  storeName,
221
166
  modelName
222
167
  );
223
-
224
- let cursor = await origStore.openCursor();
225
- let count = 0;
226
-
227
- // Copy data from original to new
228
- while (cursor && cursor.value) {
229
- // we don't pass key, since they are all new entries in the new store
230
- await newStore.put(cursor.value);
231
-
232
- cursor = await cursor.continue();
233
- count++;
234
- }
235
-
236
- // delete original
237
- db.deleteObjectStore(tmpName);
238
-
239
- logger.debug(`${count} ${storeName} records migrated`);
240
- }
241
-
242
- // add new models created after IndexedDB, but before migration
243
- // this case may happen when a user has not opened an app for
244
- // some time and a new model is added during that time
245
- Object.keys(theSchema.namespaces).forEach(namespaceName => {
246
- const namespace = theSchema.namespaces[namespaceName];
247
- const objectStoreNames = new Set(txn.objectStoreNames);
248
-
249
- Object.keys(namespace.models)
250
- .map(modelName => {
251
- return [
252
- modelName,
253
- getStorename(namespaceName, modelName),
254
- ];
255
- })
256
- .filter(([, storeName]) => !objectStoreNames.has(storeName))
257
- .forEach(([modelName, storeName]) => {
258
- this.createObjectStoreForModel(
259
- db,
260
- namespaceName,
261
- storeName,
262
- modelName
263
- );
264
- });
265
168
  });
266
- } catch (error) {
267
- logger.error('Error migrating IndexedDB data', error);
268
- txn.abort();
269
- throw error;
270
- }
271
-
272
- return;
273
- }
274
- },
275
- });
169
+ });
170
+ } catch (error) {
171
+ logger.error('Error migrating IndexedDB data', error);
172
+ txn.abort();
173
+ throw error;
174
+ }
276
175
 
277
- this.resolve();
278
- }
279
- } catch (error) {
280
- this.reject(error);
281
- }
176
+ return;
177
+ }
178
+ },
179
+ });
282
180
  }
283
181
 
284
- private async _get<T>(
182
+ protected async _get<T>(
285
183
  storeOrStoreName: idb.IDBPObjectStore | string,
286
184
  keyArr: string[]
287
185
  ): Promise<T> {
@@ -297,7 +195,17 @@ class IndexedDBAdapter implements Adapter {
297
195
 
298
196
  const result = await index.get(this.canonicalKeyPath(keyArr));
299
197
 
300
- return result;
198
+ return <T>result;
199
+ }
200
+
201
+ async clear(): Promise<void> {
202
+ await this.checkPrivate();
203
+
204
+ this.db?.close();
205
+ await idb.deleteDB(this.dbName);
206
+
207
+ this.db = undefined!;
208
+ this.initPromise = undefined!;
301
209
  }
302
210
 
303
211
  async save<T extends PersistentModel>(
@@ -305,77 +213,30 @@ class IndexedDBAdapter implements Adapter {
305
213
  condition?: ModelPredicate<T>
306
214
  ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> {
307
215
  await this.checkPrivate();
308
- const modelConstructor = Object.getPrototypeOf(model)
309
- .constructor as PersistentModelConstructor<T>;
310
- const storeName = this.getStorenameForModel(modelConstructor);
311
- const namespaceName = this.namespaceResolver(modelConstructor);
312
-
313
- const connectedModels = traverseModel(
314
- modelConstructor.name,
315
- model,
316
- this.schema.namespaces[namespaceName],
317
- this.modelInstanceCreator,
318
- this.getModelConstructorByModelName!
319
- );
320
216
 
321
- const set = new Set<string>();
322
- const connectionStoreNames = Object.values(connectedModels).map(
323
- ({ modelName, item, instance }) => {
324
- const storeName = getStorename(namespaceName, modelName);
325
- set.add(storeName);
326
- const keys = getIndexKeys(
327
- this.schema.namespaces[namespaceName],
328
- modelName
329
- );
330
- return { storeName, item, instance, keys };
331
- }
332
- );
217
+ const { storeName, set, connectionStoreNames, modelKeyValues } =
218
+ this.saveMetadata(model);
333
219
 
334
220
  const tx = this.db.transaction(
335
221
  [storeName, ...Array.from(set.values())],
336
222
  'readwrite'
337
223
  );
338
- const store = tx.objectStore(storeName);
339
-
340
- const keyValues = this.getIndexKeyValuesFromModel(model);
341
224
 
342
- const fromDB = await this._get(store, keyValues);
343
-
344
- if (condition && fromDB) {
345
- const predicates = ModelPredicateCreator.getPredicates(condition);
346
- const { predicates: predicateObjs, type } = predicates || {};
347
-
348
- const isValid = validatePredicate(
349
- fromDB as any,
350
- type as any,
351
- predicateObjs as any
352
- );
353
-
354
- if (!isValid) {
355
- const msg = 'Conditional update failed';
356
- logger.error(msg, { model: fromDB, condition: predicateObjs });
225
+ const store = tx.objectStore(storeName);
226
+ const fromDB = await this._get(store, modelKeyValues);
357
227
 
358
- throw new Error(msg);
359
- }
360
- }
228
+ this.validateSaveCondition(condition, fromDB);
361
229
 
362
230
  const result: [T, OpType.INSERT | OpType.UPDATE][] = [];
363
231
  for await (const resItem of connectionStoreNames) {
364
232
  const { storeName, item, instance, keys } = resItem;
365
233
  const store = tx.objectStore(storeName);
366
234
 
367
- const itemKeyValues = keys.map(key => {
368
- const value = item[key];
369
- return value;
370
- });
235
+ const itemKeyValues: string[] = keys.map(key => item[key]);
371
236
 
372
237
  const fromDB = <T>await this._get(store, itemKeyValues);
373
- const opType: OpType =
374
- fromDB === undefined ? OpType.INSERT : OpType.UPDATE;
375
-
376
- const modelKeyValues = this.getIndexKeyValuesFromModel(model);
238
+ const opType: OpType = fromDB ? OpType.UPDATE : OpType.INSERT;
377
239
 
378
- // Even if the parent is an INSERT, the child might not be, so we need to get its key
379
240
  if (
380
241
  keysEqual(itemKeyValues, modelKeyValues) ||
381
242
  opType === OpType.INSERT
@@ -387,60 +248,25 @@ class IndexedDBAdapter implements Adapter {
387
248
  result.push([instance, opType]);
388
249
  }
389
250
  }
390
-
391
251
  await tx.done;
392
252
 
393
253
  return result;
394
254
  }
395
255
 
396
- private async load<T>(
397
- namespaceName: NAMESPACES,
398
- srcModelName: string,
399
- records: T[]
400
- ): Promise<T[]> {
401
- const namespace = this.schema.namespaces[namespaceName];
402
- const relations = namespace.relationships![srcModelName].relationTypes;
403
- const connectionStoreNames = relations.map(({ modelName }) => {
404
- return getStorename(namespaceName, modelName);
405
- });
406
- const modelConstructor = this.getModelConstructorByModelName!(
407
- namespaceName,
408
- srcModelName
409
- );
410
-
411
- if (connectionStoreNames.length === 0) {
412
- return records.map(record =>
413
- this.modelInstanceCreator(modelConstructor, record)
414
- );
415
- }
416
-
417
- return records.map(record =>
418
- this.modelInstanceCreator(modelConstructor, record)
419
- );
420
- }
421
-
422
256
  async query<T extends PersistentModel>(
423
257
  modelConstructor: PersistentModelConstructor<T>,
424
258
  predicate?: ModelPredicate<T>,
425
259
  pagination?: PaginationInput<T>
426
260
  ): Promise<T[]> {
427
261
  await this.checkPrivate();
428
- const storeName = this.getStorenameForModel(modelConstructor);
429
- const namespaceName = this.namespaceResolver(
430
- modelConstructor
431
- ) as NAMESPACES;
432
-
433
- const predicates =
434
- predicate && ModelPredicateCreator.getPredicates(predicate);
435
- const keyPath = getIndexKeys(
436
- this.schema.namespaces[namespaceName],
437
- modelConstructor.name
438
- );
439
- const queryByKey =
440
- predicates && this.keyValueFromPredicate(predicates, keyPath);
441
-
442
- const hasSort = pagination && pagination.sort;
443
- const hasPagination = pagination && pagination.limit;
262
+ const {
263
+ storeName,
264
+ namespaceName,
265
+ queryByKey,
266
+ predicates,
267
+ hasSort,
268
+ hasPagination,
269
+ } = this.queryMetadata(modelConstructor, predicate, pagination);
444
270
 
445
271
  const records: T[] = (await (async () => {
446
272
  //
@@ -459,24 +285,307 @@ class IndexedDBAdapter implements Adapter {
459
285
  return record ? [record] : [];
460
286
  }
461
287
 
462
- if (predicates) {
463
- const filtered = await this.filterOnPredicate(storeName, predicates);
464
- return this.inMemoryPagination(filtered, pagination);
465
- }
288
+ if (predicates) {
289
+ const filtered = await this.filterOnPredicate(storeName, predicates);
290
+ return this.inMemoryPagination(filtered, pagination);
291
+ }
292
+
293
+ if (hasSort) {
294
+ const all = await this.getAll(storeName);
295
+ return this.inMemoryPagination(all, pagination);
296
+ }
297
+
298
+ if (hasPagination) {
299
+ return this.enginePagination(storeName, pagination);
300
+ }
301
+
302
+ return this.getAll(storeName);
303
+ })()) as T[];
304
+
305
+ return await this.load(namespaceName, modelConstructor.name, records);
306
+ }
307
+
308
+ async queryOne<T extends PersistentModel>(
309
+ modelConstructor: PersistentModelConstructor<T>,
310
+ firstOrLast: QueryOne = QueryOne.FIRST
311
+ ): Promise<T | undefined> {
312
+ await this.checkPrivate();
313
+ const storeName = this.getStorenameForModel(modelConstructor);
314
+
315
+ const cursor = await this.db
316
+ .transaction([storeName], 'readonly')
317
+ .objectStore(storeName)
318
+ .openCursor(undefined, firstOrLast === QueryOne.FIRST ? 'next' : 'prev');
319
+
320
+ const result = cursor ? <T>cursor.value : undefined;
321
+
322
+ return result && this.modelInstanceCreator(modelConstructor, result);
323
+ }
324
+
325
+ async batchSave<T extends PersistentModel>(
326
+ modelConstructor: PersistentModelConstructor<any>,
327
+ items: ModelInstanceMetadata[]
328
+ ): Promise<[T, OpType][]> {
329
+ await this.checkPrivate();
330
+
331
+ if (items.length === 0) {
332
+ return [];
333
+ }
334
+
335
+ const modelName = modelConstructor.name;
336
+ const namespaceName = this.namespaceResolver(modelConstructor);
337
+ const storeName = this.getStorenameForModel(modelConstructor);
338
+ const result: [T, OpType][] = [];
339
+
340
+ const txn = this.db.transaction(storeName, 'readwrite');
341
+ const store = txn.store;
342
+
343
+ for (const item of items) {
344
+ const model = this.modelInstanceCreator(modelConstructor, item);
345
+
346
+ const connectedModels = traverseModel(
347
+ modelName,
348
+ model,
349
+ this.schema.namespaces[namespaceName],
350
+ this.modelInstanceCreator,
351
+ this.getModelConstructorByModelName!
352
+ );
353
+
354
+ const keyValues = this.getIndexKeyValuesFromModel(model);
355
+ const { _deleted } = item;
356
+
357
+ const index = store.index('byPk');
358
+
359
+ const key = await index.getKey(this.canonicalKeyPath(keyValues));
360
+
361
+ if (!_deleted) {
362
+ const { instance } = connectedModels.find(({ instance }) => {
363
+ const instanceKeyValues = this.getIndexKeyValuesFromModel(instance);
364
+ return keysEqual(instanceKeyValues, keyValues);
365
+ })!;
366
+
367
+ result.push([
368
+ <T>(<unknown>instance),
369
+ key ? OpType.UPDATE : OpType.INSERT,
370
+ ]);
371
+ await store.put(instance, key);
372
+ } else {
373
+ result.push([<T>(<unknown>item), OpType.DELETE]);
374
+
375
+ if (key) {
376
+ await store.delete(key);
377
+ }
378
+ }
379
+ }
380
+
381
+ await txn.done;
382
+
383
+ return result;
384
+ }
385
+
386
+ protected async deleteItem<T extends PersistentModel>(
387
+ deleteQueue?: {
388
+ storeName: string;
389
+ items: T[] | IDBValidKey[];
390
+ }[]
391
+ ) {
392
+ const connectionStoreNames = deleteQueue!.map(({ storeName }) => {
393
+ return storeName;
394
+ });
395
+
396
+ const tx = this.db.transaction([...connectionStoreNames], 'readwrite');
397
+ for await (const deleteItem of deleteQueue!) {
398
+ const { storeName, items } = deleteItem;
399
+ const store = tx.objectStore(storeName);
400
+
401
+ for await (const item of items) {
402
+ if (item) {
403
+ let key: IDBValidKey | undefined;
404
+
405
+ if (typeof item === 'object') {
406
+ const keyValues = this.getIndexKeyValuesFromModel(item as T);
407
+ key = await store
408
+ .index('byPk')
409
+ .getKey(this.canonicalKeyPath(keyValues));
410
+ } else {
411
+ const itemKey = item.toString();
412
+ key = await store.index('byPk').getKey(itemKey);
413
+ }
414
+
415
+ if (key !== undefined) {
416
+ await store.delete(key);
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Gets related Has One record for `model`
425
+ *
426
+ * @param model
427
+ * @param srcModel
428
+ * @param namespace
429
+ * @param rel
430
+ * @returns
431
+ */
432
+ protected async getHasOneChild<T extends PersistentModel>(
433
+ model: T,
434
+ srcModel: string,
435
+ namespace: NAMESPACES,
436
+ rel: RelationType
437
+ ) {
438
+ const hasOneIndex = 'byPk';
439
+ const { modelName, targetNames } = rel;
440
+ const storeName = getStorename(namespace, modelName);
441
+
442
+ const values = targetNames!
443
+ .filter(targetName => model[targetName] ?? false)
444
+ .map(targetName => model[targetName]);
445
+
446
+ if (values.length === 0) return;
447
+
448
+ const recordToDelete = <T>(
449
+ await this.db
450
+ .transaction(storeName, 'readwrite')
451
+ .objectStore(storeName)
452
+ .index(hasOneIndex)
453
+ .get(this.canonicalKeyPath(values))
454
+ );
455
+
456
+ return recordToDelete;
457
+ }
458
+
459
+ /**
460
+ * Backwards compatability for pre-CPK codegen
461
+ * TODO - deprecate this in v6; will need to re-gen MIPR for older unit
462
+ * tests that hit this path
463
+ */
464
+ protected async getHasOneChildLegacy<T extends PersistentModel>(
465
+ model: T,
466
+ srcModel: string,
467
+ namespace: NAMESPACES,
468
+ rel: RelationType
469
+ ) {
470
+ const hasOneIndex = 'byPk';
471
+ const { modelName, targetName } = rel;
472
+ const storeName = getStorename(namespace, modelName);
473
+
474
+ let index;
475
+ let values: string[];
476
+
477
+ if (targetName && targetName in model) {
478
+ index = hasOneIndex;
479
+ const value = model[targetName];
480
+ if (value === null) {
481
+ return;
482
+ }
483
+ values = [value];
484
+ } else {
485
+ // backwards compatability for older versions of codegen that did not emit targetName for HAS_ONE relations
486
+ index = getIndex(
487
+ this.schema.namespaces[namespace].relationships![modelName]
488
+ .relationTypes,
489
+ srcModel
490
+ );
491
+ values = this.getIndexKeyValuesFromModel(model);
492
+ }
493
+
494
+ if (!values || !index) return;
495
+
496
+ const recordToDelete = <T>(
497
+ await this.db
498
+ .transaction(storeName, 'readwrite')
499
+ .objectStore(storeName)
500
+ .index(index)
501
+ .get(this.canonicalKeyPath(values))
502
+ );
503
+
504
+ return recordToDelete;
505
+ }
506
+
507
+ /**
508
+ * Gets related Has Many records by given `storeName`, `index`, and `keyValues`
509
+ *
510
+ * @param storeName
511
+ * @param index
512
+ * @param keyValues
513
+ * @returns
514
+ */
515
+ protected async getHasManyChildren<T extends PersistentModel>(
516
+ storeName: string,
517
+ index: string,
518
+ keyValues: string[]
519
+ ): Promise<T[]> {
520
+ const childRecords = await this.db
521
+ .transaction(storeName, 'readwrite')
522
+ .objectStore(storeName)
523
+ .index(index as string)
524
+ .getAll(this.canonicalKeyPath(keyValues));
525
+
526
+ return childRecords;
527
+ }
528
+
529
+ //#region platform-specific helper methods
530
+
531
+ private async checkPrivate() {
532
+ const isPrivate = await isPrivateMode().then(isPrivate => {
533
+ return isPrivate;
534
+ });
535
+ if (isPrivate) {
536
+ logger.error("IndexedDB not supported in this browser's private mode");
537
+ return Promise.reject(
538
+ "IndexedDB not supported in this browser's private mode"
539
+ );
540
+ } else {
541
+ return Promise.resolve();
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Whether the browser's implementation of IndexedDB is coercing single-field
547
+ * indexes to a scalar key.
548
+ *
549
+ * If this returns `true`, we need to treat indexes containing a single field
550
+ * as scalars.
551
+ *
552
+ * See PR description for reference:
553
+ * https://github.com/aws-amplify/amplify-js/pull/10527
554
+ */
555
+ private async setSafariCompatabilityMode() {
556
+ this.safariCompatabilityMode = await isSafariCompatabilityMode();
557
+
558
+ if (this.safariCompatabilityMode === true) {
559
+ logger.debug('IndexedDB Adapter is running in Safari Compatability Mode');
560
+ }
561
+ }
466
562
 
467
- if (hasSort) {
468
- const all = await this.getAll(storeName);
469
- return this.inMemoryPagination(all, pagination);
470
- }
563
+ private getNamespaceAndModelFromStorename(storeName: string) {
564
+ const [namespaceName, ...modelNameArr] = storeName.split('_');
565
+ return {
566
+ namespaceName,
567
+ modelName: modelNameArr.join('_'),
568
+ };
569
+ }
471
570
 
472
- if (hasPagination) {
473
- return this.enginePagination(storeName, pagination);
474
- }
571
+ private createObjectStoreForModel(
572
+ db: idb.IDBPDatabase,
573
+ namespaceName: string,
574
+ storeName: string,
575
+ modelName: string
576
+ ): idb.IDBPObjectStore {
577
+ const store = db.createObjectStore(storeName, {
578
+ autoIncrement: true,
579
+ });
475
580
 
476
- return this.getAll(storeName);
477
- })()) as T[];
581
+ const { indexes } =
582
+ this.schema.namespaces[namespaceName].relationships![modelName];
478
583
 
479
- return await this.load(namespaceName, modelConstructor.name, records);
584
+ indexes.forEach(([idxName, keyPath, options]) => {
585
+ store.createIndex(idxName, keyPath, options);
586
+ });
587
+
588
+ return store;
480
589
  }
481
590
 
482
591
  private async getByKey<T extends PersistentModel>(
@@ -492,38 +601,6 @@ class IndexedDBAdapter implements Adapter {
492
601
  return await this.db.getAll(storeName);
493
602
  }
494
603
 
495
- private keyValueFromPredicate<T extends PersistentModel>(
496
- predicates: PredicatesGroup<T>,
497
- keyPath: string[]
498
- ): string[] | undefined {
499
- const { predicates: predicateObjs } = predicates;
500
-
501
- if (predicateObjs.length !== keyPath.length) {
502
- return;
503
- }
504
-
505
- const keyValues = [] as any[];
506
-
507
- for (const key of keyPath) {
508
- const predicateObj = predicateObjs.find(
509
- p =>
510
- // it's a relevant predicate object only if it's an equality
511
- // operation for a key field from the key:
512
- isPredicateObj(p) &&
513
- p.field === key &&
514
- p.operator === 'eq' &&
515
- // it's only valid if it's not nullish.
516
- // (IDB will throw a fit if it's nullish.)
517
- p.operand !== null &&
518
- p.operand !== undefined
519
- ) as PredicateObject<T>;
520
-
521
- predicateObj && keyValues.push(predicateObj.operand);
522
- }
523
-
524
- return keyValues.length === keyPath.length ? keyValues : undefined;
525
- }
526
-
527
604
  /**
528
605
  * Tries to generate an index fetcher for the given predicates. Assumes
529
606
  * that the given predicate conditions are contained by an AND group and
@@ -789,447 +866,6 @@ class IndexedDBAdapter implements Adapter {
789
866
  return result;
790
867
  }
791
868
 
792
- async queryOne<T extends PersistentModel>(
793
- modelConstructor: PersistentModelConstructor<T>,
794
- firstOrLast: QueryOne = QueryOne.FIRST
795
- ): Promise<T | undefined> {
796
- await this.checkPrivate();
797
- const storeName = this.getStorenameForModel(modelConstructor);
798
-
799
- const cursor = await this.db
800
- .transaction([storeName], 'readonly')
801
- .objectStore(storeName)
802
- .openCursor(undefined, firstOrLast === QueryOne.FIRST ? 'next' : 'prev');
803
-
804
- const result = cursor ? <T>cursor.value : undefined;
805
-
806
- return result && this.modelInstanceCreator(modelConstructor, result);
807
- }
808
-
809
- async delete<T extends PersistentModel>(
810
- modelOrModelConstructor: T | PersistentModelConstructor<T>,
811
- condition?: ModelPredicate<T>
812
- ): Promise<[T[], T[]]> {
813
- await this.checkPrivate();
814
- const deleteQueue: { storeName: string; items: T[] }[] = [];
815
-
816
- if (isModelConstructor(modelOrModelConstructor)) {
817
- const modelConstructor =
818
- modelOrModelConstructor as PersistentModelConstructor<T>;
819
- const nameSpace = this.namespaceResolver(modelConstructor) as NAMESPACES;
820
-
821
- const storeName = this.getStorenameForModel(modelConstructor);
822
-
823
- const models = await this.query(modelConstructor, condition);
824
- const relations =
825
- this.schema.namespaces![nameSpace].relationships![modelConstructor.name]
826
- .relationTypes;
827
-
828
- if (condition !== undefined) {
829
- await this.deleteTraverse(
830
- relations,
831
- models,
832
- modelConstructor.name,
833
- nameSpace,
834
- deleteQueue
835
- );
836
-
837
- await this.deleteItem(deleteQueue);
838
-
839
- const deletedModels = deleteQueue.reduce(
840
- (acc, { items }) => acc.concat(items),
841
- <T[]>[]
842
- );
843
-
844
- return [models, deletedModels];
845
- } else {
846
- await this.deleteTraverse(
847
- relations,
848
- models,
849
- modelConstructor.name,
850
- nameSpace,
851
- deleteQueue
852
- );
853
-
854
- // Delete all
855
- await this.db
856
- .transaction([storeName], 'readwrite')
857
- .objectStore(storeName)
858
- .clear();
859
-
860
- const deletedModels = deleteQueue.reduce(
861
- (acc, { items }) => acc.concat(items),
862
- <T[]>[]
863
- );
864
-
865
- return [models, deletedModels];
866
- }
867
- } else {
868
- const model = modelOrModelConstructor as T;
869
-
870
- const modelConstructor = Object.getPrototypeOf(model)
871
- .constructor as PersistentModelConstructor<T>;
872
- const namespaceName = this.namespaceResolver(
873
- modelConstructor
874
- ) as NAMESPACES;
875
-
876
- const storeName = this.getStorenameForModel(modelConstructor);
877
-
878
- if (condition) {
879
- const tx = this.db.transaction([storeName], 'readwrite');
880
- const store = tx.objectStore(storeName);
881
- const keyValues = this.getIndexKeyValuesFromModel(model);
882
-
883
- const fromDB = await this._get(store, keyValues);
884
-
885
- if (fromDB === undefined) {
886
- const msg = 'Model instance not found in storage';
887
- logger.warn(msg, { model });
888
-
889
- return [[model], []];
890
- }
891
-
892
- const predicates = ModelPredicateCreator.getPredicates(condition);
893
- const { predicates: predicateObjs, type } =
894
- predicates as PredicatesGroup<T>;
895
-
896
- const isValid = validatePredicate(fromDB as T, type, predicateObjs);
897
-
898
- if (!isValid) {
899
- const msg = 'Conditional update failed';
900
- logger.error(msg, { model: fromDB, condition: predicateObjs });
901
-
902
- throw new Error(msg);
903
- }
904
- await tx.done;
905
-
906
- const relations =
907
- this.schema.namespaces[namespaceName].relationships![
908
- modelConstructor.name
909
- ].relationTypes;
910
-
911
- await this.deleteTraverse(
912
- relations,
913
- [model],
914
- modelConstructor.name,
915
- namespaceName,
916
- deleteQueue
917
- );
918
- } else {
919
- const relations =
920
- this.schema.namespaces[namespaceName].relationships![
921
- modelConstructor.name
922
- ].relationTypes;
923
-
924
- await this.deleteTraverse(
925
- relations,
926
- [model],
927
- modelConstructor.name,
928
- namespaceName,
929
- deleteQueue
930
- );
931
- }
932
-
933
- await this.deleteItem(deleteQueue);
934
-
935
- const deletedModels = deleteQueue.reduce(
936
- (acc, { items }) => acc.concat(items),
937
- <T[]>[]
938
- );
939
-
940
- return [[model], deletedModels];
941
- }
942
- }
943
-
944
- private async deleteItem<T extends PersistentModel>(
945
- deleteQueue?: {
946
- storeName: string;
947
- items: T[] | IDBValidKey[];
948
- }[]
949
- ) {
950
- const connectionStoreNames = deleteQueue!.map(({ storeName }) => {
951
- return storeName;
952
- });
953
-
954
- const tx = this.db.transaction([...connectionStoreNames], 'readwrite');
955
- for await (const deleteItem of deleteQueue!) {
956
- const { storeName, items } = deleteItem;
957
- const store = tx.objectStore(storeName);
958
-
959
- for await (const item of items) {
960
- if (item) {
961
- let key: IDBValidKey | undefined;
962
-
963
- if (typeof item === 'object') {
964
- const keyValues = this.getIndexKeyValuesFromModel(item as T);
965
- key = await store
966
- .index('byPk')
967
- .getKey(this.canonicalKeyPath(keyValues));
968
- } else {
969
- const itemKey = item.toString();
970
- key = await store.index('byPk').getKey(itemKey);
971
- }
972
-
973
- if (key !== undefined) {
974
- await store.delete(key);
975
- }
976
- }
977
- }
978
- }
979
- }
980
-
981
- private async deleteTraverse<T extends PersistentModel>(
982
- relations: RelationType[],
983
- models: T[],
984
- srcModel: string,
985
- nameSpace: NAMESPACES,
986
- deleteQueue: { storeName: string; items: T[] }[]
987
- ): Promise<void> {
988
- for await (const rel of relations) {
989
- const {
990
- relationType,
991
- modelName,
992
- targetName,
993
- targetNames,
994
- associatedWith,
995
- } = rel;
996
-
997
- const storeName = getStorename(nameSpace, modelName);
998
-
999
- switch (relationType) {
1000
- case 'HAS_ONE':
1001
- for await (const model of models) {
1002
- const hasOneIndex = 'byPk';
1003
-
1004
- if (targetNames?.length) {
1005
- // CPK codegen
1006
- const values = targetNames
1007
- .filter(targetName => model[targetName] ?? false)
1008
- .map(targetName => model[targetName]);
1009
-
1010
- if (values.length === 0) break;
1011
-
1012
- const recordToDelete = <T>(
1013
- await this.db
1014
- .transaction(storeName, 'readwrite')
1015
- .objectStore(storeName)
1016
- .index(hasOneIndex)
1017
- .get(this.canonicalKeyPath(values))
1018
- );
1019
-
1020
- await this.deleteTraverse(
1021
- this.schema.namespaces[nameSpace].relationships![modelName]
1022
- .relationTypes,
1023
- recordToDelete ? [recordToDelete] : [],
1024
- modelName,
1025
- nameSpace,
1026
- deleteQueue
1027
- );
1028
- break;
1029
- } else {
1030
- // PRE-CPK codegen
1031
- let index;
1032
- let values: string[];
1033
-
1034
- if (targetName && targetName in model) {
1035
- index = hasOneIndex;
1036
- const value = model[targetName];
1037
- if (value === null) break;
1038
- values = [value];
1039
- } else {
1040
- // backwards compatability for older versions of codegen that did not emit targetName for HAS_ONE relations
1041
- // TODO: can we deprecate this? it's been ~2 years since codegen started including targetName for HAS_ONE
1042
- // If we deprecate, we'll need to re-gen the MIPR in __tests__/schema.ts > newSchema
1043
- // otherwise some unit tests will fail
1044
- index = getIndex(
1045
- this.schema.namespaces[nameSpace].relationships![modelName]
1046
- .relationTypes,
1047
- srcModel
1048
- );
1049
- values = this.getIndexKeyValuesFromModel(model);
1050
- }
1051
-
1052
- if (!values || !index) break;
1053
-
1054
- const recordToDelete = <T>(
1055
- await this.db
1056
- .transaction(storeName, 'readwrite')
1057
- .objectStore(storeName)
1058
- .index(index)
1059
- .get(this.canonicalKeyPath(values))
1060
- );
1061
-
1062
- // instantiate models before passing to deleteTraverse
1063
- // necessary for extracting PK values via getIndexKeyValuesFromModel
1064
- const modelsToDelete = recordToDelete
1065
- ? await this.load(nameSpace, modelName, [recordToDelete])
1066
- : [];
1067
-
1068
- await this.deleteTraverse(
1069
- this.schema.namespaces[nameSpace].relationships![modelName]
1070
- .relationTypes,
1071
- modelsToDelete,
1072
- modelName,
1073
- nameSpace,
1074
- deleteQueue
1075
- );
1076
- }
1077
- }
1078
- break;
1079
- case 'HAS_MANY':
1080
- for await (const model of models) {
1081
- const index =
1082
- // explicit bi-directional @hasMany and @manyToMany
1083
- getIndex(
1084
- this.schema.namespaces[nameSpace].relationships![modelName]
1085
- .relationTypes,
1086
- srcModel
1087
- ) ||
1088
- // uni and/or implicit @hasMany
1089
- getIndexFromAssociation(
1090
- this.schema.namespaces[nameSpace].relationships![modelName]
1091
- .indexes,
1092
- associatedWith!
1093
- );
1094
- const keyValues = this.getIndexKeyValuesFromModel(model);
1095
-
1096
- const childRecords = await this.db
1097
- .transaction(storeName, 'readwrite')
1098
- .objectStore(storeName)
1099
- .index(index as string)
1100
- .getAll(this.canonicalKeyPath(keyValues));
1101
-
1102
- // instantiate models before passing to deleteTraverse
1103
- // necessary for extracting PK values via getIndexKeyValuesFromModel
1104
- const childModels = await this.load(
1105
- nameSpace,
1106
- modelName,
1107
- childRecords
1108
- );
1109
-
1110
- await this.deleteTraverse(
1111
- this.schema.namespaces[nameSpace].relationships![modelName]
1112
- .relationTypes,
1113
- childModels,
1114
- modelName,
1115
- nameSpace,
1116
- deleteQueue
1117
- );
1118
- }
1119
- break;
1120
- case 'BELONGS_TO':
1121
- // Intentionally blank
1122
- break;
1123
- default:
1124
- throw new Error(`Invalid relation type ${relationType}`);
1125
- break;
1126
- }
1127
- }
1128
-
1129
- deleteQueue.push({
1130
- storeName: getStorename(nameSpace, srcModel),
1131
- items: models.map(record =>
1132
- this.modelInstanceCreator(
1133
- this.getModelConstructorByModelName!(nameSpace, srcModel),
1134
- record
1135
- )
1136
- ),
1137
- });
1138
- }
1139
-
1140
- async clear(): Promise<void> {
1141
- await this.checkPrivate();
1142
-
1143
- this.db?.close();
1144
-
1145
- await idb.deleteDB(this.dbName);
1146
-
1147
- this.db = undefined!;
1148
- this.initPromise = undefined!;
1149
- }
1150
-
1151
- async batchSave<T extends PersistentModel>(
1152
- modelConstructor: PersistentModelConstructor<any>,
1153
- items: ModelInstanceMetadata[]
1154
- ): Promise<[T, OpType][]> {
1155
- if (items.length === 0) {
1156
- return [];
1157
- }
1158
-
1159
- await this.checkPrivate();
1160
-
1161
- const result: [T, OpType][] = [];
1162
-
1163
- const storeName = this.getStorenameForModel(modelConstructor);
1164
-
1165
- const txn = this.db.transaction(storeName, 'readwrite');
1166
- const store = txn.store;
1167
-
1168
- for (const item of items) {
1169
- const namespaceName = this.namespaceResolver(modelConstructor);
1170
- const modelName = modelConstructor.name;
1171
- const model = this.modelInstanceCreator(modelConstructor, item);
1172
-
1173
- const connectedModels = traverseModel(
1174
- modelName,
1175
- model,
1176
- this.schema.namespaces[namespaceName],
1177
- this.modelInstanceCreator,
1178
- this.getModelConstructorByModelName!
1179
- );
1180
-
1181
- const keyValues = this.getIndexKeyValuesFromModel(model);
1182
- const { _deleted } = item;
1183
-
1184
- const index = store.index('byPk');
1185
-
1186
- const key = await index.getKey(this.canonicalKeyPath(keyValues));
1187
-
1188
- if (!_deleted) {
1189
- const { instance } = connectedModels.find(({ instance }) => {
1190
- const instanceKeyValues = this.getIndexKeyValuesFromModel(instance);
1191
- return keysEqual(instanceKeyValues, keyValues);
1192
- })!;
1193
-
1194
- result.push([
1195
- <T>(<unknown>instance),
1196
- key ? OpType.UPDATE : OpType.INSERT,
1197
- ]);
1198
- await store.put(instance, key);
1199
- } else {
1200
- result.push([<T>(<unknown>item), OpType.DELETE]);
1201
-
1202
- if (key) {
1203
- await store.delete(key);
1204
- }
1205
- }
1206
- }
1207
-
1208
- await txn.done;
1209
-
1210
- return result;
1211
- }
1212
-
1213
- private createObjectStoreForModel(
1214
- db: idb.IDBPDatabase,
1215
- namespaceName: string,
1216
- storeName: string,
1217
- modelName: string
1218
- ) {
1219
- const store = db.createObjectStore(storeName, {
1220
- autoIncrement: true,
1221
- });
1222
-
1223
- const { indexes } =
1224
- this.schema.namespaces[namespaceName].relationships![modelName];
1225
-
1226
- indexes.forEach(([idxName, keyPath, options]) => {
1227
- store.createIndex(idxName, keyPath, options);
1228
- });
1229
-
1230
- return store;
1231
- }
1232
-
1233
869
  /**
1234
870
  * Checks the given path against the browser's IndexedDB implementation for
1235
871
  * necessary compatibility transformations, applying those transforms if needed.
@@ -1244,6 +880,7 @@ class IndexedDBAdapter implements Adapter {
1244
880
  }
1245
881
  return keyArr;
1246
882
  };
883
+ //#endregion
1247
884
  }
1248
885
 
1249
886
  export default new IndexedDBAdapter();