@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.
- package/CHANGELOG.md +8 -0
- package/lib/storage/adapter/AsyncStorageAdapter.d.ts +52 -28
- package/lib/storage/adapter/AsyncStorageAdapter.js +212 -476
- package/lib/storage/adapter/AsyncStorageAdapter.js.map +1 -1
- package/lib/storage/adapter/AsyncStorageDatabase.js.map +1 -1
- package/lib/storage/adapter/IndexedDBAdapter.d.ts +53 -28
- package/lib/storage/adapter/IndexedDBAdapter.js +589 -892
- package/lib/storage/adapter/IndexedDBAdapter.js.map +1 -1
- package/lib/storage/adapter/StorageAdapterBase.d.ts +146 -0
- package/lib/storage/adapter/StorageAdapterBase.js +479 -0
- package/lib/storage/adapter/StorageAdapterBase.js.map +1 -0
- package/lib-esm/storage/adapter/AsyncStorageAdapter.d.ts +52 -28
- package/lib-esm/storage/adapter/AsyncStorageAdapter.js +215 -479
- package/lib-esm/storage/adapter/AsyncStorageAdapter.js.map +1 -1
- package/lib-esm/storage/adapter/AsyncStorageDatabase.js.map +1 -1
- package/lib-esm/storage/adapter/IndexedDBAdapter.d.ts +53 -28
- package/lib-esm/storage/adapter/IndexedDBAdapter.js +588 -891
- package/lib-esm/storage/adapter/IndexedDBAdapter.js.map +1 -1
- package/lib-esm/storage/adapter/StorageAdapterBase.d.ts +146 -0
- package/lib-esm/storage/adapter/StorageAdapterBase.js +477 -0
- package/lib-esm/storage/adapter/StorageAdapterBase.js.map +1 -0
- package/package.json +6 -6
- package/src/storage/adapter/AsyncStorageAdapter.ts +239 -543
- package/src/storage/adapter/AsyncStorageDatabase.ts +2 -2
- package/src/storage/adapter/IndexedDBAdapter.ts +423 -786
- 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 {
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
*
|
|
120
|
-
*
|
|
72
|
+
* Initialize IndexedDB database
|
|
73
|
+
* Create new DB if one doesn't exist
|
|
74
|
+
* Upgrade outdated DB
|
|
121
75
|
*
|
|
122
|
-
*
|
|
123
|
-
* as scalars.
|
|
76
|
+
* Called by `StorageAdapterBase.setUp()`
|
|
124
77
|
*
|
|
125
|
-
*
|
|
126
|
-
* https://github.com/aws-amplify/amplify-js/pull/10527
|
|
78
|
+
* @returns IDB Database instance
|
|
127
79
|
*/
|
|
128
|
-
|
|
129
|
-
this.
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
135
101
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
124
|
+
const newStore = this.createObjectStoreForModel(
|
|
125
|
+
db,
|
|
126
|
+
namespaceName,
|
|
127
|
+
storeName,
|
|
128
|
+
modelName
|
|
129
|
+
);
|
|
200
130
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
origStore.name = tmpName;
|
|
131
|
+
let cursor = await origStore.openCursor();
|
|
132
|
+
let count = 0;
|
|
204
133
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
139
|
+
cursor = await cursor.continue();
|
|
140
|
+
count++;
|
|
141
|
+
}
|
|
210
142
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
143
|
+
// delete original
|
|
144
|
+
db.deleteObjectStore(tmpName);
|
|
145
|
+
|
|
146
|
+
logger.debug(`${count} ${storeName} records migrated`);
|
|
147
|
+
}
|
|
216
148
|
|
|
217
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
});
|
|
282
180
|
}
|
|
283
181
|
|
|
284
|
-
|
|
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
|
|
322
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
477
|
-
|
|
581
|
+
const { indexes } =
|
|
582
|
+
this.schema.namespaces[namespaceName].relationships![modelName];
|
|
478
583
|
|
|
479
|
-
|
|
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();
|