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.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,6 +621,127 @@ 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
746
|
constructor(dbName, classes, printEnabled = false) {
|
|
521
747
|
this.db = null;
|
|
@@ -538,7 +764,9 @@ class Database {
|
|
|
538
764
|
throw new Error('All classes should be decorated with @DataClass.');
|
|
539
765
|
}
|
|
540
766
|
this.classes = classes;
|
|
541
|
-
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;
|
|
542
770
|
this.retentionPolicies = this.classes
|
|
543
771
|
.map((cls) => {
|
|
544
772
|
const policy = Reflect.getMetadata('retention_policy', cls);
|
|
@@ -553,9 +781,52 @@ class Database {
|
|
|
553
781
|
})
|
|
554
782
|
.filter((policy) => policy !== null);
|
|
555
783
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
});
|
|
559
830
|
}
|
|
560
831
|
static build(dbName, classes) {
|
|
561
832
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -567,77 +838,25 @@ class Database {
|
|
|
567
838
|
}
|
|
568
839
|
initDB() {
|
|
569
840
|
return __awaiter(this, void 0, void 0, function* () {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if ((_a = keyPathMetadata.options) === null || _a === void 0 ? void 0 : _a.autoIncrement) {
|
|
590
|
-
storeOptions.autoIncrement = true;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
const store = db.createObjectStore(storeName, storeOptions);
|
|
594
|
-
indexFields.forEach((indexField) => {
|
|
595
|
-
var _a;
|
|
596
|
-
const indexName = typeof indexField === 'string'
|
|
597
|
-
? indexField
|
|
598
|
-
: indexField.field;
|
|
599
|
-
const indexOptions = typeof indexField === 'string'
|
|
600
|
-
? { unique: false }
|
|
601
|
-
: ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
|
|
602
|
-
if (!store.indexNames.contains(indexName)) {
|
|
603
|
-
store.createIndex(indexName, indexName, indexOptions);
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
this.printDebug(`Updating object store: ${storeName} (version ${classVersion})`);
|
|
609
|
-
const transaction = request.transaction;
|
|
610
|
-
if (transaction) {
|
|
611
|
-
const store = transaction.objectStore(storeName);
|
|
612
|
-
indexFields.forEach((indexField) => {
|
|
613
|
-
var _a;
|
|
614
|
-
const indexName = typeof indexField === 'string'
|
|
615
|
-
? indexField
|
|
616
|
-
: indexField.field;
|
|
617
|
-
const indexOptions = typeof indexField === 'string'
|
|
618
|
-
? { unique: false }
|
|
619
|
-
: ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
|
|
620
|
-
if (!store.indexNames.contains(indexName)) {
|
|
621
|
-
this.printDebug(`Adding index: ${indexName} to ${storeName}`);
|
|
622
|
-
store.createIndex(indexName, indexName, indexOptions);
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
};
|
|
630
|
-
request.onsuccess = () => {
|
|
631
|
-
this.db = request.result;
|
|
632
|
-
this.printDebug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
|
|
633
|
-
this.startRetentionCleanup();
|
|
634
|
-
resolve();
|
|
635
|
-
};
|
|
636
|
-
request.onerror = () => {
|
|
637
|
-
this.printError('Error initializing database:', request.error);
|
|
638
|
-
reject(request.error);
|
|
639
|
-
};
|
|
640
|
-
});
|
|
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();
|
|
641
860
|
});
|
|
642
861
|
}
|
|
643
862
|
generateEntityRepositories() {
|
|
@@ -749,11 +968,17 @@ class Database {
|
|
|
749
968
|
});
|
|
750
969
|
}
|
|
751
970
|
createEntityRepository(cls, transaction) {
|
|
752
|
-
const self = this;
|
|
753
971
|
const creationTimestampField = INTERNAL_CREATED_AT_FIELD;
|
|
754
972
|
const updateTimestampField = INTERNAL_UPDATED_AT_FIELD;
|
|
755
973
|
const validators = (Reflect.getMetadata('validators', cls) ||
|
|
756
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
|
+
};
|
|
757
982
|
const validateItem = (item) => {
|
|
758
983
|
const failures = [];
|
|
759
984
|
validators.forEach((rule) => {
|
|
@@ -784,7 +1009,10 @@ class Database {
|
|
|
784
1009
|
}
|
|
785
1010
|
switch (generator) {
|
|
786
1011
|
case 'uuid':
|
|
787
|
-
|
|
1012
|
+
case 'uuidv4':
|
|
1013
|
+
return KeyGenerators.uuidV4();
|
|
1014
|
+
case 'uuidv6':
|
|
1015
|
+
return KeyGenerators.uuidV6();
|
|
788
1016
|
case 'timestamp':
|
|
789
1017
|
return KeyGenerators.timestamp();
|
|
790
1018
|
case 'random':
|
|
@@ -858,11 +1086,11 @@ class Database {
|
|
|
858
1086
|
}
|
|
859
1087
|
};
|
|
860
1088
|
return {
|
|
861
|
-
query() {
|
|
862
|
-
if (!
|
|
1089
|
+
query: () => {
|
|
1090
|
+
if (!this.db)
|
|
863
1091
|
throw new Error('Database not initialized.');
|
|
864
1092
|
const storeName = cls.name.toLowerCase();
|
|
865
|
-
return new QueryBuilder(
|
|
1093
|
+
return new QueryBuilder(this.db, storeName, transaction);
|
|
866
1094
|
},
|
|
867
1095
|
create: (item) => __awaiter(this, void 0, void 0, function* () {
|
|
868
1096
|
var _a;
|
|
@@ -879,6 +1107,7 @@ class Database {
|
|
|
879
1107
|
}
|
|
880
1108
|
}
|
|
881
1109
|
}
|
|
1110
|
+
applyCalculatedFields(item);
|
|
882
1111
|
validateItem(item);
|
|
883
1112
|
applyTimestampFields(item);
|
|
884
1113
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
@@ -906,6 +1135,7 @@ class Database {
|
|
|
906
1135
|
}, transaction);
|
|
907
1136
|
}),
|
|
908
1137
|
update: (item) => __awaiter(this, void 0, void 0, function* () {
|
|
1138
|
+
applyCalculatedFields(item);
|
|
909
1139
|
validateItem(item);
|
|
910
1140
|
return this.performOperation(cls.name, 'readwrite', (store) => {
|
|
911
1141
|
return updateStoredItem(store, item).then(() => {
|
|
@@ -1013,6 +1243,18 @@ class Database {
|
|
|
1013
1243
|
});
|
|
1014
1244
|
}, transaction);
|
|
1015
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);
|
|
1252
|
+
resolve(request.result);
|
|
1253
|
+
};
|
|
1254
|
+
request.onerror = () => reject(request.error);
|
|
1255
|
+
});
|
|
1256
|
+
}, transaction);
|
|
1257
|
+
}),
|
|
1016
1258
|
exists: (key) => __awaiter(this, void 0, void 0, function* () {
|
|
1017
1259
|
return this.performOperation(cls.name, 'readonly', (store) => {
|
|
1018
1260
|
const request = store.count(key);
|
|
@@ -1127,6 +1369,107 @@ class Database {
|
|
|
1127
1369
|
}
|
|
1128
1370
|
});
|
|
1129
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
|
+
}
|
|
1130
1473
|
close() {
|
|
1131
1474
|
if (this.retentionTimer) {
|
|
1132
1475
|
clearInterval(this.retentionTimer);
|
|
@@ -1157,4 +1500,4 @@ class Database {
|
|
|
1157
1500
|
}
|
|
1158
1501
|
}
|
|
1159
1502
|
|
|
1160
|
-
export { CompositeKeyPath, DataClass, Database, Index, KeyGenerators, KeyPath, RetentionPolicy, Validate };
|
|
1503
|
+
export { Calculated, CompositeKeyPath, DataClass, Database, Index, KeyGenerators, KeyPath, RetentionPolicy, Validate };
|