idb-ts 3.14.0 → 3.16.0

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/lib/index.cjs CHANGED
@@ -219,6 +219,34 @@ class QueryBuilder {
219
219
  this.groupField = field;
220
220
  return this;
221
221
  }
222
+ clone() {
223
+ const copy = new QueryBuilder(this.db, this.storeName, this.transaction);
224
+ copy.clauses = [...this.clauses];
225
+ copy.orderField = this.orderField;
226
+ copy.orderDirection = this.orderDirection;
227
+ copy.limitCount = this.limitCount;
228
+ copy.offsetCount = this.offsetCount;
229
+ copy.indexName = this.indexName;
230
+ copy.rangeStart = this.rangeStart;
231
+ copy.rangeEnd = this.rangeEnd;
232
+ copy.groupField = this.groupField;
233
+ copy.pendingConnector = this.pendingConnector;
234
+ return copy;
235
+ }
236
+ reset() {
237
+ this.clauses = [];
238
+ this.orderField = undefined;
239
+ this.orderDirection = 'asc';
240
+ this.limitCount = undefined;
241
+ this.offsetCount = undefined;
242
+ this.indexName = undefined;
243
+ this.rangeStart = undefined;
244
+ this.rangeEnd = undefined;
245
+ this.currentField = undefined;
246
+ this.groupField = undefined;
247
+ this.pendingConnector = 'and';
248
+ return this;
249
+ }
222
250
  loadCandidates() {
223
251
  return tslib.__awaiter(this, void 0, void 0, function* () {
224
252
  const store = this.transaction
@@ -235,6 +263,12 @@ class QueryBuilder {
235
263
  }
236
264
  createReadRequest(store) {
237
265
  if (!this.indexName) {
266
+ if (this.rangeStart !== undefined || this.rangeEnd !== undefined) {
267
+ throw new Error('range() requires useIndex(): key ranges are applied at the ' +
268
+ 'IndexedDB layer through an index. Either call useIndex(indexName) ' +
269
+ 'before range(), or express the bounds as in-memory filters via ' +
270
+ 'where(field).between(start, end) / gte() / lte().');
271
+ }
238
272
  return store.getAll();
239
273
  }
240
274
  if (!store.indexNames.contains(this.indexName)) {
@@ -431,12 +465,65 @@ class QueryBuilder {
431
465
  const INTERNAL_CREATED_AT_FIELD = '__idb_createdAt';
432
466
  const INTERNAL_UPDATED_AT_FIELD = '__idb_updatedAt';
433
467
  class KeyGenerators {
468
+ static getRandomValues(array) {
469
+ var _a;
470
+ if (typeof globalThis !== 'undefined' && ((_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.getRandomValues)) {
471
+ globalThis.crypto.getRandomValues(array);
472
+ return array;
473
+ }
474
+ try {
475
+ const { randomBytes } = require('crypto');
476
+ const bytes = randomBytes(array.length);
477
+ return new Uint8Array(bytes);
478
+ }
479
+ catch (_b) {
480
+ for (let i = 0; i < array.length; i++) {
481
+ array[i] = Math.random() * 256 | 0;
482
+ }
483
+ return array;
484
+ }
485
+ }
486
+ static uuidV4() {
487
+ var _a;
488
+ try {
489
+ if (typeof globalThis !== 'undefined' && ((_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID)) {
490
+ return globalThis.crypto.randomUUID();
491
+ }
492
+ }
493
+ catch (_b) {
494
+ }
495
+ try {
496
+ const { randomUUID } = require('crypto');
497
+ return randomUUID();
498
+ }
499
+ catch (_c) {
500
+ }
501
+ const bytes = new Uint8Array(16);
502
+ this.getRandomValues(bytes);
503
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
504
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
505
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
506
+ return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`;
507
+ }
508
+ static uuidV6() {
509
+ const bytes = new Uint8Array(16);
510
+ this.getRandomValues(bytes);
511
+ const now = Date.now();
512
+ const timeHi = Math.floor(now / 1000) & 0xffffffff;
513
+ const timeLo = (now % 1000) * 4096;
514
+ bytes[0] = (timeHi >>> 24) & 0xff;
515
+ bytes[1] = (timeHi >>> 16) & 0xff;
516
+ bytes[2] = (timeHi >>> 8) & 0xff;
517
+ bytes[3] = timeHi & 0xff;
518
+ bytes[4] = (timeLo >>> 8) & 0xff;
519
+ bytes[5] = timeLo & 0xff;
520
+ bytes[6] = (bytes[6] & 0x0f) | 0x60;
521
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
522
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
523
+ return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`;
524
+ }
434
525
  static uuid() {
435
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
436
- const r = (Math.random() * 16) | 0;
437
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
438
- return v.toString(16);
439
- });
526
+ return this.uuidV4();
440
527
  }
441
528
  static timestamp() {
442
529
  return Date.now();
@@ -460,6 +547,13 @@ function KeyPath(options) {
460
547
  }
461
548
  function CompositeKeyPath(fields, options) {
462
549
  return (target) => {
550
+ if (options === null || options === void 0 ? void 0 : options.generator) {
551
+ throw new Error(`Key generators are not supported for composite keys on class ${target.name}. ` +
552
+ 'Provide all key fields explicitly before calling create().');
553
+ }
554
+ if (options === null || options === void 0 ? void 0 : options.autoIncrement) {
555
+ throw new Error(`autoIncrement is not supported for composite keys on class ${target.name}.`);
556
+ }
463
557
  const metadata = {
464
558
  fields: fields,
465
559
  options: options,
@@ -489,6 +583,17 @@ function Validate(predicate, message) {
489
583
  Reflect.defineMetadata('validators', nextRules, constructor);
490
584
  };
491
585
  }
586
+ function Calculated(compute) {
587
+ return (target, propertyKey) => {
588
+ const constructor = target.constructor;
589
+ const existing = Reflect.getMetadata('calculated', constructor) || [];
590
+ const nextFields = [
591
+ ...existing,
592
+ { field: propertyKey, compute },
593
+ ];
594
+ Reflect.defineMetadata('calculated', nextFields, constructor);
595
+ };
596
+ }
492
597
  function RetentionPolicy(options) {
493
598
  return (target) => {
494
599
  var _a, _b;
@@ -518,18 +623,152 @@ function DataClass(options = {}) {
518
623
  Reflect.defineMetadata('version', version, target);
519
624
  };
520
625
  }
626
+ class EntitySchema {
627
+ constructor(className, storeName, version, keyPath, autoIncrement, indexes) {
628
+ this.className = className;
629
+ this.storeName = storeName;
630
+ this.version = version;
631
+ this.keyPath = keyPath;
632
+ this.autoIncrement = autoIncrement;
633
+ this.indexes = indexes;
634
+ }
635
+ static fromClass(cls) {
636
+ var _a;
637
+ const keyPathMetadata = Reflect.getMetadata('keypath', cls);
638
+ const rawIndexes = (Reflect.getMetadata('indexes', cls) || []);
639
+ const indexes = rawIndexes.map((entry) => {
640
+ var _a;
641
+ return typeof entry === 'string'
642
+ ? { name: entry, options: { unique: false } }
643
+ : { name: entry.field, options: (_a = entry.options) !== null && _a !== void 0 ? _a : { unique: false } };
644
+ });
645
+ return new EntitySchema(cls.name, cls.name.toLowerCase(), Reflect.getMetadata('version', cls) || 1, keyPathMetadata === null || keyPathMetadata === void 0 ? void 0 : keyPathMetadata.fields, ((_a = keyPathMetadata === null || keyPathMetadata === void 0 ? void 0 : keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.autoIncrement) === true, indexes);
646
+ }
647
+ hasIndex(name) {
648
+ return this.indexes.some((index) => index.name === name);
649
+ }
650
+ }
651
+ class SchemaMigrator {
652
+ constructor(schemas, logDebug, logWarn) {
653
+ this.schemas = schemas;
654
+ this.logDebug = logDebug;
655
+ this.logWarn = logWarn;
656
+ }
657
+ get targetVersion() {
658
+ return Math.max(1, ...this.schemas.map((schema) => schema.version));
659
+ }
660
+ applyTo(db, transaction) {
661
+ this.schemas.forEach((schema) => {
662
+ if (!db.objectStoreNames.contains(schema.storeName)) {
663
+ this.createStore(db, schema);
664
+ }
665
+ else if (transaction) {
666
+ this.syncStore(transaction.objectStore(schema.storeName), schema);
667
+ }
668
+ });
669
+ this.reportOrphanedStores(db);
670
+ }
671
+ detectFixableDrift(db) {
672
+ const drift = [];
673
+ const existing = this.schemas.filter((schema) => db.objectStoreNames.contains(schema.storeName));
674
+ this.schemas.forEach((schema) => {
675
+ if (!db.objectStoreNames.contains(schema.storeName)) {
676
+ drift.push(`missing object store '${schema.storeName}'`);
677
+ }
678
+ });
679
+ if (existing.length) {
680
+ const transaction = db.transaction(existing.map((schema) => schema.storeName), 'readonly');
681
+ existing.forEach((schema) => {
682
+ const store = transaction.objectStore(schema.storeName);
683
+ schema.indexes.forEach(({ name }) => {
684
+ if (!store.indexNames.contains(name)) {
685
+ drift.push(`missing index '${name}' on '${schema.storeName}'`);
686
+ }
687
+ });
688
+ Array.from(store.indexNames).forEach((name) => {
689
+ if (!schema.hasIndex(name)) {
690
+ drift.push(`stale index '${name}' on '${schema.storeName}'`);
691
+ }
692
+ });
693
+ });
694
+ }
695
+ return drift;
696
+ }
697
+ createStore(db, schema) {
698
+ const options = {};
699
+ if (schema.keyPath !== undefined) {
700
+ options.keyPath = schema.keyPath;
701
+ }
702
+ if (schema.autoIncrement) {
703
+ options.autoIncrement = true;
704
+ }
705
+ const store = db.createObjectStore(schema.storeName, options);
706
+ schema.indexes.forEach(({ name, options: indexOptions }) => {
707
+ store.createIndex(name, name, indexOptions);
708
+ });
709
+ this.logDebug(`Created object store '${schema.storeName}' (entity version ${schema.version})`);
710
+ }
711
+ syncStore(store, schema) {
712
+ if (!this.keyPathMatches(store, schema)) {
713
+ this.logWarn(`Key path of object store '${schema.storeName}' differs from the ` +
714
+ `declaration on ${schema.className}. IndexedDB cannot change a ` +
715
+ 'store\'s key path in place; migrate the data to a new entity or ' +
716
+ 'delete the database to apply this change.');
717
+ }
718
+ schema.indexes.forEach(({ name, options }) => {
719
+ if (!store.indexNames.contains(name)) {
720
+ store.createIndex(name, name, options);
721
+ this.logDebug(`Added index '${name}' to '${schema.storeName}'`);
722
+ }
723
+ });
724
+ Array.from(store.indexNames).forEach((name) => {
725
+ if (!schema.hasIndex(name)) {
726
+ store.deleteIndex(name);
727
+ this.logDebug(`Removed stale index '${name}' from '${schema.storeName}'`);
728
+ }
729
+ });
730
+ }
731
+ keyPathMatches(store, schema) {
732
+ const normalise = (keyPath) => (Array.isArray(keyPath) ? keyPath.join(',') : (keyPath !== null && keyPath !== void 0 ? keyPath : ''));
733
+ return (normalise(store.keyPath) === normalise(schema.keyPath) &&
734
+ store.autoIncrement === schema.autoIncrement);
735
+ }
736
+ reportOrphanedStores(db) {
737
+ const declared = new Set(this.schemas.map((schema) => schema.storeName));
738
+ Array.from(db.objectStoreNames).forEach((storeName) => {
739
+ if (!declared.has(storeName)) {
740
+ this.logWarn(`Object store '${storeName}' has no registered entity class. ` +
741
+ 'Its data is preserved; register the entity again or delete the ' +
742
+ 'store manually if it is no longer needed.');
743
+ }
744
+ });
745
+ }
746
+ }
521
747
  class Database {
522
- constructor(dbName, classes) {
748
+ constructor(dbName, classes, printEnabled = false) {
523
749
  this.db = null;
524
750
  this.entityRepositories = new Map();
525
751
  this.retentionTimer = null;
526
752
  this.retentionCleanupRunning = false;
753
+ this.printDebug = (...data) => {
754
+ if (!this.printEnabled)
755
+ return;
756
+ console.debug('[idb-ts]:DEBUG:', ...data);
757
+ };
758
+ this.printError = (...error) => {
759
+ if (!this.printEnabled)
760
+ return;
761
+ console.error('[idb-ts]:ERROR:', ...error);
762
+ };
527
763
  this.dbName = dbName;
764
+ this.printEnabled = printEnabled;
528
765
  if (!classes.every((cls) => Reflect.getMetadata('dataclass', cls))) {
529
766
  throw new Error('All classes should be decorated with @DataClass.');
530
767
  }
531
768
  this.classes = classes;
532
- this.dbVersion = this.calculateDatabaseVersion();
769
+ this.schemas = classes.map((cls) => EntitySchema.fromClass(cls));
770
+ this.migrator = new SchemaMigrator(this.schemas, this.printDebug, this.printError);
771
+ this.dbVersion = this.migrator.targetVersion;
533
772
  this.retentionPolicies = this.classes
534
773
  .map((cls) => {
535
774
  const policy = Reflect.getMetadata('retention_policy', cls);
@@ -544,9 +783,52 @@ class Database {
544
783
  })
545
784
  .filter((policy) => policy !== null);
546
785
  }
547
- calculateDatabaseVersion() {
548
- const versions = this.classes.map((cls) => Reflect.getMetadata('version', cls) || 1);
549
- return Math.max(...versions);
786
+ readPersistedVersion() {
787
+ return tslib.__awaiter(this, void 0, void 0, function* () {
788
+ var _a;
789
+ const databasesFn = indexedDB.databases;
790
+ if (typeof databasesFn === 'function') {
791
+ try {
792
+ const infos = yield databasesFn.call(indexedDB);
793
+ const info = infos.find((entry) => entry.name === this.dbName);
794
+ return (_a = info === null || info === void 0 ? void 0 : info.version) !== null && _a !== void 0 ? _a : 0;
795
+ }
796
+ catch (_b) {
797
+ }
798
+ }
799
+ return new Promise((resolve) => {
800
+ const request = indexedDB.open(this.dbName);
801
+ request.onupgradeneeded = () => {
802
+ var _a;
803
+ (_a = request.transaction) === null || _a === void 0 ? void 0 : _a.abort();
804
+ resolve(0);
805
+ };
806
+ request.onsuccess = () => {
807
+ const version = request.result.version;
808
+ request.result.close();
809
+ resolve(version);
810
+ };
811
+ request.onerror = () => resolve(0);
812
+ });
813
+ });
814
+ }
815
+ openConnection(version) {
816
+ return new Promise((resolve, reject) => {
817
+ const request = indexedDB.open(this.dbName, version);
818
+ request.onupgradeneeded = (event) => {
819
+ var _a;
820
+ this.printDebug(`Database upgrade from version ${event.oldVersion} to ${(_a = event.newVersion) !== null && _a !== void 0 ? _a : version}`);
821
+ this.migrator.applyTo(request.result, request.transaction);
822
+ };
823
+ request.onblocked = () => {
824
+ this.printDebug(`Opening '${this.dbName}' at version ${version} is blocked by another open connection.`);
825
+ };
826
+ request.onsuccess = () => resolve(request.result);
827
+ request.onerror = () => {
828
+ this.printError('Error initializing database:', request.error);
829
+ reject(request.error);
830
+ };
831
+ });
550
832
  }
551
833
  static build(dbName, classes) {
552
834
  return tslib.__awaiter(this, void 0, void 0, function* () {
@@ -558,77 +840,25 @@ class Database {
558
840
  }
559
841
  initDB() {
560
842
  return tslib.__awaiter(this, void 0, void 0, function* () {
561
- return new Promise((resolve, reject) => {
562
- const request = indexedDB.open(this.dbName, this.dbVersion);
563
- request.onupgradeneeded = (event) => {
564
- const db = request.result;
565
- const oldVersion = event.oldVersion;
566
- const newVersion = event.newVersion || this.dbVersion;
567
- console.debug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
568
- this.classes.forEach((cls) => {
569
- var _a;
570
- const keyPathMetadata = Reflect.getMetadata('keypath', cls);
571
- const indexFields = Reflect.getMetadata('indexes', cls) || [];
572
- const classVersion = Reflect.getMetadata('version', cls) || 1;
573
- const storeName = cls.name.toLowerCase();
574
- if (classVersion > oldVersion) {
575
- if (!db.objectStoreNames.contains(storeName)) {
576
- console.debug(`Creating object store: ${storeName} (version ${classVersion})`);
577
- const storeOptions = {};
578
- if (keyPathMetadata) {
579
- storeOptions.keyPath = keyPathMetadata.fields;
580
- if ((_a = keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.autoIncrement) {
581
- storeOptions.autoIncrement = true;
582
- }
583
- }
584
- const store = db.createObjectStore(storeName, storeOptions);
585
- indexFields.forEach((indexField) => {
586
- var _a;
587
- const indexName = typeof indexField === 'string'
588
- ? indexField
589
- : indexField.field;
590
- const indexOptions = typeof indexField === 'string'
591
- ? { unique: false }
592
- : ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
593
- if (!store.indexNames.contains(indexName)) {
594
- store.createIndex(indexName, indexName, indexOptions);
595
- }
596
- });
597
- }
598
- else {
599
- console.debug(`Updating object store: ${storeName} (version ${classVersion})`);
600
- const transaction = request.transaction;
601
- if (transaction) {
602
- const store = transaction.objectStore(storeName);
603
- indexFields.forEach((indexField) => {
604
- var _a;
605
- const indexName = typeof indexField === 'string'
606
- ? indexField
607
- : indexField.field;
608
- const indexOptions = typeof indexField === 'string'
609
- ? { unique: false }
610
- : ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
611
- if (!store.indexNames.contains(indexName)) {
612
- console.debug(`Adding index: ${indexName} to ${storeName}`);
613
- store.createIndex(indexName, indexName, indexOptions);
614
- }
615
- });
616
- }
617
- }
618
- }
619
- });
620
- };
621
- request.onsuccess = () => {
622
- this.db = request.result;
623
- console.debug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
624
- this.startRetentionCleanup();
625
- resolve();
626
- };
627
- request.onerror = () => {
628
- console.error('Error initializing database:', request.error);
629
- reject(request.error);
630
- };
631
- });
843
+ const persistedVersion = yield this.readPersistedVersion();
844
+ if (persistedVersion > this.dbVersion) {
845
+ this.printDebug(`Declared schema version ${this.dbVersion} is lower than the ` +
846
+ `persisted database version ${persistedVersion}. IndexedDB cannot ` +
847
+ `downgrade; opening at version ${persistedVersion}.`);
848
+ }
849
+ let db = yield this.openConnection(Math.max(this.dbVersion, persistedVersion));
850
+ const drift = this.migrator.detectFixableDrift(db);
851
+ if (drift.length > 0) {
852
+ this.printDebug(`Schema drift detected without a version bump: ${drift.join('; ')}. ` +
853
+ `Reopening at version ${db.version + 1} to reconcile.`);
854
+ const nextVersion = db.version + 1;
855
+ db.close();
856
+ db = yield this.openConnection(nextVersion);
857
+ }
858
+ this.db = db;
859
+ this.dbVersion = db.version;
860
+ this.printDebug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.schemas.map((schema) => `${schema.className}(v${schema.version})`).join(', ')}`);
861
+ this.startRetentionCleanup();
632
862
  });
633
863
  }
634
864
  generateEntityRepositories() {
@@ -666,7 +896,7 @@ class Database {
666
896
  if (!cleanupIntervalMs || !this.db || this.retentionTimer) {
667
897
  return;
668
898
  }
669
- console.debug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
899
+ this.printDebug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
670
900
  void this.runRetentionCleanup();
671
901
  this.retentionTimer = setInterval(() => {
672
902
  void this.runRetentionCleanup();
@@ -681,11 +911,11 @@ class Database {
681
911
  }
682
912
  this.retentionCleanupRunning = true;
683
913
  try {
684
- console.debug('Retention cleanup tick started');
914
+ this.printDebug('Retention cleanup tick started');
685
915
  for (const { storeName, className, policy } of this.retentionPolicies) {
686
916
  yield this.cleanupExpiredRecords(storeName, className, policy);
687
917
  }
688
- console.debug('Retention cleanup tick finished');
918
+ this.printDebug('Retention cleanup tick finished');
689
919
  }
690
920
  finally {
691
921
  this.retentionCleanupRunning = false;
@@ -709,11 +939,11 @@ class Database {
709
939
  }
710
940
  const value = cursor.value;
711
941
  const timestamp = value === null || value === void 0 ? void 0 : value[policy.field];
712
- console.debug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
942
+ this.printDebug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
713
943
  if (typeof timestamp === 'number' && timestamp <= cutoff) {
714
944
  const deleteRequest = cursor.delete();
715
945
  deleteRequest.onsuccess = () => {
716
- console.debug(`Retention cleanup removed expired record from ${className}`);
946
+ this.printDebug(`Retention cleanup removed expired record from ${className}`);
717
947
  cursor.continue();
718
948
  };
719
949
  deleteRequest.onerror = () => {
@@ -740,11 +970,17 @@ class Database {
740
970
  });
741
971
  }
742
972
  createEntityRepository(cls, transaction) {
743
- const self = this;
744
973
  const creationTimestampField = INTERNAL_CREATED_AT_FIELD;
745
974
  const updateTimestampField = INTERNAL_UPDATED_AT_FIELD;
746
975
  const validators = (Reflect.getMetadata('validators', cls) ||
747
976
  []);
977
+ const calculatedFields = (Reflect.getMetadata('calculated', cls) ||
978
+ []);
979
+ const applyCalculatedFields = (item) => {
980
+ calculatedFields.forEach(({ field, compute }) => {
981
+ item[field] = compute(item);
982
+ });
983
+ };
748
984
  const validateItem = (item) => {
749
985
  const failures = [];
750
986
  validators.forEach((rule) => {
@@ -775,7 +1011,10 @@ class Database {
775
1011
  }
776
1012
  switch (generator) {
777
1013
  case 'uuid':
778
- return KeyGenerators.uuid();
1014
+ case 'uuidv4':
1015
+ return KeyGenerators.uuidV4();
1016
+ case 'uuidv6':
1017
+ return KeyGenerators.uuidV6();
779
1018
  case 'timestamp':
780
1019
  return KeyGenerators.timestamp();
781
1020
  case 'random':
@@ -849,11 +1088,11 @@ class Database {
849
1088
  }
850
1089
  };
851
1090
  return {
852
- query() {
853
- if (!self.db)
1091
+ query: () => {
1092
+ if (!this.db)
854
1093
  throw new Error('Database not initialized.');
855
1094
  const storeName = cls.name.toLowerCase();
856
- return new QueryBuilder(self.db, storeName, transaction);
1095
+ return new QueryBuilder(this.db, storeName, transaction);
857
1096
  },
858
1097
  create: (item) => tslib.__awaiter(this, void 0, void 0, function* () {
859
1098
  var _a;
@@ -870,11 +1109,12 @@ class Database {
870
1109
  }
871
1110
  }
872
1111
  }
1112
+ applyCalculatedFields(item);
873
1113
  validateItem(item);
874
1114
  applyTimestampFields(item);
875
1115
  return this.performOperation(cls.name, 'readwrite', (store) => {
876
1116
  return createStoredItem(store, item).then(() => {
877
- console.debug(`Item added to ${cls.name}:`, item);
1117
+ this.printDebug(`Item added to ${cls.name}:`, item);
878
1118
  });
879
1119
  }, transaction);
880
1120
  }),
@@ -889,7 +1129,7 @@ class Database {
889
1129
  const request = store.get(key);
890
1130
  return new Promise((resolve, reject) => {
891
1131
  request.onsuccess = () => {
892
- console.debug(`Item read from ${cls.name}:`, request.result);
1132
+ this.printDebug(`Item read from ${cls.name}:`, request.result);
893
1133
  resolve(request.result);
894
1134
  };
895
1135
  request.onerror = () => reject(request.error);
@@ -897,10 +1137,11 @@ class Database {
897
1137
  }, transaction);
898
1138
  }),
899
1139
  update: (item) => tslib.__awaiter(this, void 0, void 0, function* () {
1140
+ applyCalculatedFields(item);
900
1141
  validateItem(item);
901
1142
  return this.performOperation(cls.name, 'readwrite', (store) => {
902
1143
  return updateStoredItem(store, item).then(() => {
903
- console.debug(`Item updated in ${cls.name}:`, item);
1144
+ this.printDebug(`Item updated in ${cls.name}:`, item);
904
1145
  });
905
1146
  }, transaction);
906
1147
  }),
@@ -913,7 +1154,7 @@ class Database {
913
1154
  delete: (key) => tslib.__awaiter(this, void 0, void 0, function* () {
914
1155
  return this.performOperation(cls.name, 'readwrite', (store) => {
915
1156
  return deleteStoredItem(store, key).then(() => {
916
- console.debug(`Item deleted from ${cls.name}:`, key);
1157
+ this.printDebug(`Item deleted from ${cls.name}:`, key);
917
1158
  });
918
1159
  }, transaction);
919
1160
  }),
@@ -939,7 +1180,7 @@ class Database {
939
1180
  const request = store.getAll();
940
1181
  return new Promise((resolve, reject) => {
941
1182
  request.onsuccess = () => {
942
- console.debug(`All items from ${cls.name}:`, request.result);
1183
+ this.printDebug(`All items from ${cls.name}:`, request.result);
943
1184
  resolve(request.result);
944
1185
  };
945
1186
  request.onerror = () => reject(request.error);
@@ -953,7 +1194,7 @@ class Database {
953
1194
  request.onsuccess = () => {
954
1195
  const items = request.result;
955
1196
  const paginatedItems = items.slice((page - 1) * pageSize, page * pageSize);
956
- console.debug(`Paginated items from ${cls.name}:`, paginatedItems);
1197
+ this.printDebug(`Paginated items from ${cls.name}:`, paginatedItems);
957
1198
  resolve(paginatedItems);
958
1199
  };
959
1200
  request.onerror = () => reject(request.error);
@@ -969,7 +1210,7 @@ class Database {
969
1210
  const request = index.getAll(value);
970
1211
  return new Promise((resolve, reject) => {
971
1212
  request.onsuccess = () => {
972
- console.debug(`Items found by index ${indexName} with value ${value}:`, request.result);
1213
+ this.printDebug(`Items found by index ${indexName} with value ${value}:`, request.result);
973
1214
  resolve(request.result);
974
1215
  };
975
1216
  request.onerror = () => reject(request.error);
@@ -985,7 +1226,7 @@ class Database {
985
1226
  const request = index.get(value);
986
1227
  return new Promise((resolve, reject) => {
987
1228
  request.onsuccess = () => {
988
- console.debug(`Item found by index ${indexName} with value ${value}:`, request.result);
1229
+ this.printDebug(`Item found by index ${indexName} with value ${value}:`, request.result);
989
1230
  resolve(request.result);
990
1231
  };
991
1232
  request.onerror = () => reject(request.error);
@@ -997,7 +1238,19 @@ class Database {
997
1238
  const request = store.count();
998
1239
  return new Promise((resolve, reject) => {
999
1240
  request.onsuccess = () => {
1000
- console.debug(`Count for ${cls.name}:`, request.result);
1241
+ this.printDebug(`Count for ${cls.name}:`, request.result);
1242
+ resolve(request.result);
1243
+ };
1244
+ request.onerror = () => reject(request.error);
1245
+ });
1246
+ }, transaction);
1247
+ }),
1248
+ getKeys: () => tslib.__awaiter(this, void 0, void 0, function* () {
1249
+ return this.performOperation(cls.name, 'readonly', (store) => {
1250
+ const request = store.getAllKeys();
1251
+ return new Promise((resolve, reject) => {
1252
+ request.onsuccess = () => {
1253
+ this.printDebug(`Keys for ${cls.name}:`, request.result);
1001
1254
  resolve(request.result);
1002
1255
  };
1003
1256
  request.onerror = () => reject(request.error);
@@ -1010,7 +1263,7 @@ class Database {
1010
1263
  return new Promise((resolve, reject) => {
1011
1264
  request.onsuccess = () => {
1012
1265
  const exists = request.result > 0;
1013
- console.debug(`Exists check for ${cls.name} with key ${key}:`, exists);
1266
+ this.printDebug(`Exists check for ${cls.name} with key ${key}:`, exists);
1014
1267
  resolve(exists);
1015
1268
  };
1016
1269
  request.onerror = () => reject(request.error);
@@ -1022,7 +1275,7 @@ class Database {
1022
1275
  const request = store.clear();
1023
1276
  return new Promise((resolve, reject) => {
1024
1277
  request.onsuccess = () => {
1025
- console.debug(`Cleared all items from ${cls.name}`);
1278
+ this.printDebug(`Cleared all items from ${cls.name}`);
1026
1279
  resolve();
1027
1280
  };
1028
1281
  request.onerror = () => reject(request.error);
@@ -1118,6 +1371,107 @@ class Database {
1118
1371
  }
1119
1372
  });
1120
1373
  }
1374
+ exportDatabase() {
1375
+ return tslib.__awaiter(this, void 0, void 0, function* () {
1376
+ const dump = {};
1377
+ for (const cls of this.classes) {
1378
+ const repository = this.entityRepositories.get(cls.name);
1379
+ if (repository) {
1380
+ dump[cls.name] = yield repository.list();
1381
+ }
1382
+ }
1383
+ return dump;
1384
+ });
1385
+ }
1386
+ importDatabase(dump_1) {
1387
+ return tslib.__awaiter(this, arguments, void 0, function* (dump, options = {}) {
1388
+ for (const [entityName, records] of Object.entries(dump)) {
1389
+ const entityClass = this.classes.find((cls) => cls.name === entityName);
1390
+ if (!entityClass) {
1391
+ this.printDebug(`Skipping import for unregistered entity '${entityName}'.`);
1392
+ continue;
1393
+ }
1394
+ if (!Array.isArray(records)) {
1395
+ this.printDebug(`Skipping import for '${entityName}': expected an array of records.`);
1396
+ continue;
1397
+ }
1398
+ yield this.performOperation(entityName, 'readwrite', (store) => {
1399
+ return new Promise((resolve, reject) => {
1400
+ const writeRecords = () => {
1401
+ let pending = records.length;
1402
+ if (!pending) {
1403
+ resolve();
1404
+ return;
1405
+ }
1406
+ records.forEach((record) => {
1407
+ const request = store.put(record);
1408
+ request.onsuccess = () => {
1409
+ pending -= 1;
1410
+ if (!pending)
1411
+ resolve();
1412
+ };
1413
+ request.onerror = () => reject(request.error);
1414
+ });
1415
+ };
1416
+ if (options.clear) {
1417
+ const clearRequest = store.clear();
1418
+ clearRequest.onsuccess = () => writeRecords();
1419
+ clearRequest.onerror = () => reject(clearRequest.error);
1420
+ return;
1421
+ }
1422
+ writeRecords();
1423
+ });
1424
+ });
1425
+ this.printDebug(`Imported ${records.length} record(s) into '${entityName}'.`);
1426
+ }
1427
+ });
1428
+ }
1429
+ pushTo(adapter) {
1430
+ return tslib.__awaiter(this, void 0, void 0, function* () {
1431
+ for (const cls of this.classes) {
1432
+ const repository = this.entityRepositories.get(cls.name);
1433
+ if (!repository) {
1434
+ continue;
1435
+ }
1436
+ const records = yield repository.list();
1437
+ yield adapter.push(cls.name, records);
1438
+ this.printDebug(`Pushed ${records.length} record(s) of '${cls.name}' to adapter.`);
1439
+ }
1440
+ });
1441
+ }
1442
+ pullFrom(adapter) {
1443
+ return tslib.__awaiter(this, void 0, void 0, function* () {
1444
+ for (const cls of this.classes) {
1445
+ const records = yield adapter.pull(cls.name);
1446
+ if (records === undefined) {
1447
+ continue;
1448
+ }
1449
+ if (!Array.isArray(records)) {
1450
+ this.printDebug(`Skipping pull for '${cls.name}': adapter did not return an array.`);
1451
+ continue;
1452
+ }
1453
+ yield this.performOperation(cls.name, 'readwrite', (store) => {
1454
+ return new Promise((resolve, reject) => {
1455
+ let pending = records.length;
1456
+ if (!pending) {
1457
+ resolve();
1458
+ return;
1459
+ }
1460
+ records.forEach((record) => {
1461
+ const request = store.put(record);
1462
+ request.onsuccess = () => {
1463
+ pending -= 1;
1464
+ if (!pending)
1465
+ resolve();
1466
+ };
1467
+ request.onerror = () => reject(request.error);
1468
+ });
1469
+ });
1470
+ });
1471
+ this.printDebug(`Pulled ${records.length} record(s) of '${cls.name}' from adapter.`);
1472
+ }
1473
+ });
1474
+ }
1121
1475
  close() {
1122
1476
  if (this.retentionTimer) {
1123
1477
  clearInterval(this.retentionTimer);
@@ -1148,6 +1502,7 @@ class Database {
1148
1502
  }
1149
1503
  }
1150
1504
 
1505
+ exports.Calculated = Calculated;
1151
1506
  exports.CompositeKeyPath = CompositeKeyPath;
1152
1507
  exports.DataClass = DataClass;
1153
1508
  exports.Database = Database;