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.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
|
|
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.
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|