remult 3.3.19-next.1 → 3.3.19-next.3
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/esm/src/data-providers/in-memory-database.js +12 -1
- package/esm/src/data-providers/indexed-db-data-provider.js +233 -33
- package/esm/src/remult3/RepositoryImplementation.js +8 -0
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/data-interfaces.d.ts +7 -0
- package/src/data-providers/in-memory-database.d.ts +5 -2
- package/src/data-providers/in-memory-database.js +22 -1
- package/src/data-providers/indexed-db-data-provider.d.ts +26 -2
- package/src/data-providers/indexed-db-data-provider.js +545 -134
- package/src/remult3/RepositoryImplementation.d.ts +2 -2
- package/src/remult3/RepositoryImplementation.js +245 -211
- package/src/remult3/remult3.d.ts +15 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getEntityMetadata, } from '../remult3/RepositoryImplementation.js';
|
|
1
2
|
import { ArrayEntityDataProvider } from './array-entity-data-provider.js';
|
|
2
3
|
export class InMemoryDataProvider {
|
|
3
4
|
async transaction(action) {
|
|
@@ -15,7 +16,17 @@ export class InMemoryDataProvider {
|
|
|
15
16
|
let name = entity.dbName;
|
|
16
17
|
if (!this.rows[name])
|
|
17
18
|
this.rows[name] = [];
|
|
18
|
-
return new ArrayEntityDataProvider(entity, () =>
|
|
19
|
+
return new ArrayEntityDataProvider(entity, () => {
|
|
20
|
+
if (!this.rows[name])
|
|
21
|
+
this.rows[name] = [];
|
|
22
|
+
return this.rows[name];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async dropDatabase() {
|
|
26
|
+
this.rows = {};
|
|
27
|
+
}
|
|
28
|
+
async dropTable(entity) {
|
|
29
|
+
delete this.rows[getEntityMetadata(entity).dbName];
|
|
19
30
|
}
|
|
20
31
|
toString() {
|
|
21
32
|
return 'InMemoryDataProvider';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dbNamesOf, isDbReadonly, } from '../filter/filter-consumer-bridge-to-sql-request.js';
|
|
2
|
-
import { isAutoIncrement } from '../remult3/RepositoryImplementation.js';
|
|
2
|
+
import { getEntityMetadata, isAutoIncrement, } from '../remult3/RepositoryImplementation.js';
|
|
3
3
|
import { ArrayEntityDataProvider } from './array-entity-data-provider.js';
|
|
4
4
|
export class IndexedDbIndexBuilder {
|
|
5
5
|
//@internal
|
|
@@ -9,12 +9,27 @@ export class IndexedDbIndexBuilder {
|
|
|
9
9
|
return this;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
const IDB_KEYS_STORE = '__remult_keys';
|
|
13
|
+
const IDB_DEK_ID = 'dek';
|
|
14
|
+
const IDB_ENC = '_enc';
|
|
15
|
+
const IDB_IV = '_iv';
|
|
12
16
|
export class IndexedDbDataProvider {
|
|
13
17
|
dbName;
|
|
14
18
|
//@internal
|
|
15
19
|
declaredIndexes = [];
|
|
20
|
+
//@internal
|
|
21
|
+
encrypt;
|
|
22
|
+
//@internal
|
|
23
|
+
customGetKey;
|
|
24
|
+
//@internal
|
|
25
|
+
dek;
|
|
16
26
|
constructor(dbName = 'remult', options) {
|
|
17
27
|
this.dbName = dbName;
|
|
28
|
+
this.encrypt = options?.encrypt === true;
|
|
29
|
+
this.customGetKey = options?.getEncryptionKey;
|
|
30
|
+
if (this.encrypt && !(typeof crypto !== 'undefined' && crypto.subtle)) {
|
|
31
|
+
throw new Error('Web Crypto API is required when IndexedDB encryption is enabled');
|
|
32
|
+
}
|
|
18
33
|
if (options?.indexes) {
|
|
19
34
|
const builder = new IndexedDbIndexBuilder();
|
|
20
35
|
options.indexes(builder);
|
|
@@ -40,6 +55,29 @@ export class IndexedDbDataProvider {
|
|
|
40
55
|
this.db?.close();
|
|
41
56
|
this.db = undefined;
|
|
42
57
|
}
|
|
58
|
+
async dropDatabase() {
|
|
59
|
+
await this.enqueue(async () => {
|
|
60
|
+
this.close();
|
|
61
|
+
this.dek = undefined;
|
|
62
|
+
await idbDeleteDatabase(this.dbName);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async dropTable(entity) {
|
|
66
|
+
await this.enqueue(async () => {
|
|
67
|
+
const name = storeNameOf(getEntityMetadata(entity));
|
|
68
|
+
const db = await this.open();
|
|
69
|
+
if (!db.objectStoreNames.contains(name))
|
|
70
|
+
return;
|
|
71
|
+
const nextVersion = db.version + 1;
|
|
72
|
+
this.forget(db);
|
|
73
|
+
db.close();
|
|
74
|
+
await this.open(nextVersion, (upgradeDb) => {
|
|
75
|
+
if (upgradeDb.objectStoreNames.contains(name)) {
|
|
76
|
+
upgradeDb.deleteObjectStore(name);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
43
81
|
//@internal
|
|
44
82
|
enqueue(fn) {
|
|
45
83
|
const run = this.tail.then(fn, fn);
|
|
@@ -66,10 +104,14 @@ export class IndexedDbDataProvider {
|
|
|
66
104
|
const storeName = storeNameOf(entity);
|
|
67
105
|
const prefetch = await idbPrefetchFromFilter(entity, where, this.storeIndexes(storeName));
|
|
68
106
|
this.lastFetch = prefetch;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
const rows = prefetch.type === 'all'
|
|
108
|
+
? await this.getAll(storeName)
|
|
109
|
+
: await this.fetchOps(storeName, entity, prefetch.type === 'or' ? prefetch.parts : [prefetch]);
|
|
110
|
+
if (!this.encrypt)
|
|
111
|
+
return rows;
|
|
112
|
+
const dek = await this.loadDek();
|
|
113
|
+
const names = await dbNamesOf(entity, (x) => x);
|
|
114
|
+
return Promise.all(rows.map((row) => decryptJson(storeName, row, entity, names, dek)));
|
|
73
115
|
}
|
|
74
116
|
//@internal
|
|
75
117
|
storeIndexes(storeName) {
|
|
@@ -106,18 +148,24 @@ export class IndexedDbDataProvider {
|
|
|
106
148
|
};
|
|
107
149
|
}));
|
|
108
150
|
const db = await this.open();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
151
|
+
const needsKeyStore = this.usesKeyStore() && !db.objectStoreNames.contains(IDB_KEYS_STORE);
|
|
152
|
+
const needsUpgrade = needsKeyStore ||
|
|
153
|
+
wanted.some((w) => {
|
|
154
|
+
if (!db.objectStoreNames.contains(w.name))
|
|
155
|
+
return true;
|
|
156
|
+
const existing = this.storeIndexes(w.name).map((i) => i.name);
|
|
157
|
+
return w.indexes.some((idx) => !existing.includes(idx.name));
|
|
158
|
+
});
|
|
115
159
|
if (!needsUpgrade)
|
|
116
160
|
return;
|
|
117
161
|
const nextVersion = db.version + 1;
|
|
118
162
|
this.forget(db);
|
|
119
163
|
db.close();
|
|
120
164
|
await this.open(nextVersion, (upgradeDb, tx) => {
|
|
165
|
+
if (this.usesKeyStore() &&
|
|
166
|
+
!upgradeDb.objectStoreNames.contains(IDB_KEYS_STORE)) {
|
|
167
|
+
upgradeDb.createObjectStore(IDB_KEYS_STORE, { keyPath: 'id' });
|
|
168
|
+
}
|
|
121
169
|
for (const { name, keyPath, autoIncrement, indexes } of wanted) {
|
|
122
170
|
const store = upgradeDb.objectStoreNames.contains(name)
|
|
123
171
|
? tx.objectStore(name)
|
|
@@ -131,6 +179,63 @@ export class IndexedDbDataProvider {
|
|
|
131
179
|
});
|
|
132
180
|
}
|
|
133
181
|
//@internal
|
|
182
|
+
usesKeyStore() {
|
|
183
|
+
return this.encrypt && !this.customGetKey;
|
|
184
|
+
}
|
|
185
|
+
//@internal
|
|
186
|
+
async loadDek() {
|
|
187
|
+
if (this.dek)
|
|
188
|
+
return this.dek;
|
|
189
|
+
if (this.customGetKey) {
|
|
190
|
+
const key = await this.customGetKey();
|
|
191
|
+
this.dek = key;
|
|
192
|
+
return key;
|
|
193
|
+
}
|
|
194
|
+
const existing = await this.withStore(IDB_KEYS_STORE, 'readonly', (s) => idbReq(s.get(IDB_DEK_ID)));
|
|
195
|
+
if (existing?.key) {
|
|
196
|
+
this.dek = existing.key;
|
|
197
|
+
return existing.key;
|
|
198
|
+
}
|
|
199
|
+
const generated = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
200
|
+
const key = await this.withStore(IDB_KEYS_STORE, 'readwrite', async (s) => {
|
|
201
|
+
const rec = await idbReq(s.get(IDB_DEK_ID));
|
|
202
|
+
if (rec?.key)
|
|
203
|
+
return rec.key;
|
|
204
|
+
try {
|
|
205
|
+
await idbReq(s.add({ id: IDB_DEK_ID, key: generated }));
|
|
206
|
+
return generated;
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
if (e?.name === 'ConstraintError') {
|
|
210
|
+
const again = await idbReq(s.get(IDB_DEK_ID));
|
|
211
|
+
if (!again?.key)
|
|
212
|
+
throw new Error('Failed to load IndexedDB encryption key');
|
|
213
|
+
return again.key;
|
|
214
|
+
}
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
this.dek = key;
|
|
219
|
+
return key;
|
|
220
|
+
}
|
|
221
|
+
//@internal
|
|
222
|
+
async plaintextDbNames(entity) {
|
|
223
|
+
const names = await dbNamesOf(entity, (x) => x);
|
|
224
|
+
const set = new Set();
|
|
225
|
+
for (const f of entity.idMetadata.fields) {
|
|
226
|
+
set.add(names.$dbNameOf(f));
|
|
227
|
+
}
|
|
228
|
+
for (const def of this.indexesFor(entity)) {
|
|
229
|
+
const keys = Array.isArray(def) ? [...def] : [def];
|
|
230
|
+
for (const key of keys) {
|
|
231
|
+
const field = [...entity.fields].find((f) => f.key === key);
|
|
232
|
+
if (field)
|
|
233
|
+
set.add(names.$dbNameOf(field));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return set;
|
|
237
|
+
}
|
|
238
|
+
//@internal
|
|
134
239
|
forget(db) {
|
|
135
240
|
if (this.db === db)
|
|
136
241
|
this.db = undefined;
|
|
@@ -246,23 +351,48 @@ class IndexedDbEntityDataProvider {
|
|
|
246
351
|
delete json[idName];
|
|
247
352
|
return json;
|
|
248
353
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
354
|
+
const storeName = storeNameOf(this.entity);
|
|
355
|
+
const dek = this.provider.encrypt
|
|
356
|
+
? await this.provider.loadDek()
|
|
357
|
+
: undefined;
|
|
358
|
+
const keep = dek
|
|
359
|
+
? await this.provider.plaintextDbNames(this.entity)
|
|
360
|
+
: undefined;
|
|
361
|
+
if (dek && auto) {
|
|
362
|
+
const keys = await this.provider.withStore(storeName, 'readwrite', async (store) => {
|
|
363
|
+
const allocated = [];
|
|
364
|
+
for (let i = 0; i < rows.length; i++) {
|
|
365
|
+
allocated.push(await idbReq(store.add({})));
|
|
366
|
+
}
|
|
367
|
+
return allocated;
|
|
368
|
+
});
|
|
369
|
+
for (let i = 0; i < rows.length; i++) {
|
|
370
|
+
rows[i][idName] = keys[i];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const toWrite = dek
|
|
374
|
+
? await Promise.all(rows.map((json) => encryptJson(storeName, json, keep, keyFromRow(this.entity, names, json), dek)))
|
|
375
|
+
: rows;
|
|
376
|
+
await this.provider.withStore(storeName, 'readwrite', async (store) => {
|
|
377
|
+
for (let i = 0; i < toWrite.length; i++) {
|
|
252
378
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
379
|
+
if (dek && auto) {
|
|
380
|
+
await idbReq(store.put(toWrite[i]));
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const key = await idbReq(store.add(toWrite[i]));
|
|
384
|
+
if (auto)
|
|
385
|
+
rows[i][idName] = rows[i][idName] ?? key;
|
|
386
|
+
}
|
|
256
387
|
}
|
|
257
388
|
catch (e) {
|
|
258
389
|
if (e?.name === 'ConstraintError')
|
|
259
390
|
throw Error('id already exists');
|
|
260
391
|
throw e;
|
|
261
392
|
}
|
|
262
|
-
result.push(helper.translateFromJson(json, names));
|
|
263
393
|
}
|
|
264
|
-
return result;
|
|
265
394
|
});
|
|
395
|
+
return rows.map((json) => helper.translateFromJson(json, names));
|
|
266
396
|
});
|
|
267
397
|
}
|
|
268
398
|
update(id, data) {
|
|
@@ -271,28 +401,38 @@ class IndexedDbEntityDataProvider {
|
|
|
271
401
|
const helper = new ArrayEntityDataProvider(this.entity, () => []);
|
|
272
402
|
const names = await helper.init();
|
|
273
403
|
const key = toIdbKey(this.entity, id);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
404
|
+
const storeName = storeNameOf(this.entity);
|
|
405
|
+
const existingRaw = await this.provider.withStore(storeName, 'readonly', (store) => idbReq(store.get(key)));
|
|
406
|
+
if (existingRaw == null)
|
|
407
|
+
throw new Error(`Couldn't find row with id "${id}" in entity "${this.entity.key}" to update`);
|
|
408
|
+
const dek = this.provider.encrypt
|
|
409
|
+
? await this.provider.loadDek()
|
|
410
|
+
: undefined;
|
|
411
|
+
const existing = dek
|
|
412
|
+
? await decryptJson(storeName, existingRaw, this.entity, names, dek)
|
|
413
|
+
: existingRaw;
|
|
414
|
+
const json = { ...existing };
|
|
415
|
+
const keys = Object.keys(data);
|
|
416
|
+
for (const f of this.entity.fields) {
|
|
417
|
+
if (!isDbReadonly(f, names) && keys.includes(f.key)) {
|
|
418
|
+
json[names.$dbNameOf(f)] = f.valueConverter.toJson(data[f.key]);
|
|
284
419
|
}
|
|
285
|
-
|
|
286
|
-
|
|
420
|
+
}
|
|
421
|
+
helper.verifyThatRowHasAllNotNullColumns(json, names);
|
|
422
|
+
const newKey = keyFromRow(this.entity, names, json);
|
|
423
|
+
const toWrite = dek
|
|
424
|
+
? await encryptJson(storeName, json, await this.provider.plaintextDbNames(this.entity), newKey, dek)
|
|
425
|
+
: json;
|
|
426
|
+
await this.provider.withStore(storeName, 'readwrite', async (store) => {
|
|
287
427
|
if (!idbKeysEqual(key, newKey)) {
|
|
288
428
|
const conflict = await idbReq(store.get(newKey));
|
|
289
429
|
if (conflict != null)
|
|
290
430
|
throw Error('id already exists');
|
|
291
431
|
await idbReq(store.delete(key));
|
|
292
432
|
}
|
|
293
|
-
await idbReq(store.put(
|
|
294
|
-
return helper.translateFromJson(json, names);
|
|
433
|
+
await idbReq(store.put(toWrite));
|
|
295
434
|
});
|
|
435
|
+
return helper.translateFromJson(json, names);
|
|
296
436
|
});
|
|
297
437
|
}
|
|
298
438
|
delete(ids) {
|
|
@@ -642,3 +782,63 @@ function idbReq(request) {
|
|
|
642
782
|
request.onerror = () => reject(request.error);
|
|
643
783
|
});
|
|
644
784
|
}
|
|
785
|
+
function idbDeleteDatabase(name) {
|
|
786
|
+
return new Promise((resolve, reject) => {
|
|
787
|
+
const req = indexedDB.deleteDatabase(name);
|
|
788
|
+
req.onsuccess = () => resolve();
|
|
789
|
+
req.onerror = () => reject(req.error);
|
|
790
|
+
req.onblocked = () => {
|
|
791
|
+
/* wait for onsuccess after other connections close */
|
|
792
|
+
};
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
function encAad(storeName, id) {
|
|
796
|
+
return new TextEncoder().encode(storeName + '\0' + JSON.stringify(id));
|
|
797
|
+
}
|
|
798
|
+
function asBufferSource(value, label) {
|
|
799
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer)
|
|
800
|
+
return value;
|
|
801
|
+
throw new Error(`Failed to decrypt IndexedDB row: missing ${label}`);
|
|
802
|
+
}
|
|
803
|
+
async function encryptJson(storeName, json, plaintext, idbKey, dek) {
|
|
804
|
+
const stored = {};
|
|
805
|
+
const payload = {};
|
|
806
|
+
for (const k of Object.keys(json)) {
|
|
807
|
+
if (k === IDB_ENC || k === IDB_IV)
|
|
808
|
+
continue;
|
|
809
|
+
if (plaintext.has(k))
|
|
810
|
+
stored[k] = json[k];
|
|
811
|
+
else
|
|
812
|
+
payload[k] = json[k];
|
|
813
|
+
}
|
|
814
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
815
|
+
stored[IDB_ENC] = await crypto.subtle.encrypt({
|
|
816
|
+
name: 'AES-GCM',
|
|
817
|
+
iv,
|
|
818
|
+
additionalData: encAad(storeName, idbKey),
|
|
819
|
+
}, dek, new TextEncoder().encode(JSON.stringify(payload)));
|
|
820
|
+
stored[IDB_IV] = iv;
|
|
821
|
+
return stored;
|
|
822
|
+
}
|
|
823
|
+
async function decryptJson(storeName, row, entity, names, dek) {
|
|
824
|
+
if (row == null || row[IDB_ENC] == null)
|
|
825
|
+
return row;
|
|
826
|
+
const idbKey = keyFromRow(entity, names, row);
|
|
827
|
+
try {
|
|
828
|
+
const plain = await crypto.subtle.decrypt({
|
|
829
|
+
name: 'AES-GCM',
|
|
830
|
+
iv: asBufferSource(row[IDB_IV], IDB_IV),
|
|
831
|
+
additionalData: encAad(storeName, idbKey),
|
|
832
|
+
}, dek, asBufferSource(row[IDB_ENC], IDB_ENC));
|
|
833
|
+
const payload = JSON.parse(new TextDecoder().decode(plain));
|
|
834
|
+
const result = { ...row, ...payload };
|
|
835
|
+
delete result[IDB_ENC];
|
|
836
|
+
delete result[IDB_IV];
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
catch (e) {
|
|
840
|
+
if (e?.message?.startsWith('Failed to decrypt IndexedDB row'))
|
|
841
|
+
throw e;
|
|
842
|
+
throw new Error(`Failed to decrypt IndexedDB row in "${storeName}"`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
@@ -323,6 +323,14 @@ export class RepositoryImplementation {
|
|
|
323
323
|
}
|
|
324
324
|
async insert(entity, options) {
|
|
325
325
|
if (Array.isArray(entity)) {
|
|
326
|
+
if (!options?.bulk) {
|
|
327
|
+
const r = [];
|
|
328
|
+
for (const item of entity)
|
|
329
|
+
r.push(await this.insert(item, options));
|
|
330
|
+
if (options?.select === 'none')
|
|
331
|
+
return undefined;
|
|
332
|
+
return r;
|
|
333
|
+
}
|
|
326
334
|
let refs = [];
|
|
327
335
|
let raw = [];
|
|
328
336
|
for (const item of entity) {
|
package/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { IdEntity } from './src/remult3/IdEntity.js';
|
|
|
23
23
|
export { describeClass, describeBackendMethods, describeEntity, } from './src/remult3/classDescribers.js';
|
|
24
24
|
export type { EntityOptions, PreprocessFilterEvent } from './src/entity.js';
|
|
25
25
|
export { EntityError } from './src/data-interfaces.js';
|
|
26
|
-
export type { DataProvider, EntityDataProvider, EntityDataProviderGroupByOptions, EntityDataProviderFindOptions, ErrorInfo, RestDataProviderHttpProvider, } from './src/data-interfaces.js';
|
|
26
|
+
export type { DataProvider, DroppableDataProvider, EntityDataProvider, EntityDataProviderGroupByOptions, EntityDataProviderFindOptions, ErrorInfo, RestDataProviderHttpProvider, } from './src/data-interfaces.js';
|
|
27
27
|
export type { SqlCommand, SqlCommandWithParameters, SqlImplementation, SqlResult, } from './src/sql-command.js';
|
|
28
28
|
export type { FieldMetadata, FieldOptions, FieldValidator, ValueConverter, ValueListItem, // reconsider, maybe it should go to remult angular as the abstraction ?
|
|
29
29
|
ValueOrExpression, } from './src/column-interfaces.js';
|
package/package.json
CHANGED
package/src/data-interfaces.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FieldMetadata } from '../index.js';
|
|
2
2
|
import type { Filter } from './filter/filter-interfaces.js';
|
|
3
3
|
import type { GroupByOperators, EntityMetadata, MembersOnly, InsertOrUpdateOptions } from './remult3/remult3.js';
|
|
4
|
+
import type { EntityMetadataOverloads } from './remult3/RepositoryImplementation.js';
|
|
4
5
|
import { Sort } from './sort.js';
|
|
5
6
|
export interface DataProvider {
|
|
6
7
|
getEntityDataProvider(entity: EntityMetadata): EntityDataProvider;
|
|
@@ -8,6 +9,12 @@ export interface DataProvider {
|
|
|
8
9
|
ensureSchema?(entities: EntityMetadata[]): Promise<void>;
|
|
9
10
|
isProxy?: boolean;
|
|
10
11
|
}
|
|
12
|
+
/** `InMemoryDataProvider` + `IndexedDbDataProvider` — swap in tests. */
|
|
13
|
+
export interface DroppableDataProvider extends DataProvider {
|
|
14
|
+
dropDatabase(): Promise<void>;
|
|
15
|
+
/** Recreated with indexes on next use (`ensureSchema` / first write). */
|
|
16
|
+
dropTable(entity: EntityMetadataOverloads): Promise<void>;
|
|
17
|
+
}
|
|
11
18
|
export interface Storage {
|
|
12
19
|
ensureSchema(): Promise<void>;
|
|
13
20
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { __RowsOfDataForTesting } from '../__RowsOfDataForTesting.js';
|
|
2
|
-
import type { DataProvider, EntityDataProvider } from '../data-interfaces.js';
|
|
2
|
+
import type { DataProvider, DroppableDataProvider, EntityDataProvider } from '../data-interfaces.js';
|
|
3
|
+
import { type EntityMetadataOverloads } from '../remult3/RepositoryImplementation.js';
|
|
3
4
|
import type { EntityMetadata } from '../remult3/remult3.js';
|
|
4
|
-
export declare class InMemoryDataProvider implements
|
|
5
|
+
export declare class InMemoryDataProvider implements DroppableDataProvider, __RowsOfDataForTesting {
|
|
5
6
|
transaction(action: (dataProvider: DataProvider) => Promise<void>): Promise<void>;
|
|
6
7
|
rows: any;
|
|
7
8
|
getEntityDataProvider(entity: EntityMetadata): EntityDataProvider;
|
|
9
|
+
dropDatabase(): Promise<void>;
|
|
10
|
+
dropTable(entity: EntityMetadataOverloads): Promise<void>;
|
|
8
11
|
toString(): string;
|
|
9
12
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemoryDataProvider = void 0;
|
|
4
4
|
var tslib_1 = require("tslib");
|
|
5
|
+
var RepositoryImplementation_js_1 = require("../remult3/RepositoryImplementation.js");
|
|
5
6
|
var array_entity_data_provider_js_1 = require("./array-entity-data-provider.js");
|
|
6
7
|
var InMemoryDataProvider = /** @class */ (function () {
|
|
7
8
|
function InMemoryDataProvider() {
|
|
@@ -35,7 +36,27 @@ var InMemoryDataProvider = /** @class */ (function () {
|
|
|
35
36
|
var name = entity.dbName;
|
|
36
37
|
if (!this.rows[name])
|
|
37
38
|
this.rows[name] = [];
|
|
38
|
-
return new array_entity_data_provider_js_1.ArrayEntityDataProvider(entity, function () {
|
|
39
|
+
return new array_entity_data_provider_js_1.ArrayEntityDataProvider(entity, function () {
|
|
40
|
+
if (!_this.rows[name])
|
|
41
|
+
_this.rows[name] = [];
|
|
42
|
+
return _this.rows[name];
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
InMemoryDataProvider.prototype.dropDatabase = function () {
|
|
46
|
+
return tslib_1.__awaiter(this, void 0, void 0, function () {
|
|
47
|
+
return tslib_1.__generator(this, function (_a) {
|
|
48
|
+
this.rows = {};
|
|
49
|
+
return [2 /*return*/];
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
InMemoryDataProvider.prototype.dropTable = function (entity) {
|
|
54
|
+
return tslib_1.__awaiter(this, void 0, void 0, function () {
|
|
55
|
+
return tslib_1.__generator(this, function (_a) {
|
|
56
|
+
delete this.rows[(0, RepositoryImplementation_js_1.getEntityMetadata)(entity).dbName];
|
|
57
|
+
return [2 /*return*/];
|
|
58
|
+
});
|
|
59
|
+
});
|
|
39
60
|
};
|
|
40
61
|
InMemoryDataProvider.prototype.toString = function () {
|
|
41
62
|
return 'InMemoryDataProvider';
|
|
@@ -1,20 +1,44 @@
|
|
|
1
|
-
import type { DataProvider, EntityDataProvider } from '../data-interfaces.js';
|
|
1
|
+
import type { DataProvider, DroppableDataProvider, EntityDataProvider } from '../data-interfaces.js';
|
|
2
2
|
import type { ClassType } from '../../classType.js';
|
|
3
3
|
import type { EntityMetadata, MembersOnly } from '../remult3/remult3.js';
|
|
4
|
+
import { type EntityMetadataOverloads } from '../remult3/RepositoryImplementation.js';
|
|
4
5
|
export type IndexedDbIndexDef<entityType> = keyof MembersOnly<entityType> | readonly (keyof MembersOnly<entityType>)[];
|
|
5
6
|
export declare class IndexedDbIndexBuilder {
|
|
6
7
|
ensureIndexes<entityType>(entity: ClassType<entityType>, indexes: readonly IndexedDbIndexDef<entityType>[]): this;
|
|
7
8
|
}
|
|
8
9
|
export type IndexedDbDataProviderOptions = {
|
|
10
|
+
/** IDB-only indexes (not unique). PK is skipped. Compound names join db names with `_`. */
|
|
9
11
|
indexes?: (x: IndexedDbIndexBuilder) => void;
|
|
12
|
+
/**
|
|
13
|
+
* AES-GCM 256 on non-indexed fields (`_enc` + `_iv`). Requires `crypto.subtle`.
|
|
14
|
+
*
|
|
15
|
+
* Not full security. Stops casual disk/profile dump and other origins
|
|
16
|
+
* (non-extractable `CryptoKey` wrapped by the browser). Does **not** protect
|
|
17
|
+
* against same-origin XSS / any JS that can use the stored `CryptoKey` to decrypt.
|
|
18
|
+
*
|
|
19
|
+
* PK + `ensureIndexes` fields stay plaintext (required for IDB keyPath/indexes).
|
|
20
|
+
* Only non-indexed fields go into `_enc`.
|
|
21
|
+
*
|
|
22
|
+
* Default auto-key is origin-bound theater vs XSS. Pass `getEncryptionKey` for
|
|
23
|
+
* a stronger secret you control.
|
|
24
|
+
*/
|
|
25
|
+
encrypt?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Your AES-GCM `CryptoKey` (or Promise). Skips `__remult_keys`. Stronger than
|
|
28
|
+
* the default origin-bound auto-key — still not XSS-safe if same-origin JS can
|
|
29
|
+
* call this and decrypt.
|
|
30
|
+
*/
|
|
31
|
+
getEncryptionKey?: () => CryptoKey | Promise<CryptoKey>;
|
|
10
32
|
};
|
|
11
|
-
export declare class IndexedDbDataProvider implements
|
|
33
|
+
export declare class IndexedDbDataProvider implements DroppableDataProvider {
|
|
12
34
|
private dbName;
|
|
13
35
|
constructor(dbName?: string, options?: IndexedDbDataProviderOptions);
|
|
14
36
|
getEntityDataProvider(entity: EntityMetadata): EntityDataProvider;
|
|
15
37
|
transaction(action: (dataProvider: DataProvider) => Promise<void>): Promise<void>;
|
|
16
38
|
ensureSchema(entities: EntityMetadata[]): Promise<void>;
|
|
17
39
|
close(): void;
|
|
40
|
+
dropDatabase(): Promise<void>;
|
|
41
|
+
dropTable(entity: EntityMetadataOverloads): Promise<void>;
|
|
18
42
|
}
|
|
19
43
|
export type IdbFetchOp = {
|
|
20
44
|
type: 'keys';
|