@pixagram/lacerta-db 0.9.0 → 0.9.2
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/dist/browser.min.js +23 -5
- package/dist/index.min.js +23 -5
- package/index.js +174 -97
- package/package.json +1 -1
- package/readme.md +763 -420
- package/.idea/lacerta-db.iml +0 -8
- package/.idea/modules.xml +0 -8
- package/.idea/php.xml +0 -19
- package/.idea/workspace.xml +0 -61
- package/index (Copy).js +0 -3718
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LacertaDB V0.9.
|
|
2
|
+
* LacertaDB V0.9.2 - Production Library with QuickStore (Optimized)
|
|
3
3
|
* * A high-performance, browser-based document database with support for:
|
|
4
4
|
* - IndexedDB storage with connection pooling
|
|
5
5
|
* - Multiple caching strategies (LRU, LFU, TTL)
|
|
@@ -9,17 +9,35 @@
|
|
|
9
9
|
* - MongoDB-like query syntax and aggregation pipeline
|
|
10
10
|
* - Schema migrations and versioning
|
|
11
11
|
* - QuickStore for fast localStorage-based operations
|
|
12
|
-
* * Changelog V0.9.
|
|
12
|
+
* * Changelog V0.9.2:
|
|
13
|
+
* - CRITICAL: Fixed non-extractable CryptoKey in _importMasterKeys (changePin was broken).
|
|
14
|
+
* - CRITICAL: Fixed TransactionInactiveError in batchOperation (sync request queueing).
|
|
15
|
+
* - CRITICAL: Fixed batchAdd crash when called without options (from import()).
|
|
16
|
+
* - SECURITY: Constant-time comparison in _arrayEquals (timing attack prevention).
|
|
17
|
+
* - SECURITY: Unbiased PIN generation via rejection sampling.
|
|
18
|
+
* - SECURITY: changePin now verifies oldPin before allowing change.
|
|
19
|
+
* - SECURITY: Standardized AES-GCM IV to 12 bytes in encryptPrivateKey/decryptPrivateKey.
|
|
20
|
+
* - FIX: Environment-safe polyfills (no bare `window` at module scope).
|
|
21
|
+
* - FIX: BTreeIndex.remove only decrements _size when key/value actually existed.
|
|
22
|
+
* - FIX: LFUCache.has() now checks TTL expiration.
|
|
23
|
+
* - FIX: GeoIndex._size cannot go negative on redundant removePoint calls.
|
|
24
|
+
* - FIX: $group aggregation uses JSON.stringify for object keys (Map comparison).
|
|
25
|
+
* - FIX: TextIndex tokenizer minimum length normalized across code paths.
|
|
26
|
+
* - FIX: QuickStore properly cleans up beforeunload listener via destroy().
|
|
27
|
+
* - FIX: Consistent window.requestIdleCallback usage in _saveIndexMetadata.
|
|
28
|
+
* - FIX: Block-scoped const in $avg aggregation case.
|
|
29
|
+
* * Changelog V0.9.1:
|
|
13
30
|
* - CRITICAL: Implemented Master Key Wrapping for encryption (fixes data loss on PIN change).
|
|
14
31
|
* - CRITICAL: Fixed UI freezing in QuickStore using in-memory caching and async persistence.
|
|
15
32
|
* - CRITICAL: Replaced O(N) GeoIndex with O(log N) QuadTree.
|
|
33
|
+
* - CRITICAL: Fixed TransactionInactiveError by implementing Batch Processing for Indexes.
|
|
16
34
|
* - SECURITY: Increased PBKDF2 iterations to 600,000 (OWASP standard).
|
|
17
35
|
* - OPTIMIZATION: Removed Global Mutex for read operations (concurrency fix).
|
|
18
36
|
* - OPTIMIZATION: Implemented Cursor-based indexing (OOM fix).
|
|
19
37
|
* - FIX: Added Magic Byte check for robust compression detection.
|
|
20
38
|
* - FIX: Use Intl.Segmenter for proper CJK text tokenization.
|
|
21
39
|
* * @module LacertaDB
|
|
22
|
-
* @version 0.9.
|
|
40
|
+
* @version 0.9.2
|
|
23
41
|
* @license MIT
|
|
24
42
|
* @author Pixagram SA
|
|
25
43
|
*/
|
|
@@ -30,7 +48,7 @@
|
|
|
30
48
|
// Polyfills
|
|
31
49
|
// ========================
|
|
32
50
|
|
|
33
|
-
if (!window.requestIdleCallback) {
|
|
51
|
+
if (typeof window !== 'undefined' && !window.requestIdleCallback) {
|
|
34
52
|
window.requestIdleCallback = function(callback) {
|
|
35
53
|
return setTimeout(callback, 0);
|
|
36
54
|
};
|
|
@@ -76,8 +94,25 @@ class QuickStore {
|
|
|
76
94
|
this._dirty = false;
|
|
77
95
|
|
|
78
96
|
// Safety: Flush on unload to prevent data loss
|
|
97
|
+
this._flushHandler = () => this._flushSync();
|
|
79
98
|
if (typeof window !== 'undefined') {
|
|
80
|
-
window.addEventListener('beforeunload',
|
|
99
|
+
window.addEventListener('beforeunload', this._flushHandler);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
destroy() {
|
|
104
|
+
this._flushSync();
|
|
105
|
+
if (typeof window !== 'undefined' && this._flushHandler) {
|
|
106
|
+
window.removeEventListener('beforeunload', this._flushHandler);
|
|
107
|
+
this._flushHandler = null;
|
|
108
|
+
}
|
|
109
|
+
if (this._saveIndexTimer) {
|
|
110
|
+
if (typeof window !== 'undefined' && window.cancelIdleCallback) {
|
|
111
|
+
window.cancelIdleCallback(this._saveIndexTimer);
|
|
112
|
+
} else {
|
|
113
|
+
clearTimeout(this._saveIndexTimer);
|
|
114
|
+
}
|
|
115
|
+
this._saveIndexTimer = null;
|
|
81
116
|
}
|
|
82
117
|
}
|
|
83
118
|
|
|
@@ -451,7 +486,7 @@ class LFUCache {
|
|
|
451
486
|
}
|
|
452
487
|
|
|
453
488
|
has(key) {
|
|
454
|
-
return this.
|
|
489
|
+
return this.get(key) !== null;
|
|
455
490
|
}
|
|
456
491
|
|
|
457
492
|
get size() {
|
|
@@ -676,7 +711,7 @@ class BrowserEncryptionUtility {
|
|
|
676
711
|
{
|
|
677
712
|
name: 'PBKDF2',
|
|
678
713
|
salt: salt,
|
|
679
|
-
iterations:
|
|
714
|
+
iterations: 600000,
|
|
680
715
|
hash: 'SHA-256'
|
|
681
716
|
},
|
|
682
717
|
keyMaterial,
|
|
@@ -718,7 +753,7 @@ class BrowserEncryptionUtility {
|
|
|
718
753
|
{
|
|
719
754
|
name: 'PBKDF2',
|
|
720
755
|
salt: salt,
|
|
721
|
-
iterations:
|
|
756
|
+
iterations: 600000,
|
|
722
757
|
hash: 'SHA-256'
|
|
723
758
|
},
|
|
724
759
|
keyMaterial,
|
|
@@ -833,10 +868,10 @@ class SecureDatabaseEncryption {
|
|
|
833
868
|
const hmacBytes = rawBytes.slice(32, 64);
|
|
834
869
|
|
|
835
870
|
this._masterKey = await crypto.subtle.importKey(
|
|
836
|
-
'raw', encBytes, { name: 'AES-GCM', length: 256 },
|
|
871
|
+
'raw', encBytes, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
|
|
837
872
|
);
|
|
838
873
|
this._hmacKey = await crypto.subtle.importKey(
|
|
839
|
-
'raw', hmacBytes, { name: 'HMAC', hash: 'SHA-256' },
|
|
874
|
+
'raw', hmacBytes, { name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify']
|
|
840
875
|
);
|
|
841
876
|
}
|
|
842
877
|
|
|
@@ -854,6 +889,17 @@ class SecureDatabaseEncryption {
|
|
|
854
889
|
throw new Error('Database encryption not initialized');
|
|
855
890
|
}
|
|
856
891
|
|
|
892
|
+
// Verify old PIN by attempting to unwrap current master key
|
|
893
|
+
const oldKek = await this._deriveKEK(new TextEncoder().encode(oldPin), this._salt);
|
|
894
|
+
const currentWrappedBytes = base64.decode(this._wrappedKeyBlob);
|
|
895
|
+
const currentIv = currentWrappedBytes.slice(0, 12);
|
|
896
|
+
const currentEncMK = currentWrappedBytes.slice(12);
|
|
897
|
+
try {
|
|
898
|
+
await crypto.subtle.decrypt({ name: 'AES-GCM', iv: currentIv }, oldKek, currentEncMK);
|
|
899
|
+
} catch (e) {
|
|
900
|
+
throw new Error('Invalid old PIN');
|
|
901
|
+
}
|
|
902
|
+
|
|
857
903
|
// Derive new KEK
|
|
858
904
|
const newSalt = crypto.getRandomValues(new Uint8Array(this._saltLength));
|
|
859
905
|
const newKek = await this._deriveKEK(new TextEncoder().encode(newPin), newSalt);
|
|
@@ -973,7 +1019,7 @@ class SecureDatabaseEncryption {
|
|
|
973
1019
|
keyData = serializer.serialize(privateKey);
|
|
974
1020
|
}
|
|
975
1021
|
|
|
976
|
-
const iv = crypto.getRandomValues(new Uint8Array(
|
|
1022
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
977
1023
|
|
|
978
1024
|
const encryptedKey = await crypto.subtle.encrypt(
|
|
979
1025
|
{
|
|
@@ -988,13 +1034,13 @@ class SecureDatabaseEncryption {
|
|
|
988
1034
|
|
|
989
1035
|
const authLength = new Uint32Array([authData.length]);
|
|
990
1036
|
const result = new Uint8Array(
|
|
991
|
-
|
|
1037
|
+
12 + 4 + authData.length + encryptedKey.byteLength
|
|
992
1038
|
);
|
|
993
1039
|
|
|
994
1040
|
result.set(iv, 0);
|
|
995
|
-
result.set(new Uint8Array(authLength.buffer),
|
|
996
|
-
result.set(authData,
|
|
997
|
-
result.set(new Uint8Array(encryptedKey),
|
|
1041
|
+
result.set(new Uint8Array(authLength.buffer), 12);
|
|
1042
|
+
result.set(authData, 16);
|
|
1043
|
+
result.set(new Uint8Array(encryptedKey), 16 + authData.length);
|
|
998
1044
|
|
|
999
1045
|
return base64.encode(result);
|
|
1000
1046
|
}
|
|
@@ -1006,11 +1052,11 @@ class SecureDatabaseEncryption {
|
|
|
1006
1052
|
|
|
1007
1053
|
const encryptedPackage = base64.decode(encryptedKeyString);
|
|
1008
1054
|
|
|
1009
|
-
const iv = encryptedPackage.slice(0,
|
|
1010
|
-
const authLengthBytes = encryptedPackage.slice(
|
|
1055
|
+
const iv = encryptedPackage.slice(0, 12);
|
|
1056
|
+
const authLengthBytes = encryptedPackage.slice(12, 16);
|
|
1011
1057
|
const authLength = new Uint32Array(authLengthBytes.buffer)[0];
|
|
1012
|
-
const authData = encryptedPackage.slice(
|
|
1013
|
-
const encryptedKey = encryptedPackage.slice(
|
|
1058
|
+
const authData = encryptedPackage.slice(16, 16 + authLength);
|
|
1059
|
+
const encryptedKey = encryptedPackage.slice(16 + authLength);
|
|
1014
1060
|
|
|
1015
1061
|
const encoder = new TextEncoder();
|
|
1016
1062
|
const expectedAuth = encoder.encode(additionalAuth);
|
|
@@ -1034,11 +1080,16 @@ class SecureDatabaseEncryption {
|
|
|
1034
1080
|
}
|
|
1035
1081
|
|
|
1036
1082
|
static generateSecurePIN(length = 6) {
|
|
1037
|
-
const digits =
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
.
|
|
1041
|
-
|
|
1083
|
+
const digits = [];
|
|
1084
|
+
const buf = new Uint8Array(1);
|
|
1085
|
+
while (digits.length < length) {
|
|
1086
|
+
crypto.getRandomValues(buf);
|
|
1087
|
+
// Rejection sampling: only accept values 0-249 to avoid modulo bias
|
|
1088
|
+
if (buf[0] < 250) {
|
|
1089
|
+
digits.push((buf[0] % 10).toString());
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return digits.join('');
|
|
1042
1093
|
}
|
|
1043
1094
|
|
|
1044
1095
|
destroy() {
|
|
@@ -1049,10 +1100,12 @@ class SecureDatabaseEncryption {
|
|
|
1049
1100
|
|
|
1050
1101
|
_arrayEquals(a, b) {
|
|
1051
1102
|
if (a.length !== b.length) return false;
|
|
1103
|
+
// Constant-time comparison to prevent timing attacks
|
|
1104
|
+
let diff = 0;
|
|
1052
1105
|
for (let i = 0; i < a.length; i++) {
|
|
1053
|
-
|
|
1106
|
+
diff |= a[i] ^ b[i];
|
|
1054
1107
|
}
|
|
1055
|
-
return
|
|
1108
|
+
return diff === 0;
|
|
1056
1109
|
}
|
|
1057
1110
|
|
|
1058
1111
|
exportMetadata() {
|
|
@@ -1385,13 +1438,14 @@ class BTreeIndex {
|
|
|
1385
1438
|
|
|
1386
1439
|
remove(key, value) {
|
|
1387
1440
|
if (!this._root) return;
|
|
1388
|
-
this._root.
|
|
1389
|
-
if (
|
|
1390
|
-
|
|
1441
|
+
const existing = this._root.search(key);
|
|
1442
|
+
if (existing && existing.has(value)) {
|
|
1443
|
+
this._root.remove(key, value);
|
|
1444
|
+
if (this._root.n === 0 && !this._root.leaf && this._root.children[0]) {
|
|
1391
1445
|
this._root = this._root.children[0];
|
|
1392
1446
|
}
|
|
1447
|
+
this._size--;
|
|
1393
1448
|
}
|
|
1394
|
-
this._size--;
|
|
1395
1449
|
}
|
|
1396
1450
|
|
|
1397
1451
|
verify() {
|
|
@@ -1492,7 +1546,7 @@ class TextIndex {
|
|
|
1492
1546
|
return text.toLowerCase()
|
|
1493
1547
|
.replace(/[^\w\s]/g, ' ')
|
|
1494
1548
|
.split(/\s+/)
|
|
1495
|
-
.filter(token => token.length >
|
|
1549
|
+
.filter(token => token.length > 1);
|
|
1496
1550
|
}
|
|
1497
1551
|
}
|
|
1498
1552
|
|
|
@@ -1521,7 +1575,7 @@ class GeoIndex {
|
|
|
1521
1575
|
|
|
1522
1576
|
removePoint(docId) {
|
|
1523
1577
|
this._tree.remove(docId);
|
|
1524
|
-
this._size--;
|
|
1578
|
+
if (this._size > 0) this._size--;
|
|
1525
1579
|
}
|
|
1526
1580
|
|
|
1527
1581
|
updatePoint(docId, newCoords) {
|
|
@@ -1636,36 +1690,58 @@ class IndexManager {
|
|
|
1636
1690
|
const indexData = this._createIndexStructure(index.type);
|
|
1637
1691
|
this._indexData.set(indexName, indexData);
|
|
1638
1692
|
|
|
1639
|
-
// Optimization: Cursor
|
|
1640
|
-
|
|
1641
|
-
|
|
1693
|
+
// Optimization: Use Batched Processing instead of single Cursor
|
|
1694
|
+
// This prevents transaction timeouts caused by async crypto operations inside the loop
|
|
1695
|
+
let lastKey = null;
|
|
1696
|
+
const batchSize = 100; // Keep batch small for responsiveness
|
|
1697
|
+
|
|
1698
|
+
while (true) {
|
|
1699
|
+
// 1. Fetch Batch (Transaction opens and closes here)
|
|
1700
|
+
const batch = await this._collection._indexedDB.getBatch(
|
|
1701
|
+
this._collection._db,
|
|
1702
|
+
'documents',
|
|
1703
|
+
lastKey,
|
|
1704
|
+
batchSize
|
|
1705
|
+
);
|
|
1642
1706
|
|
|
1643
|
-
if (
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1707
|
+
if (batch.length === 0) break;
|
|
1708
|
+
|
|
1709
|
+
// 2. Process Batch (Async crypto operations safe here)
|
|
1710
|
+
for (const docData of batch) {
|
|
1711
|
+
lastKey = docData._id; // Update for next batch
|
|
1712
|
+
let doc = docData;
|
|
1713
|
+
|
|
1714
|
+
if (docData.packedData) {
|
|
1715
|
+
const d = new Document(docData, {
|
|
1716
|
+
compressed: docData._compressed,
|
|
1717
|
+
encrypted: docData._encrypted
|
|
1718
|
+
});
|
|
1719
|
+
// This await is what killed the transaction before
|
|
1720
|
+
await d.unpack(this._collection.database.encryption);
|
|
1721
|
+
doc = d.objectOutput();
|
|
1722
|
+
}
|
|
1651
1723
|
|
|
1652
|
-
|
|
1724
|
+
let value = this._getFieldValue(doc, index.fieldPath);
|
|
1653
1725
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1726
|
+
if (index.sparse && (value === null || value === undefined)) {
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1657
1729
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1730
|
+
if (index.unique && indexData.has && indexData.has(value)) {
|
|
1731
|
+
console.error(`Unique constraint violation on index '${indexName}'`);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1662
1734
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1735
|
+
if (index.hashed && index.type === 'btree') {
|
|
1736
|
+
value = await this._hashVal(value);
|
|
1737
|
+
}
|
|
1666
1738
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1739
|
+
this._addToIndex(indexData, value, doc._id, index.type);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Optional: Yield to main thread briefly to prevent UI freeze
|
|
1743
|
+
if (window.requestIdleCallback) await new Promise(r => window.requestIdleCallback(r));
|
|
1744
|
+
}
|
|
1669
1745
|
}
|
|
1670
1746
|
|
|
1671
1747
|
_createIndexStructure(type) {
|
|
@@ -1883,7 +1959,7 @@ class IndexManager {
|
|
|
1883
1959
|
async _saveIndexMetadata() {
|
|
1884
1960
|
const key = `lacertadb_${this._collection.database.name}_${this._collection.name}_indexes`;
|
|
1885
1961
|
return new Promise((resolve) => {
|
|
1886
|
-
|
|
1962
|
+
const save = () => {
|
|
1887
1963
|
const metadata = {
|
|
1888
1964
|
indexes: Array.from(this._indexes.entries()).map(([name, index]) => ({
|
|
1889
1965
|
name,
|
|
@@ -1894,7 +1970,12 @@ class IndexManager {
|
|
|
1894
1970
|
const encoded = base64.encode(serialized);
|
|
1895
1971
|
localStorage.setItem(key, encoded);
|
|
1896
1972
|
resolve();
|
|
1897
|
-
}
|
|
1973
|
+
};
|
|
1974
|
+
if (typeof window !== 'undefined' && window.requestIdleCallback) {
|
|
1975
|
+
window.requestIdleCallback(save);
|
|
1976
|
+
} else {
|
|
1977
|
+
setTimeout(save, 0);
|
|
1978
|
+
}
|
|
1898
1979
|
});
|
|
1899
1980
|
}
|
|
1900
1981
|
|
|
@@ -2081,7 +2162,7 @@ class OPFSUtility {
|
|
|
2081
2162
|
}
|
|
2082
2163
|
|
|
2083
2164
|
// ========================
|
|
2084
|
-
// IndexedDB Utility (Optimized with
|
|
2165
|
+
// IndexedDB Utility (Optimized with Batches)
|
|
2085
2166
|
// ========================
|
|
2086
2167
|
|
|
2087
2168
|
class IndexedDBUtility {
|
|
@@ -2139,6 +2220,19 @@ class IndexedDBUtility {
|
|
|
2139
2220
|
});
|
|
2140
2221
|
}
|
|
2141
2222
|
|
|
2223
|
+
// New: Batched Retrieval for processing large datasets efficiently
|
|
2224
|
+
async getBatch(db, storeName, lastKey, limit) {
|
|
2225
|
+
return this.performTransaction(db, [storeName], 'readonly', tx => {
|
|
2226
|
+
const store = tx.objectStore(storeName);
|
|
2227
|
+
let range;
|
|
2228
|
+
if (lastKey !== null && lastKey !== undefined) {
|
|
2229
|
+
range = IDBKeyRange.lowerBound(lastKey, true); // true = open range (skip lastKey)
|
|
2230
|
+
}
|
|
2231
|
+
// Use getAll which is faster than cursor for batches
|
|
2232
|
+
return this._promisifyRequest(() => store.getAll(range, limit));
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2142
2236
|
add(db, storeName, value, key) {
|
|
2143
2237
|
return this.performTransaction(db, [storeName], 'readwrite', tx => {
|
|
2144
2238
|
const store = tx.objectStore(storeName);
|
|
@@ -2165,23 +2259,6 @@ class IndexedDBUtility {
|
|
|
2165
2259
|
});
|
|
2166
2260
|
}
|
|
2167
2261
|
|
|
2168
|
-
// Cursor iteration for memory efficiency
|
|
2169
|
-
async iterate(db, storeName, callback) {
|
|
2170
|
-
return this.performTransaction(db, [storeName], 'readonly', tx => {
|
|
2171
|
-
return new Promise((resolve, reject) => {
|
|
2172
|
-
const req = tx.objectStore(storeName).openCursor();
|
|
2173
|
-
req.onsuccess = async (e) => {
|
|
2174
|
-
const cursor = e.target.result;
|
|
2175
|
-
if (cursor) {
|
|
2176
|
-
await callback(cursor.value);
|
|
2177
|
-
cursor.continue();
|
|
2178
|
-
}
|
|
2179
|
-
};
|
|
2180
|
-
req.onerror = () => reject(req.error);
|
|
2181
|
-
});
|
|
2182
|
-
});
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
2262
|
delete(db, storeName, key) {
|
|
2186
2263
|
return this.performTransaction(db, [storeName], 'readwrite', tx => {
|
|
2187
2264
|
return this._promisifyRequest(() => tx.objectStore(storeName).delete(key));
|
|
@@ -2201,33 +2278,32 @@ class IndexedDBUtility {
|
|
|
2201
2278
|
}
|
|
2202
2279
|
|
|
2203
2280
|
async batchOperation(db, operations) {
|
|
2204
|
-
return this.performTransaction(db, ['documents'], 'readwrite',
|
|
2281
|
+
return this.performTransaction(db, ['documents'], 'readwrite', tx => {
|
|
2205
2282
|
const store = tx.objectStore('documents');
|
|
2206
|
-
const results = [];
|
|
2207
2283
|
|
|
2208
|
-
|
|
2284
|
+
// CRITICAL: Queue ALL IDB requests synchronously to prevent
|
|
2285
|
+
// TransactionInactiveError. Do NOT use await between requests.
|
|
2286
|
+
const promises = operations.map(op => {
|
|
2209
2287
|
try {
|
|
2210
|
-
let result;
|
|
2211
2288
|
switch (op.type) {
|
|
2212
2289
|
case 'add':
|
|
2213
|
-
|
|
2214
|
-
|
|
2290
|
+
return this._promisifyRequest(() => store.add(op.data))
|
|
2291
|
+
.then(result => ({ success: true, result }));
|
|
2215
2292
|
case 'put':
|
|
2216
|
-
|
|
2217
|
-
|
|
2293
|
+
return this._promisifyRequest(() => store.put(op.data))
|
|
2294
|
+
.then(result => ({ success: true, result }));
|
|
2218
2295
|
case 'delete':
|
|
2219
|
-
|
|
2220
|
-
|
|
2296
|
+
return this._promisifyRequest(() => store.delete(op.key))
|
|
2297
|
+
.then(result => ({ success: true, result }));
|
|
2221
2298
|
default:
|
|
2222
|
-
|
|
2299
|
+
return Promise.resolve({ success: false, error: `Unknown operation type: ${op.type}` });
|
|
2223
2300
|
}
|
|
2224
|
-
results.push({ success: true, result });
|
|
2225
2301
|
} catch (error) {
|
|
2226
|
-
|
|
2302
|
+
return Promise.resolve({ success: false, error: error.message });
|
|
2227
2303
|
}
|
|
2228
|
-
}
|
|
2304
|
+
});
|
|
2229
2305
|
|
|
2230
|
-
return
|
|
2306
|
+
return Promise.all(promises);
|
|
2231
2307
|
});
|
|
2232
2308
|
}
|
|
2233
2309
|
}
|
|
@@ -2654,7 +2730,7 @@ class AggregationPipeline {
|
|
|
2654
2730
|
for (const doc of docs) {
|
|
2655
2731
|
const groupKey = typeof idField === 'string' ?
|
|
2656
2732
|
queryEngine.getFieldValue(doc, idField.replace('$', '')) :
|
|
2657
|
-
|
|
2733
|
+
JSON.stringify(idField);
|
|
2658
2734
|
|
|
2659
2735
|
if (!groups.has(groupKey)) {
|
|
2660
2736
|
groups.set(groupKey, { _id: groupKey, docs: [] });
|
|
@@ -2675,10 +2751,11 @@ class AggregationPipeline {
|
|
|
2675
2751
|
case '$sum':
|
|
2676
2752
|
result[fieldKey] = group.docs.reduce((sum, d) => sum + (queryEngine.getFieldValue(d, field) || 0), 0);
|
|
2677
2753
|
break;
|
|
2678
|
-
case '$avg':
|
|
2754
|
+
case '$avg': {
|
|
2679
2755
|
const sum = group.docs.reduce((s, d) => s + (queryEngine.getFieldValue(d, field) || 0), 0);
|
|
2680
2756
|
result[fieldKey] = sum / group.docs.length;
|
|
2681
2757
|
break;
|
|
2758
|
+
}
|
|
2682
2759
|
case '$count':
|
|
2683
2760
|
result[fieldKey] = group.docs.length;
|
|
2684
2761
|
break;
|
|
@@ -3243,7 +3320,7 @@ class Collection {
|
|
|
3243
3320
|
return result;
|
|
3244
3321
|
}
|
|
3245
3322
|
|
|
3246
|
-
async batchAdd(documents, options) {
|
|
3323
|
+
async batchAdd(documents, options = {}) {
|
|
3247
3324
|
if (!this._initialized) await this.init();
|
|
3248
3325
|
|
|
3249
3326
|
const startTime = performance.now();
|
|
@@ -3623,7 +3700,7 @@ class Database {
|
|
|
3623
3700
|
|
|
3624
3701
|
async export(format = 'json', password = null) {
|
|
3625
3702
|
const data = {
|
|
3626
|
-
version: '0.9.
|
|
3703
|
+
version: '0.9.2',
|
|
3627
3704
|
database: this.name,
|
|
3628
3705
|
timestamp: Date.now(),
|
|
3629
3706
|
collections: {}
|
|
@@ -3704,7 +3781,7 @@ class Database {
|
|
|
3704
3781
|
|
|
3705
3782
|
// Clear quickstore
|
|
3706
3783
|
if (this._quickStore) {
|
|
3707
|
-
this._quickStore.
|
|
3784
|
+
this._quickStore.destroy();
|
|
3708
3785
|
}
|
|
3709
3786
|
|
|
3710
3787
|
// Destroy encryption
|
|
@@ -3790,7 +3867,7 @@ class LacertaDB {
|
|
|
3790
3867
|
|
|
3791
3868
|
async createBackup(password = null) {
|
|
3792
3869
|
const backup = {
|
|
3793
|
-
version: '0.9.
|
|
3870
|
+
version: '0.9.2',
|
|
3794
3871
|
timestamp: Date.now(),
|
|
3795
3872
|
databases: {}
|
|
3796
3873
|
};
|