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.
@@ -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, () => this.rows[name]);
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
- if (prefetch.type === 'all')
70
- return this.getAll(storeName);
71
- const ops = prefetch.type === 'or' ? prefetch.parts : [prefetch];
72
- return this.fetchOps(storeName, entity, ops);
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 needsUpgrade = wanted.some((w) => {
110
- if (!db.objectStoreNames.contains(w.name))
111
- return true;
112
- const existing = this.storeIndexes(w.name).map((i) => i.name);
113
- return w.indexes.some((idx) => !existing.includes(idx.name));
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
- return this.provider.withStore(storeNameOf(this.entity), 'readwrite', async (store) => {
250
- const result = [];
251
- for (const json of rows) {
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
- const key = await idbReq(store.add(json));
254
- if (auto)
255
- json[idName] = json[idName] ?? key;
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
- return this.provider.withStore(storeNameOf(this.entity), 'readwrite', async (store) => {
275
- const existing = await idbReq(store.get(key));
276
- if (existing == null)
277
- throw new Error(`Couldn't find row with id "${id}" in entity "${this.entity.key}" to update`);
278
- const json = { ...existing };
279
- const keys = Object.keys(data);
280
- for (const f of this.entity.fields) {
281
- if (!isDbReadonly(f, names) && keys.includes(f.key)) {
282
- json[names.$dbNameOf(f)] = f.valueConverter.toJson(data[f.key]);
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
- helper.verifyThatRowHasAllNotNullColumns(json, names);
286
- const newKey = keyFromRow(this.entity, names, json);
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(json));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remult",
3
- "version": "3.3.19-next.1",
3
+ "version": "3.3.19-next.3",
4
4
  "description": "A CRUD framework for full-stack TypeScript",
5
5
  "homepage": "https://remult.dev",
6
6
  "repository": {
@@ -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 DataProvider, __RowsOfDataForTesting {
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 () { return _this.rows[name]; });
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 DataProvider {
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';