idb-ts 3.15.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 +269 -82
- package/lib/index.cjs +429 -85
- package/lib/index.d.ts +28 -8
- package/lib/index.esm.js +429 -86
- package/lib/index.js +429 -86
- package/package.json +13 -13
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,6 +623,127 @@ 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
748
|
constructor(dbName, classes, printEnabled = false) {
|
|
523
749
|
this.db = null;
|
|
@@ -540,7 +766,9 @@ class Database {
|
|
|
540
766
|
throw new Error('All classes should be decorated with @DataClass.');
|
|
541
767
|
}
|
|
542
768
|
this.classes = classes;
|
|
543
|
-
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;
|
|
544
772
|
this.retentionPolicies = this.classes
|
|
545
773
|
.map((cls) => {
|
|
546
774
|
const policy = Reflect.getMetadata('retention_policy', cls);
|
|
@@ -555,9 +783,52 @@ class Database {
|
|
|
555
783
|
})
|
|
556
784
|
.filter((policy) => policy !== null);
|
|
557
785
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
+
});
|
|
561
832
|
}
|
|
562
833
|
static build(dbName, classes) {
|
|
563
834
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -569,77 +840,25 @@ class Database {
|
|
|
569
840
|
}
|
|
570
841
|
initDB() {
|
|
571
842
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if ((_a = keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.autoIncrement) {
|
|
592
|
-
storeOptions.autoIncrement = true;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
const store = db.createObjectStore(storeName, storeOptions);
|
|
596
|
-
indexFields.forEach((indexField) => {
|
|
597
|
-
var _a;
|
|
598
|
-
const indexName = typeof indexField === 'string'
|
|
599
|
-
? indexField
|
|
600
|
-
: indexField.field;
|
|
601
|
-
const indexOptions = typeof indexField === 'string'
|
|
602
|
-
? { unique: false }
|
|
603
|
-
: ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
|
|
604
|
-
if (!store.indexNames.contains(indexName)) {
|
|
605
|
-
store.createIndex(indexName, indexName, indexOptions);
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
this.printDebug(`Updating object store: ${storeName} (version ${classVersion})`);
|
|
611
|
-
const transaction = request.transaction;
|
|
612
|
-
if (transaction) {
|
|
613
|
-
const store = transaction.objectStore(storeName);
|
|
614
|
-
indexFields.forEach((indexField) => {
|
|
615
|
-
var _a;
|
|
616
|
-
const indexName = typeof indexField === 'string'
|
|
617
|
-
? indexField
|
|
618
|
-
: indexField.field;
|
|
619
|
-
const indexOptions = typeof indexField === 'string'
|
|
620
|
-
? { unique: false }
|
|
621
|
-
: ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
|
|
622
|
-
if (!store.indexNames.contains(indexName)) {
|
|
623
|
-
this.printDebug(`Adding index: ${indexName} to ${storeName}`);
|
|
624
|
-
store.createIndex(indexName, indexName, indexOptions);
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
};
|
|
632
|
-
request.onsuccess = () => {
|
|
633
|
-
this.db = request.result;
|
|
634
|
-
this.printDebug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
|
|
635
|
-
this.startRetentionCleanup();
|
|
636
|
-
resolve();
|
|
637
|
-
};
|
|
638
|
-
request.onerror = () => {
|
|
639
|
-
this.printError('Error initializing database:', request.error);
|
|
640
|
-
reject(request.error);
|
|
641
|
-
};
|
|
642
|
-
});
|
|
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();
|
|
643
862
|
});
|
|
644
863
|
}
|
|
645
864
|
generateEntityRepositories() {
|
|
@@ -751,11 +970,17 @@ class Database {
|
|
|
751
970
|
});
|
|
752
971
|
}
|
|
753
972
|
createEntityRepository(cls, transaction) {
|
|
754
|
-
const self = this;
|
|
755
973
|
const creationTimestampField = INTERNAL_CREATED_AT_FIELD;
|
|
756
974
|
const updateTimestampField = INTERNAL_UPDATED_AT_FIELD;
|
|
757
975
|
const validators = (Reflect.getMetadata('validators', cls) ||
|
|
758
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
|
+
};
|
|
759
984
|
const validateItem = (item) => {
|
|
760
985
|
const failures = [];
|
|
761
986
|
validators.forEach((rule) => {
|
|
@@ -786,7 +1011,10 @@ class Database {
|
|
|
786
1011
|
}
|
|
787
1012
|
switch (generator) {
|
|
788
1013
|
case 'uuid':
|
|
789
|
-
|
|
1014
|
+
case 'uuidv4':
|
|
1015
|
+
return KeyGenerators.uuidV4();
|
|
1016
|
+
case 'uuidv6':
|
|
1017
|
+
return KeyGenerators.uuidV6();
|
|
790
1018
|
case 'timestamp':
|
|
791
1019
|
return KeyGenerators.timestamp();
|
|
792
1020
|
case 'random':
|
|
@@ -860,11 +1088,11 @@ class Database {
|
|
|
860
1088
|
}
|
|
861
1089
|
};
|
|
862
1090
|
return {
|
|
863
|
-
query() {
|
|
864
|
-
if (!
|
|
1091
|
+
query: () => {
|
|
1092
|
+
if (!this.db)
|
|
865
1093
|
throw new Error('Database not initialized.');
|
|
866
1094
|
const storeName = cls.name.toLowerCase();
|
|
867
|
-
return new QueryBuilder(
|
|
1095
|
+
return new QueryBuilder(this.db, storeName, transaction);
|
|
868
1096
|
},
|
|
869
1097
|
create: (item) => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
870
1098
|
var _a;
|
|
@@ -881,6 +1109,7 @@ class Database {
|
|
|
881
1109
|
}
|
|
882
1110
|
}
|
|
883
1111
|
}
|
|
1112
|
+
applyCalculatedFields(item);
|
|
884
1113
|
validateItem(item);
|
|
885
1114
|
applyTimestampFields(item);
|
|
886
1115
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
@@ -908,6 +1137,7 @@ class Database {
|
|
|
908
1137
|
}, transaction);
|
|
909
1138
|
}),
|
|
910
1139
|
update: (item) => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
1140
|
+
applyCalculatedFields(item);
|
|
911
1141
|
validateItem(item);
|
|
912
1142
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
913
1143
|
return updateStoredItem(store, item).then(() => {
|
|
@@ -1015,6 +1245,18 @@ class Database {
|
|
|
1015
1245
|
});
|
|
1016
1246
|
}, transaction);
|
|
1017
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);
|
|
1254
|
+
resolve(request.result);
|
|
1255
|
+
};
|
|
1256
|
+
request.onerror = () => reject(request.error);
|
|
1257
|
+
});
|
|
1258
|
+
}, transaction);
|
|
1259
|
+
}),
|
|
1018
1260
|
exists: (key) => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
1019
1261
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
1020
1262
|
const request = store.count(key);
|
|
@@ -1129,6 +1371,107 @@ class Database {
|
|
|
1129
1371
|
}
|
|
1130
1372
|
});
|
|
1131
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
|
+
}
|
|
1132
1475
|
close() {
|
|
1133
1476
|
if (this.retentionTimer) {
|
|
1134
1477
|
clearInterval(this.retentionTimer);
|
|
@@ -1159,6 +1502,7 @@ class Database {
|
|
|
1159
1502
|
}
|
|
1160
1503
|
}
|
|
1161
1504
|
|
|
1505
|
+
exports.Calculated = Calculated;
|
|
1162
1506
|
exports.CompositeKeyPath = CompositeKeyPath;
|
|
1163
1507
|
exports.DataClass = DataClass;
|
|
1164
1508
|
exports.Database = Database;
|