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/README.md +395 -84
- package/lib/index.cjs +457 -102
- package/lib/index.d.ts +31 -8
- package/lib/index.esm.js +457 -103
- package/lib/index.js +457 -103
- package/package.json +15 -12
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
|
|
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.
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|