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