@pixagram/lacerta-db 0.3.1 → 0.5.1
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 +6 -5
- package/dist/index.min.js +6 -5
- package/index.js +1544 -88
- package/legacy.js +1901 -0
- package/package.json +8 -4
- package/readme.md +176 -60
package/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LacertaDB
|
|
3
|
-
* A powerful browser-based document database with encryption, compression,
|
|
4
|
-
*
|
|
2
|
+
* LacertaDB V5.0.0 - Complete Production Library
|
|
3
|
+
* A powerful browser-based document database with encryption, compression, OPFS support,
|
|
4
|
+
* custom indexes, LRU caching, and database-level security for private keys
|
|
5
|
+
* @version 5.0.0
|
|
5
6
|
* @license MIT
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
'use strict';
|
|
9
|
-
|
|
10
|
-
//
|
|
10
|
+
|
|
11
|
+
// Dependencies - for browser environments using a bundler (e.g., Webpack, Vite)
|
|
11
12
|
import TurboSerial from "@pixagram/turboserial";
|
|
12
13
|
import TurboBase64 from "@pixagram/turbobase64";
|
|
13
14
|
|
|
@@ -20,9 +21,10 @@ const serializer = new TurboSerial({
|
|
|
20
21
|
});
|
|
21
22
|
const base64 = new TurboBase64();
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// ========================
|
|
25
|
+
// Async Mutex for managing concurrent operations
|
|
26
|
+
// ========================
|
|
27
|
+
|
|
26
28
|
class AsyncMutex {
|
|
27
29
|
constructor() {
|
|
28
30
|
this._queue = [];
|
|
@@ -60,9 +62,10 @@ class AsyncMutex {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// ========================
|
|
66
|
+
// Custom error class for LacertaDB
|
|
67
|
+
// ========================
|
|
68
|
+
|
|
66
69
|
class LacertaDBError extends Error {
|
|
67
70
|
constructor(message, code, originalError) {
|
|
68
71
|
super(message);
|
|
@@ -73,6 +76,269 @@ class LacertaDBError extends Error {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
// ========================
|
|
80
|
+
// LRU Cache Implementation
|
|
81
|
+
// ========================
|
|
82
|
+
|
|
83
|
+
class LRUCache {
|
|
84
|
+
constructor(maxSize = 100, ttl = null) {
|
|
85
|
+
this.maxSize = maxSize;
|
|
86
|
+
this.ttl = ttl;
|
|
87
|
+
this.cache = new Map();
|
|
88
|
+
this.accessOrder = [];
|
|
89
|
+
this.timestamps = new Map();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get(key) {
|
|
93
|
+
if (!this.cache.has(key)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.ttl) {
|
|
98
|
+
const timestamp = this.timestamps.get(key);
|
|
99
|
+
if (Date.now() - timestamp > this.ttl) {
|
|
100
|
+
this.delete(key);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const index = this.accessOrder.indexOf(key);
|
|
106
|
+
if (index > -1) {
|
|
107
|
+
this.accessOrder.splice(index, 1);
|
|
108
|
+
}
|
|
109
|
+
this.accessOrder.push(key);
|
|
110
|
+
|
|
111
|
+
return this.cache.get(key);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
set(key, value) {
|
|
115
|
+
if (this.cache.has(key)) {
|
|
116
|
+
const index = this.accessOrder.indexOf(key);
|
|
117
|
+
if (index > -1) {
|
|
118
|
+
this.accessOrder.splice(index, 1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.cache.set(key, value);
|
|
123
|
+
this.accessOrder.push(key);
|
|
124
|
+
this.timestamps.set(key, Date.now());
|
|
125
|
+
|
|
126
|
+
while (this.cache.size > this.maxSize) {
|
|
127
|
+
const oldest = this.accessOrder.shift();
|
|
128
|
+
this.cache.delete(oldest);
|
|
129
|
+
this.timestamps.delete(oldest);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
delete(key) {
|
|
134
|
+
const index = this.accessOrder.indexOf(key);
|
|
135
|
+
if (index > -1) {
|
|
136
|
+
this.accessOrder.splice(index, 1);
|
|
137
|
+
}
|
|
138
|
+
this.timestamps.delete(key);
|
|
139
|
+
return this.cache.delete(key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
clear() {
|
|
143
|
+
this.cache.clear();
|
|
144
|
+
this.accessOrder = [];
|
|
145
|
+
this.timestamps.clear();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
has(key) {
|
|
149
|
+
if (this.ttl && this.cache.has(key)) {
|
|
150
|
+
const timestamp = this.timestamps.get(key);
|
|
151
|
+
if (Date.now() - timestamp > this.ttl) {
|
|
152
|
+
this.delete(key);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return this.cache.has(key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get size() {
|
|
160
|
+
return this.cache.size;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// LFU (Least Frequently Used) Cache
|
|
165
|
+
class LFUCache {
|
|
166
|
+
constructor(maxSize = 100, ttl = null) {
|
|
167
|
+
this.maxSize = maxSize;
|
|
168
|
+
this.ttl = ttl;
|
|
169
|
+
this.cache = new Map();
|
|
170
|
+
this.frequencies = new Map();
|
|
171
|
+
this.timestamps = new Map();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get(key) {
|
|
175
|
+
if (!this.cache.has(key)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this.ttl) {
|
|
180
|
+
const timestamp = this.timestamps.get(key);
|
|
181
|
+
if (Date.now() - timestamp > this.ttl) {
|
|
182
|
+
this.delete(key);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.frequencies.set(key, (this.frequencies.get(key) || 0) + 1);
|
|
188
|
+
return this.cache.get(key);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
set(key, value) {
|
|
192
|
+
if (this.cache.has(key)) {
|
|
193
|
+
this.cache.set(key, value);
|
|
194
|
+
this.frequencies.set(key, (this.frequencies.get(key) || 0) + 1);
|
|
195
|
+
} else {
|
|
196
|
+
if (this.cache.size >= this.maxSize) {
|
|
197
|
+
let minFreq = Infinity;
|
|
198
|
+
let evictKey = null;
|
|
199
|
+
for (const [k, freq] of this.frequencies) {
|
|
200
|
+
if (freq < minFreq) {
|
|
201
|
+
minFreq = freq;
|
|
202
|
+
evictKey = k;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (evictKey) {
|
|
206
|
+
this.delete(evictKey);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.cache.set(key, value);
|
|
211
|
+
this.frequencies.set(key, 1);
|
|
212
|
+
this.timestamps.set(key, Date.now());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
delete(key) {
|
|
217
|
+
this.frequencies.delete(key);
|
|
218
|
+
this.timestamps.delete(key);
|
|
219
|
+
return this.cache.delete(key);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
clear() {
|
|
223
|
+
this.cache.clear();
|
|
224
|
+
this.frequencies.clear();
|
|
225
|
+
this.timestamps.clear();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
has(key) {
|
|
229
|
+
return this.cache.has(key);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get size() {
|
|
233
|
+
return this.cache.size;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// TTL (Time To Live) Only Cache
|
|
238
|
+
class TTLCache {
|
|
239
|
+
constructor(ttl = 60000) {
|
|
240
|
+
this.ttl = ttl;
|
|
241
|
+
this.cache = new Map();
|
|
242
|
+
this.timers = new Map();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get(key) {
|
|
246
|
+
return this.cache.get(key) || null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
set(key, value) {
|
|
250
|
+
if (this.timers.has(key)) {
|
|
251
|
+
clearTimeout(this.timers.get(key));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.cache.set(key, value);
|
|
255
|
+
|
|
256
|
+
const timer = setTimeout(() => {
|
|
257
|
+
this.delete(key);
|
|
258
|
+
}, this.ttl);
|
|
259
|
+
this.timers.set(key, timer);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
delete(key) {
|
|
263
|
+
if (this.timers.has(key)) {
|
|
264
|
+
clearTimeout(this.timers.get(key));
|
|
265
|
+
this.timers.delete(key);
|
|
266
|
+
}
|
|
267
|
+
return this.cache.delete(key);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
clear() {
|
|
271
|
+
for (const timer of this.timers.values()) {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
}
|
|
274
|
+
this.timers.clear();
|
|
275
|
+
this.cache.clear();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
has(key) {
|
|
279
|
+
return this.cache.has(key);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
get size() {
|
|
283
|
+
return this.cache.size;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ========================
|
|
288
|
+
// Cache Strategy System
|
|
289
|
+
// ========================
|
|
290
|
+
|
|
291
|
+
class CacheStrategy {
|
|
292
|
+
constructor(config = {}) {
|
|
293
|
+
this.type = config.type || 'lru';
|
|
294
|
+
this.maxSize = config.maxSize || 100;
|
|
295
|
+
this.ttl = config.ttl || null;
|
|
296
|
+
this.enabled = config.enabled !== false;
|
|
297
|
+
|
|
298
|
+
this.cache = this.createCache();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
createCache() {
|
|
302
|
+
switch (this.type) {
|
|
303
|
+
case 'lru':
|
|
304
|
+
return new LRUCache(this.maxSize, this.ttl);
|
|
305
|
+
case 'lfu':
|
|
306
|
+
return new LFUCache(this.maxSize, this.ttl);
|
|
307
|
+
case 'ttl':
|
|
308
|
+
return new TTLCache(this.ttl);
|
|
309
|
+
case 'none':
|
|
310
|
+
return null;
|
|
311
|
+
default:
|
|
312
|
+
return new LRUCache(this.maxSize, this.ttl);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
get(key) {
|
|
317
|
+
if (!this.enabled || !this.cache) return null;
|
|
318
|
+
return this.cache.get(key);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
set(key, value) {
|
|
322
|
+
if (!this.enabled || !this.cache) return;
|
|
323
|
+
this.cache.set(key, value);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
delete(key) {
|
|
327
|
+
if (!this.enabled || !this.cache) return;
|
|
328
|
+
this.cache.delete(key);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
clear() {
|
|
332
|
+
if (!this.enabled || !this.cache) return;
|
|
333
|
+
this.cache.clear();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
updateStrategy(newConfig) {
|
|
337
|
+
Object.assign(this, newConfig);
|
|
338
|
+
this.cache = this.createCache();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
76
342
|
// ========================
|
|
77
343
|
// Compression Utility
|
|
78
344
|
// ========================
|
|
@@ -106,7 +372,6 @@ class BrowserCompressionUtility {
|
|
|
106
372
|
}
|
|
107
373
|
}
|
|
108
374
|
|
|
109
|
-
// Fallback sync methods are simple pass-throughs
|
|
110
375
|
compressSync(input) {
|
|
111
376
|
if (!(input instanceof Uint8Array)) {
|
|
112
377
|
throw new TypeError('Input must be Uint8Array');
|
|
@@ -123,7 +388,7 @@ class BrowserCompressionUtility {
|
|
|
123
388
|
}
|
|
124
389
|
|
|
125
390
|
// ========================
|
|
126
|
-
// Encryption Utility
|
|
391
|
+
// Encryption Utility
|
|
127
392
|
// ========================
|
|
128
393
|
|
|
129
394
|
class BrowserEncryptionUtility {
|
|
@@ -165,7 +430,6 @@ class BrowserEncryptionUtility {
|
|
|
165
430
|
data
|
|
166
431
|
);
|
|
167
432
|
|
|
168
|
-
// The checksum was removed as AES-GCM provides this via an authentication tag.
|
|
169
433
|
const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
|
|
170
434
|
result.set(salt, 0);
|
|
171
435
|
result.set(iv, salt.length);
|
|
@@ -216,15 +480,984 @@ class BrowserEncryptionUtility {
|
|
|
216
480
|
encryptedData
|
|
217
481
|
);
|
|
218
482
|
|
|
219
|
-
// Checksum verification removed. crypto.subtle.decrypt will throw on failure.
|
|
220
483
|
return new Uint8Array(decrypted);
|
|
221
484
|
} catch (error) {
|
|
222
|
-
// Provide a more specific error for failed decryption, which often indicates a wrong password.
|
|
223
485
|
throw new LacertaDBError('Decryption failed. This may be due to an incorrect password or corrupted data.', 'DECRYPTION_FAILED', error);
|
|
224
486
|
}
|
|
225
487
|
}
|
|
226
488
|
}
|
|
227
489
|
|
|
490
|
+
// ========================
|
|
491
|
+
// Database-Level Encryption
|
|
492
|
+
// ========================
|
|
493
|
+
|
|
494
|
+
class SecureDatabaseEncryption {
|
|
495
|
+
constructor() {
|
|
496
|
+
this.masterKey = null;
|
|
497
|
+
this.salt = null;
|
|
498
|
+
this.iterations = 1000000;
|
|
499
|
+
this.initialized = false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async initialize(pin, salt = null) {
|
|
503
|
+
if (this.initialized) {
|
|
504
|
+
throw new Error('Database encryption already initialized');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.salt = salt || crypto.getRandomValues(new Uint8Array(32));
|
|
508
|
+
|
|
509
|
+
const encoder = new TextEncoder();
|
|
510
|
+
const pinBuffer = encoder.encode(pin);
|
|
511
|
+
|
|
512
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
513
|
+
'raw',
|
|
514
|
+
pinBuffer,
|
|
515
|
+
'PBKDF2',
|
|
516
|
+
false,
|
|
517
|
+
['deriveBits', 'deriveKey']
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const derivedBits = await crypto.subtle.deriveBits(
|
|
521
|
+
{
|
|
522
|
+
name: 'PBKDF2',
|
|
523
|
+
salt: this.salt,
|
|
524
|
+
iterations: this.iterations,
|
|
525
|
+
hash: 'SHA-512'
|
|
526
|
+
},
|
|
527
|
+
keyMaterial,
|
|
528
|
+
512
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const derivedArray = new Uint8Array(derivedBits);
|
|
532
|
+
|
|
533
|
+
const encKeyBytes = derivedArray.slice(0, 32);
|
|
534
|
+
const hmacKeyBytes = derivedArray.slice(32, 64);
|
|
535
|
+
|
|
536
|
+
this.encKey = await crypto.subtle.importKey(
|
|
537
|
+
'raw',
|
|
538
|
+
encKeyBytes,
|
|
539
|
+
{ name: 'AES-GCM', length: 256 },
|
|
540
|
+
false,
|
|
541
|
+
['encrypt', 'decrypt']
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
this.hmacKey = await crypto.subtle.importKey(
|
|
545
|
+
'raw',
|
|
546
|
+
hmacKeyBytes,
|
|
547
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
548
|
+
false,
|
|
549
|
+
['sign', 'verify']
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
this.initialized = true;
|
|
553
|
+
|
|
554
|
+
return base64.encode(this.salt);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async encrypt(data) {
|
|
558
|
+
if (!this.initialized) {
|
|
559
|
+
throw new Error('Database encryption not initialized');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let dataBytes;
|
|
563
|
+
if (typeof data === 'string') {
|
|
564
|
+
dataBytes = new TextEncoder().encode(data);
|
|
565
|
+
} else if (data instanceof Uint8Array) {
|
|
566
|
+
dataBytes = data;
|
|
567
|
+
} else {
|
|
568
|
+
dataBytes = serializer.serialize(data);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
572
|
+
|
|
573
|
+
const encryptedData = await crypto.subtle.encrypt(
|
|
574
|
+
{ name: 'AES-GCM', iv },
|
|
575
|
+
this.encKey,
|
|
576
|
+
dataBytes
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const hmacData = new Uint8Array(iv.length + encryptedData.byteLength);
|
|
580
|
+
hmacData.set(iv, 0);
|
|
581
|
+
hmacData.set(new Uint8Array(encryptedData), iv.length);
|
|
582
|
+
|
|
583
|
+
const hmac = await crypto.subtle.sign(
|
|
584
|
+
'HMAC',
|
|
585
|
+
this.hmacKey,
|
|
586
|
+
hmacData
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const result = new Uint8Array(
|
|
590
|
+
iv.length + encryptedData.byteLength + 32
|
|
591
|
+
);
|
|
592
|
+
result.set(iv, 0);
|
|
593
|
+
result.set(new Uint8Array(encryptedData), iv.length);
|
|
594
|
+
result.set(new Uint8Array(hmac), iv.length + encryptedData.byteLength);
|
|
595
|
+
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async decrypt(encryptedPackage) {
|
|
600
|
+
if (!this.initialized) {
|
|
601
|
+
throw new Error('Database encryption not initialized');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!(encryptedPackage instanceof Uint8Array)) {
|
|
605
|
+
throw new TypeError('Encrypted data must be Uint8Array');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const iv = encryptedPackage.slice(0, 12);
|
|
609
|
+
const hmac = encryptedPackage.slice(-32);
|
|
610
|
+
const encryptedData = encryptedPackage.slice(12, -32);
|
|
611
|
+
|
|
612
|
+
const hmacData = new Uint8Array(iv.length + encryptedData.length);
|
|
613
|
+
hmacData.set(iv, 0);
|
|
614
|
+
hmacData.set(encryptedData, iv.length);
|
|
615
|
+
|
|
616
|
+
const isValid = await crypto.subtle.verify(
|
|
617
|
+
'HMAC',
|
|
618
|
+
this.hmacKey,
|
|
619
|
+
hmac,
|
|
620
|
+
hmacData
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
if (!isValid) {
|
|
624
|
+
throw new Error('HMAC verification failed - data may be tampered');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const decryptedData = await crypto.subtle.decrypt(
|
|
628
|
+
{ name: 'AES-GCM', iv },
|
|
629
|
+
this.encKey,
|
|
630
|
+
encryptedData
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
return new Uint8Array(decryptedData);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async encryptPrivateKey(privateKey, additionalAuth = '') {
|
|
637
|
+
if (!this.initialized) {
|
|
638
|
+
throw new Error('Database encryption not initialized');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const encoder = new TextEncoder();
|
|
642
|
+
const authData = encoder.encode(additionalAuth);
|
|
643
|
+
|
|
644
|
+
let keyData;
|
|
645
|
+
if (typeof privateKey === 'string') {
|
|
646
|
+
keyData = encoder.encode(privateKey);
|
|
647
|
+
} else if (privateKey instanceof Uint8Array) {
|
|
648
|
+
keyData = privateKey;
|
|
649
|
+
} else {
|
|
650
|
+
keyData = encoder.encode(JSON.stringify(privateKey));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
654
|
+
|
|
655
|
+
const encryptedKey = await crypto.subtle.encrypt(
|
|
656
|
+
{
|
|
657
|
+
name: 'AES-GCM',
|
|
658
|
+
iv,
|
|
659
|
+
additionalData: authData,
|
|
660
|
+
tagLength: 128
|
|
661
|
+
},
|
|
662
|
+
this.encKey,
|
|
663
|
+
keyData
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const authLength = new Uint32Array([authData.length]);
|
|
667
|
+
const result = new Uint8Array(
|
|
668
|
+
16 + 4 + authData.length + encryptedKey.byteLength
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
result.set(iv, 0);
|
|
672
|
+
result.set(new Uint8Array(authLength.buffer), 16);
|
|
673
|
+
result.set(authData, 20);
|
|
674
|
+
result.set(new Uint8Array(encryptedKey), 20 + authData.length);
|
|
675
|
+
|
|
676
|
+
return base64.encode(result);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async decryptPrivateKey(encryptedKeyString, additionalAuth = '') {
|
|
680
|
+
if (!this.initialized) {
|
|
681
|
+
throw new Error('Database encryption not initialized');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const encryptedPackage = base64.decode(encryptedKeyString);
|
|
685
|
+
|
|
686
|
+
const iv = encryptedPackage.slice(0, 16);
|
|
687
|
+
const authLengthBytes = encryptedPackage.slice(16, 20);
|
|
688
|
+
const authLength = new Uint32Array(authLengthBytes.buffer)[0];
|
|
689
|
+
const authData = encryptedPackage.slice(20, 20 + authLength);
|
|
690
|
+
const encryptedKey = encryptedPackage.slice(20 + authLength);
|
|
691
|
+
|
|
692
|
+
const encoder = new TextEncoder();
|
|
693
|
+
const expectedAuth = encoder.encode(additionalAuth);
|
|
694
|
+
|
|
695
|
+
if (!this.arrayEquals(authData, expectedAuth)) {
|
|
696
|
+
throw new Error('Additional authentication data mismatch');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const decryptedKey = await crypto.subtle.decrypt(
|
|
700
|
+
{
|
|
701
|
+
name: 'AES-GCM',
|
|
702
|
+
iv,
|
|
703
|
+
additionalData: authData,
|
|
704
|
+
tagLength: 128
|
|
705
|
+
},
|
|
706
|
+
this.encKey,
|
|
707
|
+
encryptedKey
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
return new TextDecoder().decode(decryptedKey);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
static generateSecurePIN(length = 6) {
|
|
714
|
+
const digits = new Uint8Array(length);
|
|
715
|
+
crypto.getRandomValues(digits);
|
|
716
|
+
return Array.from(digits)
|
|
717
|
+
.map(b => (b % 10).toString())
|
|
718
|
+
.join('');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
destroy() {
|
|
722
|
+
this.masterKey = null;
|
|
723
|
+
this.encKey = null;
|
|
724
|
+
this.hmacKey = null;
|
|
725
|
+
this.salt = null;
|
|
726
|
+
this.initialized = false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
arrayEquals(a, b) {
|
|
730
|
+
if (a.length !== b.length) return false;
|
|
731
|
+
for (let i = 0; i < a.length; i++) {
|
|
732
|
+
if (a[i] !== b[i]) return false;
|
|
733
|
+
}
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async changePin(oldPin, newPin) {
|
|
738
|
+
if (!this.initialized) {
|
|
739
|
+
throw new Error('Database encryption not initialized');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const currentSalt = this.salt;
|
|
743
|
+
|
|
744
|
+
this.destroy();
|
|
745
|
+
await this.initialize(oldPin, currentSalt);
|
|
746
|
+
|
|
747
|
+
this.destroy();
|
|
748
|
+
const newSalt = await this.initialize(newPin);
|
|
749
|
+
|
|
750
|
+
return newSalt;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
exportMetadata() {
|
|
754
|
+
if (!this.salt) {
|
|
755
|
+
throw new Error('No encryption metadata to export');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
salt: base64.encode(this.salt),
|
|
760
|
+
iterations: this.iterations,
|
|
761
|
+
algorithm: 'AES-GCM-256',
|
|
762
|
+
kdf: 'PBKDF2-SHA-512'
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
importMetadata(metadata) {
|
|
767
|
+
if (!metadata.salt) {
|
|
768
|
+
throw new Error('Invalid encryption metadata');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
this.salt = base64.decode(metadata.salt);
|
|
772
|
+
this.iterations = metadata.iterations || 1000000;
|
|
773
|
+
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ========================
|
|
779
|
+
// B-Tree Index Implementation
|
|
780
|
+
// ========================
|
|
781
|
+
|
|
782
|
+
class BTreeNode {
|
|
783
|
+
constructor(order, leaf) {
|
|
784
|
+
this.keys = new Array(2 * order - 1);
|
|
785
|
+
this.values = new Array(2 * order - 1);
|
|
786
|
+
this.children = new Array(2 * order);
|
|
787
|
+
this.n = 0;
|
|
788
|
+
this.leaf = leaf;
|
|
789
|
+
this.order = order;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
search(key) {
|
|
793
|
+
let i = 0;
|
|
794
|
+
while (i < this.n && key > this.keys[i]) {
|
|
795
|
+
i++;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (i < this.n && key === this.keys[i]) {
|
|
799
|
+
return this.values[i];
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (this.leaf) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return this.children[i].search(key);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
rangeSearch(min, max, results) {
|
|
810
|
+
let i = 0;
|
|
811
|
+
|
|
812
|
+
while (i < this.n) {
|
|
813
|
+
if (!this.leaf && this.children[i]) {
|
|
814
|
+
this.children[i].rangeSearch(min, max, results);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (this.keys[i] >= min && this.keys[i] <= max) {
|
|
818
|
+
this.values[i].forEach(v => results.push(v));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
i++;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (!this.leaf && this.children[i]) {
|
|
825
|
+
this.children[i].rangeSearch(min, max, results);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
insertNonFull(key, value) {
|
|
830
|
+
let i = this.n - 1;
|
|
831
|
+
|
|
832
|
+
if (this.leaf) {
|
|
833
|
+
while (i >= 0 && this.keys[i] > key) {
|
|
834
|
+
this.keys[i + 1] = this.keys[i];
|
|
835
|
+
this.values[i + 1] = this.values[i];
|
|
836
|
+
i--;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (i >= 0 && this.keys[i] === key) {
|
|
840
|
+
this.values[i].add(value);
|
|
841
|
+
} else {
|
|
842
|
+
this.keys[i + 1] = key;
|
|
843
|
+
this.values[i + 1] = new Set([value]);
|
|
844
|
+
this.n++;
|
|
845
|
+
}
|
|
846
|
+
} else {
|
|
847
|
+
while (i >= 0 && this.keys[i] > key) {
|
|
848
|
+
i--;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (i >= 0 && this.keys[i] === key) {
|
|
852
|
+
this.values[i].add(value);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
i++;
|
|
857
|
+
if (this.children[i].n === 2 * this.order - 1) {
|
|
858
|
+
this.splitChild(i, this.children[i]);
|
|
859
|
+
if (this.keys[i] < key) {
|
|
860
|
+
i++;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
this.children[i].insertNonFull(key, value);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
splitChild(i, y) {
|
|
868
|
+
const z = new BTreeNode(this.order, y.leaf);
|
|
869
|
+
z.n = this.order - 1;
|
|
870
|
+
|
|
871
|
+
for (let j = 0; j < this.order - 1; j++) {
|
|
872
|
+
z.keys[j] = y.keys[j + this.order];
|
|
873
|
+
z.values[j] = y.values[j + this.order];
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (!y.leaf) {
|
|
877
|
+
for (let j = 0; j < this.order; j++) {
|
|
878
|
+
z.children[j] = y.children[j + this.order];
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
y.n = this.order - 1;
|
|
883
|
+
|
|
884
|
+
for (let j = this.n; j >= i + 1; j--) {
|
|
885
|
+
this.children[j + 1] = this.children[j];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
this.children[i + 1] = z;
|
|
889
|
+
|
|
890
|
+
for (let j = this.n - 1; j >= i; j--) {
|
|
891
|
+
this.keys[j + 1] = this.keys[j];
|
|
892
|
+
this.values[j + 1] = this.values[j];
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
this.keys[i] = y.keys[this.order - 1];
|
|
896
|
+
this.values[i] = y.values[this.order - 1];
|
|
897
|
+
this.n++;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
remove(key, value) {
|
|
901
|
+
let i = 0;
|
|
902
|
+
while (i < this.n && key > this.keys[i]) {
|
|
903
|
+
i++;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (i < this.n && key === this.keys[i]) {
|
|
907
|
+
this.values[i].delete(value);
|
|
908
|
+
if (this.values[i].size === 0) {
|
|
909
|
+
for (let j = i; j < this.n - 1; j++) {
|
|
910
|
+
this.keys[j] = this.keys[j + 1];
|
|
911
|
+
this.values[j] = this.values[j + 1];
|
|
912
|
+
}
|
|
913
|
+
this.n--;
|
|
914
|
+
}
|
|
915
|
+
} else if (!this.leaf) {
|
|
916
|
+
this.children[i].remove(key, value);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
class BTreeIndex {
|
|
922
|
+
constructor(order = 4) {
|
|
923
|
+
this.root = null;
|
|
924
|
+
this.order = order;
|
|
925
|
+
this.size = 0;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
insert(key, value) {
|
|
929
|
+
if (!this.root) {
|
|
930
|
+
this.root = new BTreeNode(this.order, true);
|
|
931
|
+
this.root.keys[0] = key;
|
|
932
|
+
this.root.values[0] = new Set([value]);
|
|
933
|
+
this.root.n = 1;
|
|
934
|
+
} else {
|
|
935
|
+
if (this.root.n === 2 * this.order - 1) {
|
|
936
|
+
const s = new BTreeNode(this.order, false);
|
|
937
|
+
s.children[0] = this.root;
|
|
938
|
+
s.splitChild(0, this.root);
|
|
939
|
+
|
|
940
|
+
let i = 0;
|
|
941
|
+
if (s.keys[0] < key) i++;
|
|
942
|
+
s.children[i].insertNonFull(key, value);
|
|
943
|
+
|
|
944
|
+
this.root = s;
|
|
945
|
+
} else {
|
|
946
|
+
this.root.insertNonFull(key, value);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
this.size++;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
find(key) {
|
|
953
|
+
if (!this.root) return [];
|
|
954
|
+
const values = this.root.search(key);
|
|
955
|
+
return values ? Array.from(values) : [];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
contains(key) {
|
|
959
|
+
if (!this.root) return false;
|
|
960
|
+
return this.root.search(key) !== null;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
range(min, max) {
|
|
964
|
+
if (!this.root) return [];
|
|
965
|
+
const results = [];
|
|
966
|
+
this.root.rangeSearch(min, max, results);
|
|
967
|
+
return results;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
rangeFrom(min) {
|
|
971
|
+
if (!this.root) return [];
|
|
972
|
+
const results = [];
|
|
973
|
+
this.root.rangeSearch(min, Infinity, results);
|
|
974
|
+
return results;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
rangeTo(max) {
|
|
978
|
+
if (!this.root) return [];
|
|
979
|
+
const results = [];
|
|
980
|
+
this.root.rangeSearch(-Infinity, max, results);
|
|
981
|
+
return results;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
remove(key, value) {
|
|
985
|
+
if (!this.root) return;
|
|
986
|
+
this.root.remove(key, value);
|
|
987
|
+
if (this.root.n === 0) {
|
|
988
|
+
if (!this.root.leaf && this.root.children[0]) {
|
|
989
|
+
this.root = this.root.children[0];
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
this.size--;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Text Index for full-text search
|
|
997
|
+
class TextIndex {
|
|
998
|
+
constructor() {
|
|
999
|
+
this.invertedIndex = new Map();
|
|
1000
|
+
this.documentTexts = new Map();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
addDocument(text, docId) {
|
|
1004
|
+
if (typeof text !== 'string') return;
|
|
1005
|
+
|
|
1006
|
+
this.documentTexts.set(docId, text);
|
|
1007
|
+
const tokens = this.tokenize(text);
|
|
1008
|
+
|
|
1009
|
+
for (const token of tokens) {
|
|
1010
|
+
if (!this.invertedIndex.has(token)) {
|
|
1011
|
+
this.invertedIndex.set(token, new Set());
|
|
1012
|
+
}
|
|
1013
|
+
this.invertedIndex.get(token).add(docId);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
removeDocument(docId) {
|
|
1018
|
+
const text = this.documentTexts.get(docId);
|
|
1019
|
+
if (!text) return;
|
|
1020
|
+
|
|
1021
|
+
const tokens = this.tokenize(text);
|
|
1022
|
+
for (const token of tokens) {
|
|
1023
|
+
const docs = this.invertedIndex.get(token);
|
|
1024
|
+
if (docs) {
|
|
1025
|
+
docs.delete(docId);
|
|
1026
|
+
if (docs.size === 0) {
|
|
1027
|
+
this.invertedIndex.delete(token);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
this.documentTexts.delete(docId);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
search(query) {
|
|
1036
|
+
const tokens = this.tokenize(query);
|
|
1037
|
+
if (tokens.length === 0) return [];
|
|
1038
|
+
|
|
1039
|
+
let results = null;
|
|
1040
|
+
for (const token of tokens) {
|
|
1041
|
+
const docs = this.invertedIndex.get(token);
|
|
1042
|
+
if (!docs || docs.size === 0) {
|
|
1043
|
+
return [];
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (results === null) {
|
|
1047
|
+
results = new Set(docs);
|
|
1048
|
+
} else {
|
|
1049
|
+
results = new Set([...results].filter(x => docs.has(x)));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return results ? Array.from(results) : [];
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
tokenize(text) {
|
|
1057
|
+
return text.toLowerCase()
|
|
1058
|
+
.replace(/[^\w\s]/g, ' ')
|
|
1059
|
+
.split(/\s+/)
|
|
1060
|
+
.filter(token => token.length > 2);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
get size() {
|
|
1064
|
+
return this.documentTexts.size;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Geo Index for spatial queries
|
|
1069
|
+
class GeoIndex {
|
|
1070
|
+
constructor() {
|
|
1071
|
+
this.points = new Map();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
addPoint(coords, docId) {
|
|
1075
|
+
if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
this.points.set(docId, coords);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
removePoint(docId) {
|
|
1082
|
+
this.points.delete(docId);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
findNear(center, maxDistance) {
|
|
1086
|
+
const results = [];
|
|
1087
|
+
|
|
1088
|
+
for (const [docId, coords] of this.points) {
|
|
1089
|
+
const distance = this.haversine(center, coords);
|
|
1090
|
+
if (distance <= maxDistance) {
|
|
1091
|
+
results.push({ docId, distance });
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return results.sort((a, b) => a.distance - b.distance)
|
|
1096
|
+
.map(r => r.docId);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
findWithin(bounds) {
|
|
1100
|
+
const results = [];
|
|
1101
|
+
|
|
1102
|
+
for (const [docId, coords] of this.points) {
|
|
1103
|
+
if (this.isWithinBounds(coords, bounds)) {
|
|
1104
|
+
results.push(docId);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return results;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
haversine(coord1, coord2) {
|
|
1112
|
+
const R = 6371;
|
|
1113
|
+
const dLat = this.toRad(coord2.lat - coord1.lat);
|
|
1114
|
+
const dLng = this.toRad(coord2.lng - coord1.lng);
|
|
1115
|
+
|
|
1116
|
+
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
|
1117
|
+
Math.cos(this.toRad(coord1.lat)) * Math.cos(this.toRad(coord2.lat)) *
|
|
1118
|
+
Math.sin(dLng/2) * Math.sin(dLng/2);
|
|
1119
|
+
|
|
1120
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
1121
|
+
return R * c;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
toRad(deg) {
|
|
1125
|
+
return deg * (Math.PI / 180);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
isWithinBounds(coords, bounds) {
|
|
1129
|
+
return coords.lat >= bounds.minLat && coords.lat <= bounds.maxLat &&
|
|
1130
|
+
coords.lng >= bounds.minLng && coords.lng <= bounds.maxLng;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
get size() {
|
|
1134
|
+
return this.points.size;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ========================
|
|
1139
|
+
// Index Manager
|
|
1140
|
+
// ========================
|
|
1141
|
+
|
|
1142
|
+
class IndexManager {
|
|
1143
|
+
constructor(collection) {
|
|
1144
|
+
this.collection = collection;
|
|
1145
|
+
this.indexes = new Map();
|
|
1146
|
+
this.indexData = new Map();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
async createIndex(fieldPath, options = {}) {
|
|
1150
|
+
const indexName = options.name || fieldPath;
|
|
1151
|
+
|
|
1152
|
+
if (this.indexes.has(indexName)) {
|
|
1153
|
+
throw new Error(`Index '${indexName}' already exists`);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const index = {
|
|
1157
|
+
fieldPath,
|
|
1158
|
+
unique: options.unique || false,
|
|
1159
|
+
sparse: options.sparse || false,
|
|
1160
|
+
type: options.type || 'btree',
|
|
1161
|
+
collation: options.collation || null,
|
|
1162
|
+
createdAt: Date.now()
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
this.indexes.set(indexName, index);
|
|
1166
|
+
|
|
1167
|
+
await this.rebuildIndex(indexName);
|
|
1168
|
+
|
|
1169
|
+
this.saveIndexMetadata();
|
|
1170
|
+
|
|
1171
|
+
return indexName;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async rebuildIndex(indexName) {
|
|
1175
|
+
const index = this.indexes.get(indexName);
|
|
1176
|
+
if (!index) {
|
|
1177
|
+
throw new Error(`Index '${indexName}' not found`);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const indexData = this.createIndexStructure(index.type);
|
|
1181
|
+
|
|
1182
|
+
const allDocs = await this.collection.getAll();
|
|
1183
|
+
|
|
1184
|
+
for (const doc of allDocs) {
|
|
1185
|
+
const value = this.getFieldValue(doc, index.fieldPath);
|
|
1186
|
+
|
|
1187
|
+
if (index.sparse && (value === null || value === undefined)) {
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (index.unique && indexData.has && indexData.has(value)) {
|
|
1192
|
+
throw new Error(`Unique constraint violation on index '${indexName}'`);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
this.addToIndex(indexData, value, doc._id, index.type);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
this.indexData.set(indexName, indexData);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
createIndexStructure(type) {
|
|
1202
|
+
switch (type) {
|
|
1203
|
+
case 'btree':
|
|
1204
|
+
return new BTreeIndex();
|
|
1205
|
+
case 'hash':
|
|
1206
|
+
return new Map();
|
|
1207
|
+
case 'text':
|
|
1208
|
+
return new TextIndex();
|
|
1209
|
+
case 'geo':
|
|
1210
|
+
return new GeoIndex();
|
|
1211
|
+
default:
|
|
1212
|
+
return new Map();
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
addToIndex(indexData, value, docId, type) {
|
|
1217
|
+
switch (type) {
|
|
1218
|
+
case 'btree':
|
|
1219
|
+
indexData.insert(value, docId);
|
|
1220
|
+
break;
|
|
1221
|
+
case 'hash':
|
|
1222
|
+
if (!indexData.has(value)) {
|
|
1223
|
+
indexData.set(value, new Set());
|
|
1224
|
+
}
|
|
1225
|
+
indexData.get(value).add(docId);
|
|
1226
|
+
break;
|
|
1227
|
+
case 'text':
|
|
1228
|
+
indexData.addDocument(value, docId);
|
|
1229
|
+
break;
|
|
1230
|
+
case 'geo':
|
|
1231
|
+
indexData.addPoint(value, docId);
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async updateIndex(indexName, docId, oldValue, newValue) {
|
|
1237
|
+
const index = this.indexes.get(indexName);
|
|
1238
|
+
const indexData = this.indexData.get(indexName);
|
|
1239
|
+
|
|
1240
|
+
if (!index || !indexData) return;
|
|
1241
|
+
|
|
1242
|
+
if (oldValue !== undefined && oldValue !== null) {
|
|
1243
|
+
this.removeFromIndex(indexData, oldValue, docId, index.type);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (newValue !== undefined && newValue !== null) {
|
|
1247
|
+
if (index.unique && this.indexContains(indexData, newValue, index.type)) {
|
|
1248
|
+
throw new Error(`Unique constraint violation on index '${indexName}'`);
|
|
1249
|
+
}
|
|
1250
|
+
this.addToIndex(indexData, newValue, docId, index.type);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
removeFromIndex(indexData, value, docId, type) {
|
|
1255
|
+
switch (type) {
|
|
1256
|
+
case 'btree':
|
|
1257
|
+
indexData.remove(value, docId);
|
|
1258
|
+
break;
|
|
1259
|
+
case 'hash':
|
|
1260
|
+
const docs = indexData.get(value);
|
|
1261
|
+
if (docs) {
|
|
1262
|
+
docs.delete(docId);
|
|
1263
|
+
if (docs.size === 0) {
|
|
1264
|
+
indexData.delete(value);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
break;
|
|
1268
|
+
case 'text':
|
|
1269
|
+
indexData.removeDocument(docId);
|
|
1270
|
+
break;
|
|
1271
|
+
case 'geo':
|
|
1272
|
+
indexData.removePoint(docId);
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
indexContains(indexData, value, type) {
|
|
1278
|
+
switch (type) {
|
|
1279
|
+
case 'btree':
|
|
1280
|
+
return indexData.contains(value);
|
|
1281
|
+
case 'hash':
|
|
1282
|
+
return indexData.has(value);
|
|
1283
|
+
case 'text':
|
|
1284
|
+
return false;
|
|
1285
|
+
case 'geo':
|
|
1286
|
+
return false;
|
|
1287
|
+
default:
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async query(indexName, queryOptions) {
|
|
1293
|
+
const index = this.indexes.get(indexName);
|
|
1294
|
+
const indexData = this.indexData.get(indexName);
|
|
1295
|
+
|
|
1296
|
+
if (!index || !indexData) {
|
|
1297
|
+
throw new Error(`Index '${indexName}' not found`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
return this.queryIndex(indexData, queryOptions, index.type);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
queryIndex(indexData, options, type) {
|
|
1304
|
+
switch (type) {
|
|
1305
|
+
case 'btree':
|
|
1306
|
+
return this.queryBTree(indexData, options);
|
|
1307
|
+
case 'hash':
|
|
1308
|
+
return this.queryHash(indexData, options);
|
|
1309
|
+
case 'text':
|
|
1310
|
+
return this.queryText(indexData, options);
|
|
1311
|
+
case 'geo':
|
|
1312
|
+
return this.queryGeo(indexData, options);
|
|
1313
|
+
default:
|
|
1314
|
+
return [];
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
queryBTree(indexData, options) {
|
|
1319
|
+
const results = new Set();
|
|
1320
|
+
|
|
1321
|
+
if (options.$eq !== undefined) {
|
|
1322
|
+
const docs = indexData.find(options.$eq);
|
|
1323
|
+
docs.forEach(doc => results.add(doc));
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (options.$gte !== undefined && options.$lte !== undefined) {
|
|
1327
|
+
const docs = indexData.range(options.$gte, options.$lte);
|
|
1328
|
+
docs.forEach(doc => results.add(doc));
|
|
1329
|
+
} else if (options.$gte !== undefined) {
|
|
1330
|
+
const docs = indexData.rangeFrom(options.$gte);
|
|
1331
|
+
docs.forEach(doc => results.add(doc));
|
|
1332
|
+
} else if (options.$lte !== undefined) {
|
|
1333
|
+
const docs = indexData.rangeTo(options.$lte);
|
|
1334
|
+
docs.forEach(doc => results.add(doc));
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
return Array.from(results);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
queryHash(indexData, options) {
|
|
1341
|
+
if (options.$eq !== undefined) {
|
|
1342
|
+
const docs = indexData.get(options.$eq);
|
|
1343
|
+
return docs ? Array.from(docs) : [];
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (options.$in !== undefined) {
|
|
1347
|
+
const results = new Set();
|
|
1348
|
+
for (const value of options.$in) {
|
|
1349
|
+
const docs = indexData.get(value);
|
|
1350
|
+
if (docs) {
|
|
1351
|
+
docs.forEach(doc => results.add(doc));
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return Array.from(results);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return [];
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
queryText(indexData, options) {
|
|
1361
|
+
if (options.$search) {
|
|
1362
|
+
return indexData.search(options.$search);
|
|
1363
|
+
}
|
|
1364
|
+
return [];
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
queryGeo(indexData, options) {
|
|
1368
|
+
if (options.$near) {
|
|
1369
|
+
return indexData.findNear(
|
|
1370
|
+
options.$near.coordinates,
|
|
1371
|
+
options.$near.maxDistance || 1000
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
if (options.$within) {
|
|
1375
|
+
return indexData.findWithin(options.$within);
|
|
1376
|
+
}
|
|
1377
|
+
return [];
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
dropIndex(indexName) {
|
|
1381
|
+
this.indexes.delete(indexName);
|
|
1382
|
+
this.indexData.delete(indexName);
|
|
1383
|
+
this.saveIndexMetadata();
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
getFieldValue(doc, path) {
|
|
1387
|
+
const parts = path.split('.');
|
|
1388
|
+
let value = doc;
|
|
1389
|
+
for (const part of parts) {
|
|
1390
|
+
if (value === null || value === undefined) {
|
|
1391
|
+
return undefined;
|
|
1392
|
+
}
|
|
1393
|
+
value = value[part];
|
|
1394
|
+
}
|
|
1395
|
+
return value;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
saveIndexMetadata() {
|
|
1399
|
+
const metadata = {
|
|
1400
|
+
indexes: Array.from(this.indexes.entries()).map(([name, index]) => ({
|
|
1401
|
+
name,
|
|
1402
|
+
...index
|
|
1403
|
+
}))
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
const key = `lacertadb_${this.collection.database.name}_${this.collection.name}_indexes`;
|
|
1407
|
+
const serialized = serializer.serialize(metadata);
|
|
1408
|
+
const encoded = base64.encode(serialized);
|
|
1409
|
+
localStorage.setItem(key, encoded);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
async loadIndexMetadata() {
|
|
1413
|
+
const key = `lacertadb_${this.collection.database.name}_${this.collection.name}_indexes`;
|
|
1414
|
+
const stored = localStorage.getItem(key);
|
|
1415
|
+
|
|
1416
|
+
if (!stored) return;
|
|
1417
|
+
|
|
1418
|
+
try {
|
|
1419
|
+
const decoded = base64.decode(stored);
|
|
1420
|
+
const metadata = serializer.deserialize(decoded);
|
|
1421
|
+
|
|
1422
|
+
for (const indexDef of metadata.indexes) {
|
|
1423
|
+
const { name, ...index } = indexDef;
|
|
1424
|
+
this.indexes.set(name, index);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
for (const indexName of this.indexes.keys()) {
|
|
1428
|
+
await this.rebuildIndex(indexName);
|
|
1429
|
+
}
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
console.error('Failed to load index metadata:', error);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
getIndexStats() {
|
|
1436
|
+
const stats = {};
|
|
1437
|
+
for (const [name, index] of this.indexes) {
|
|
1438
|
+
const indexData = this.indexData.get(name);
|
|
1439
|
+
stats[name] = {
|
|
1440
|
+
...index,
|
|
1441
|
+
size: indexData ? indexData.size || indexData.length || 0 : 0,
|
|
1442
|
+
memoryUsage: this.estimateMemoryUsage(indexData)
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
return stats;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
estimateMemoryUsage(indexData) {
|
|
1449
|
+
if (!indexData) return 0;
|
|
1450
|
+
|
|
1451
|
+
if (indexData instanceof Map) {
|
|
1452
|
+
return indexData.size * 100;
|
|
1453
|
+
}
|
|
1454
|
+
if (indexData instanceof BTreeIndex) {
|
|
1455
|
+
return indexData.size * 120;
|
|
1456
|
+
}
|
|
1457
|
+
return 0;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
228
1461
|
// ========================
|
|
229
1462
|
// OPFS (Origin Private File System) Utility
|
|
230
1463
|
// ========================
|
|
@@ -298,7 +1531,6 @@ class OPFSUtility {
|
|
|
298
1531
|
});
|
|
299
1532
|
} catch (error) {
|
|
300
1533
|
console.error(`Failed to get attachment: ${attachmentInfo.path}`, error);
|
|
301
|
-
// Optionally, collect errors and return them
|
|
302
1534
|
}
|
|
303
1535
|
}
|
|
304
1536
|
return attachments;
|
|
@@ -311,7 +1543,6 @@ class OPFSUtility {
|
|
|
311
1543
|
const collDir = await dbDir.getDirectoryHandle(collectionName);
|
|
312
1544
|
await collDir.removeEntry(documentId, { recursive: true });
|
|
313
1545
|
} catch (error) {
|
|
314
|
-
// Ignore "NotFoundError" as the directory might already be gone
|
|
315
1546
|
if (error.name !== 'NotFoundError') {
|
|
316
1547
|
console.error(`Failed to delete attachments for ${documentId}:`, error);
|
|
317
1548
|
}
|
|
@@ -498,7 +1729,6 @@ class Document {
|
|
|
498
1729
|
this.data = data.data || {};
|
|
499
1730
|
this.packedData = data.packedData || null;
|
|
500
1731
|
|
|
501
|
-
// Utilities can be passed in or instantiated. For simplicity, we keep instantiation here.
|
|
502
1732
|
this.compression = new BrowserCompressionUtility();
|
|
503
1733
|
this.encryption = new BrowserEncryptionUtility();
|
|
504
1734
|
this.password = options.password || null;
|
|
@@ -743,7 +1973,6 @@ class DatabaseMetadata {
|
|
|
743
1973
|
class Settings {
|
|
744
1974
|
constructor(dbName, data = {}) {
|
|
745
1975
|
this.dbName = dbName;
|
|
746
|
-
// Replaced `??` with ternary operator for compatibility
|
|
747
1976
|
this.sizeLimitKB = data.sizeLimitKB != null ? data.sizeLimitKB : Infinity;
|
|
748
1977
|
const defaultBuffer = this.sizeLimitKB === Infinity ? 0 : this.sizeLimitKB * 0.8;
|
|
749
1978
|
this.bufferLimitKB = data.bufferLimitKB != null ? data.bufferLimitKB : defaultBuffer;
|
|
@@ -886,7 +2115,6 @@ class QuickStore {
|
|
|
886
2115
|
class QueryEngine {
|
|
887
2116
|
constructor() {
|
|
888
2117
|
this.operators = {
|
|
889
|
-
// Comparison
|
|
890
2118
|
'$eq': (a, b) => a === b,
|
|
891
2119
|
'$ne': (a, b) => a !== b,
|
|
892
2120
|
'$gt': (a, b) => a > b,
|
|
@@ -896,22 +2124,18 @@ class QueryEngine {
|
|
|
896
2124
|
'$in': (a, b) => Array.isArray(b) && b.includes(a),
|
|
897
2125
|
'$nin': (a, b) => Array.isArray(b) && !b.includes(a),
|
|
898
2126
|
|
|
899
|
-
// Logical
|
|
900
2127
|
'$and': (doc, conditions) => conditions.every(cond => this.evaluate(doc, cond)),
|
|
901
2128
|
'$or': (doc, conditions) => conditions.some(cond => this.evaluate(doc, cond)),
|
|
902
2129
|
'$not': (doc, condition) => !this.evaluate(doc, condition),
|
|
903
2130
|
'$nor': (doc, conditions) => !conditions.some(cond => this.evaluate(doc, cond)),
|
|
904
2131
|
|
|
905
|
-
// Element
|
|
906
2132
|
'$exists': (value, exists) => (value !== undefined) === exists,
|
|
907
2133
|
'$type': (value, type) => typeof value === type,
|
|
908
2134
|
|
|
909
|
-
// Array
|
|
910
2135
|
'$all': (arr, values) => Array.isArray(arr) && values.every(v => arr.includes(v)),
|
|
911
2136
|
'$elemMatch': (arr, condition) => Array.isArray(arr) && arr.some(elem => this.evaluate({ value: elem }, { value: condition })),
|
|
912
2137
|
'$size': (arr, size) => Array.isArray(arr) && arr.length === size,
|
|
913
2138
|
|
|
914
|
-
// String
|
|
915
2139
|
'$regex': (str, pattern) => {
|
|
916
2140
|
if (typeof str !== 'string') return false;
|
|
917
2141
|
try {
|
|
@@ -929,14 +2153,11 @@ class QueryEngine {
|
|
|
929
2153
|
for (const key in query) {
|
|
930
2154
|
const value = query[key];
|
|
931
2155
|
if (key.startsWith('$')) {
|
|
932
|
-
// Logical operator at root level
|
|
933
2156
|
const operator = this.operators[key];
|
|
934
2157
|
if (!operator || !operator(doc, value)) return false;
|
|
935
2158
|
} else {
|
|
936
|
-
// Field-level query
|
|
937
2159
|
const fieldValue = this.getFieldValue(doc, key);
|
|
938
2160
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
939
|
-
// Operator-based comparison
|
|
940
2161
|
for (const op in value) {
|
|
941
2162
|
if (op.startsWith('$')) {
|
|
942
2163
|
const operatorFn = this.operators[op];
|
|
@@ -946,7 +2167,6 @@ class QueryEngine {
|
|
|
946
2167
|
}
|
|
947
2168
|
}
|
|
948
2169
|
} else {
|
|
949
|
-
// Direct equality comparison
|
|
950
2170
|
if (fieldValue !== value) return false;
|
|
951
2171
|
}
|
|
952
2172
|
}
|
|
@@ -955,7 +2175,6 @@ class QueryEngine {
|
|
|
955
2175
|
}
|
|
956
2176
|
|
|
957
2177
|
getFieldValue(doc, path) {
|
|
958
|
-
// Replaced optional chaining with a loop for compatibility
|
|
959
2178
|
let current = doc;
|
|
960
2179
|
for (const part of path.split('.')) {
|
|
961
2180
|
if (current === null || current === undefined) {
|
|
@@ -968,7 +2187,6 @@ class QueryEngine {
|
|
|
968
2187
|
}
|
|
969
2188
|
const queryEngine = new QueryEngine();
|
|
970
2189
|
|
|
971
|
-
|
|
972
2190
|
// ========================
|
|
973
2191
|
// Aggregation Pipeline
|
|
974
2192
|
// ========================
|
|
@@ -984,13 +2202,10 @@ class AggregationPipeline {
|
|
|
984
2202
|
const value = projection[key];
|
|
985
2203
|
if (value === 1 || value === true) {
|
|
986
2204
|
projected[key] = queryEngine.getFieldValue(doc, key);
|
|
987
|
-
} else if (typeof value === 'object') {
|
|
988
|
-
// Handle computed fields if necessary
|
|
989
2205
|
} else if (typeof value === 'string' && value.startsWith('$')) {
|
|
990
2206
|
projected[key] = queryEngine.getFieldValue(doc, value.substring(1));
|
|
991
2207
|
}
|
|
992
2208
|
}
|
|
993
|
-
// Handle exclusion projection
|
|
994
2209
|
if (Object.values(projection).some(v => v === 0 || v === false)) {
|
|
995
2210
|
const exclusions = Object.keys(projection).filter(k => projection[k] === 0 || projection[k] === false);
|
|
996
2211
|
const included = { ...doc };
|
|
@@ -1002,7 +2217,7 @@ class AggregationPipeline {
|
|
|
1002
2217
|
|
|
1003
2218
|
'$sort': (docs, sortSpec) => [...docs].sort((a, b) => {
|
|
1004
2219
|
for (const key in sortSpec) {
|
|
1005
|
-
const order = sortSpec[key];
|
|
2220
|
+
const order = sortSpec[key];
|
|
1006
2221
|
const aVal = queryEngine.getFieldValue(a, key);
|
|
1007
2222
|
const bVal = queryEngine.getFieldValue(b, key);
|
|
1008
2223
|
if (aVal < bVal) return -order;
|
|
@@ -1022,7 +2237,7 @@ class AggregationPipeline {
|
|
|
1022
2237
|
for (const doc of docs) {
|
|
1023
2238
|
const groupKey = typeof idField === 'string' ?
|
|
1024
2239
|
queryEngine.getFieldValue(doc, idField.replace('$', '')) :
|
|
1025
|
-
JSON.stringify(idField);
|
|
2240
|
+
JSON.stringify(idField);
|
|
1026
2241
|
|
|
1027
2242
|
if (!groups.has(groupKey)) {
|
|
1028
2243
|
groups.set(groupKey, { _id: groupKey, docs: [] });
|
|
@@ -1227,7 +2442,6 @@ class PerformanceMonitor {
|
|
|
1227
2442
|
recordCacheMiss() { this.metrics.cacheMisses++; }
|
|
1228
2443
|
|
|
1229
2444
|
collectMetrics() {
|
|
1230
|
-
// Replaced optional chaining with `&&` for compatibility
|
|
1231
2445
|
if (performance && performance.memory) {
|
|
1232
2446
|
this.metrics.memoryUsage.push({
|
|
1233
2447
|
used: performance.memory.usedJSHeapSize,
|
|
@@ -1246,7 +2460,6 @@ class PerformanceMonitor {
|
|
|
1246
2460
|
const totalCacheOps = this.metrics.cacheHits + this.metrics.cacheMisses;
|
|
1247
2461
|
const cacheHitRate = totalCacheOps > 0 ? (this.metrics.cacheHits / totalCacheOps) * 100 : 0;
|
|
1248
2462
|
|
|
1249
|
-
// Replaced `.at(-1)` with classic index access for compatibility
|
|
1250
2463
|
const latestMemory = this.metrics.memoryUsage.length > 0 ? this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1] : null;
|
|
1251
2464
|
const memoryUsageMB = latestMemory ? latestMemory.used / (1024 * 1024) : 0;
|
|
1252
2465
|
|
|
@@ -1271,7 +2484,7 @@ class PerformanceMonitor {
|
|
|
1271
2484
|
if (this.metrics.memoryUsage.length > 10) {
|
|
1272
2485
|
const recent = this.metrics.memoryUsage.slice(-10);
|
|
1273
2486
|
const trend = recent[recent.length - 1].used - recent[0].used;
|
|
1274
|
-
if (trend > 10 * 1024 * 1024) {
|
|
2487
|
+
if (trend > 10 * 1024 * 1024) {
|
|
1275
2488
|
tips.push('Memory usage is increasing rapidly. Check for memory leaks or consider batch processing.');
|
|
1276
2489
|
}
|
|
1277
2490
|
}
|
|
@@ -1294,8 +2507,16 @@ class Collection {
|
|
|
1294
2507
|
this.opfs = new OPFSUtility();
|
|
1295
2508
|
this.cleanupInterval = null;
|
|
1296
2509
|
this.events = new Map();
|
|
1297
|
-
|
|
1298
|
-
|
|
2510
|
+
|
|
2511
|
+
// Enhanced features
|
|
2512
|
+
this.indexManager = new IndexManager(this);
|
|
2513
|
+
this.cacheStrategy = new CacheStrategy({
|
|
2514
|
+
type: 'lru',
|
|
2515
|
+
maxSize: 100,
|
|
2516
|
+
ttl: 60000,
|
|
2517
|
+
enabled: true
|
|
2518
|
+
});
|
|
2519
|
+
|
|
1299
2520
|
this.performanceMonitor = database.performanceMonitor;
|
|
1300
2521
|
}
|
|
1301
2522
|
|
|
@@ -1306,18 +2527,38 @@ class Collection {
|
|
|
1306
2527
|
const store = db.createObjectStore('documents', { keyPath: '_id' });
|
|
1307
2528
|
store.createIndex('modified', '_modified', { unique: false });
|
|
1308
2529
|
}
|
|
1309
|
-
// Future index creation logic would go here during version bumps
|
|
1310
2530
|
});
|
|
1311
2531
|
|
|
1312
2532
|
const metadataData = this.database.metadata.collections[this.name];
|
|
1313
2533
|
this.metadata = new CollectionMetadata(this.name, metadataData);
|
|
1314
2534
|
|
|
2535
|
+
// Load indexes
|
|
2536
|
+
await this.indexManager.loadIndexMetadata();
|
|
2537
|
+
|
|
1315
2538
|
if (this.settings.freeSpaceEvery > 0) {
|
|
1316
2539
|
this.cleanupInterval = setInterval(() => this.freeSpace(), this.settings.freeSpaceEvery);
|
|
1317
2540
|
}
|
|
1318
2541
|
return this;
|
|
1319
2542
|
}
|
|
1320
2543
|
|
|
2544
|
+
// Index methods
|
|
2545
|
+
async createIndex(fieldPath, options = {}) {
|
|
2546
|
+
return await this.indexManager.createIndex(fieldPath, options);
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
async dropIndex(indexName) {
|
|
2550
|
+
return this.indexManager.dropIndex(indexName);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
async getIndexes() {
|
|
2554
|
+
return this.indexManager.getIndexStats();
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// Cache configuration
|
|
2558
|
+
configureCacheStrategy(config) {
|
|
2559
|
+
this.cacheStrategy.updateStrategy(config);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
1321
2562
|
async add(documentData, options = {}) {
|
|
1322
2563
|
await this.trigger('beforeAdd', documentData);
|
|
1323
2564
|
|
|
@@ -1342,13 +2583,22 @@ class Collection {
|
|
|
1342
2583
|
const dbOutput = doc.databaseOutput();
|
|
1343
2584
|
await this.indexedDB.add(this.db, 'documents', dbOutput);
|
|
1344
2585
|
|
|
2586
|
+
// Update indexes
|
|
2587
|
+
const fullDoc = doc.objectOutput();
|
|
2588
|
+
for (const [indexName, index] of this.indexManager.indexes) {
|
|
2589
|
+
const value = this.indexManager.getFieldValue(fullDoc, index.fieldPath);
|
|
2590
|
+
if (value !== undefined && value !== null) {
|
|
2591
|
+
await this.indexManager.updateIndex(indexName, doc._id, undefined, value);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
|
|
1345
2595
|
const sizeKB = dbOutput.packedData.byteLength / 1024;
|
|
1346
2596
|
this.metadata.addDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
|
|
1347
2597
|
this.database.metadata.setCollection(this.metadata);
|
|
1348
2598
|
|
|
1349
2599
|
await this.checkSpaceLimit();
|
|
1350
2600
|
await this.trigger('afterAdd', doc);
|
|
1351
|
-
this.
|
|
2601
|
+
this.cacheStrategy.clear();
|
|
1352
2602
|
return doc._id;
|
|
1353
2603
|
}
|
|
1354
2604
|
|
|
@@ -1409,9 +2659,9 @@ class Collection {
|
|
|
1409
2659
|
const existingDoc = new Document(stored, { password: options.password });
|
|
1410
2660
|
if (stored.packedData) await existingDoc.unpack();
|
|
1411
2661
|
|
|
2662
|
+
const oldDocOutput = existingDoc.objectOutput();
|
|
1412
2663
|
const updatedData = { ...existingDoc.data, ...updates };
|
|
1413
2664
|
|
|
1414
|
-
// Replaced `??` with ternary operator for compatibility
|
|
1415
2665
|
const doc = new Document({
|
|
1416
2666
|
_id: docId,
|
|
1417
2667
|
_created: stored._created,
|
|
@@ -1441,21 +2691,55 @@ class Collection {
|
|
|
1441
2691
|
const dbOutput = doc.databaseOutput();
|
|
1442
2692
|
await this.indexedDB.put(this.db, 'documents', dbOutput);
|
|
1443
2693
|
|
|
2694
|
+
// Update indexes
|
|
2695
|
+
const newDocOutput = doc.objectOutput();
|
|
2696
|
+
for (const [indexName, index] of this.indexManager.indexes) {
|
|
2697
|
+
const oldValue = this.indexManager.getFieldValue(oldDocOutput, index.fieldPath);
|
|
2698
|
+
const newValue = this.indexManager.getFieldValue(newDocOutput, index.fieldPath);
|
|
2699
|
+
|
|
2700
|
+
if (oldValue !== newValue) {
|
|
2701
|
+
await this.indexManager.updateIndex(indexName, doc._id, oldValue, newValue);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
1444
2705
|
const sizeKB = dbOutput.packedData.byteLength / 1024;
|
|
1445
2706
|
this.metadata.updateDocument(doc._id, sizeKB, doc._permanent, doc._attachments.length);
|
|
1446
2707
|
this.database.metadata.setCollection(this.metadata);
|
|
1447
2708
|
|
|
1448
2709
|
await this.trigger('afterUpdate', doc);
|
|
1449
|
-
this.
|
|
2710
|
+
this.cacheStrategy.clear();
|
|
1450
2711
|
return doc._id;
|
|
1451
2712
|
}
|
|
1452
2713
|
|
|
1453
|
-
async delete(docId) {
|
|
2714
|
+
async delete(docId, options = {}) {
|
|
1454
2715
|
await this.trigger('beforeDelete', docId);
|
|
1455
2716
|
|
|
1456
2717
|
const doc = await this.indexedDB.get(this.db, 'documents', docId);
|
|
1457
|
-
if (!doc)
|
|
1458
|
-
|
|
2718
|
+
if (!doc) {
|
|
2719
|
+
throw new LacertaDBError('Document not found for deletion', 'DOCUMENT_NOT_FOUND');
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
if (doc._permanent && !options.force) {
|
|
2723
|
+
throw new LacertaDBError(
|
|
2724
|
+
'Cannot delete a permanent document. Use options.force = true to force deletion.',
|
|
2725
|
+
'PERMANENT_DOCUMENT_PROTECTION'
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (doc._permanent && options.force) {
|
|
2730
|
+
console.warn(`Force deleting permanent document: ${docId}`);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// Get full document for index cleanup
|
|
2734
|
+
const fullDoc = await this.get(docId);
|
|
2735
|
+
|
|
2736
|
+
// Remove from indexes
|
|
2737
|
+
for (const [indexName, index] of this.indexManager.indexes) {
|
|
2738
|
+
const value = this.indexManager.getFieldValue(fullDoc, index.fieldPath);
|
|
2739
|
+
if (value !== undefined && value !== null) {
|
|
2740
|
+
await this.indexManager.updateIndex(indexName, docId, value, undefined);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
1459
2743
|
|
|
1460
2744
|
await this.indexedDB.delete(this.db, 'documents', docId);
|
|
1461
2745
|
const attachments = doc._attachments;
|
|
@@ -1467,36 +2751,63 @@ class Collection {
|
|
|
1467
2751
|
this.database.metadata.setCollection(this.metadata);
|
|
1468
2752
|
|
|
1469
2753
|
await this.trigger('afterDelete', docId);
|
|
1470
|
-
this.
|
|
2754
|
+
this.cacheStrategy.clear();
|
|
1471
2755
|
}
|
|
1472
2756
|
|
|
1473
2757
|
async query(filter = {}, options = {}) {
|
|
1474
2758
|
const startTime = performance.now();
|
|
2759
|
+
|
|
2760
|
+
// Try cache first
|
|
1475
2761
|
const cacheKey = base64.encode(serializer.serialize({ filter, options }));
|
|
1476
|
-
const cached = this.
|
|
1477
|
-
|
|
1478
|
-
if (cached
|
|
2762
|
+
const cached = this.cacheStrategy.get(cacheKey);
|
|
2763
|
+
|
|
2764
|
+
if (cached) {
|
|
1479
2765
|
if (this.performanceMonitor) this.performanceMonitor.recordCacheHit();
|
|
1480
|
-
return cached
|
|
2766
|
+
return cached;
|
|
1481
2767
|
}
|
|
1482
2768
|
if (this.performanceMonitor) this.performanceMonitor.recordCacheMiss();
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2769
|
+
|
|
2770
|
+
// Check if we can use an index
|
|
2771
|
+
let results;
|
|
2772
|
+
let usedIndex = false;
|
|
2773
|
+
|
|
2774
|
+
for (const [indexName, index] of this.indexManager.indexes) {
|
|
2775
|
+
const fieldValue = filter[index.fieldPath];
|
|
2776
|
+
if (fieldValue !== undefined) {
|
|
2777
|
+
const docIds = await this.indexManager.query(indexName, fieldValue);
|
|
2778
|
+
results = await Promise.all(
|
|
2779
|
+
docIds.map(id => this.get(id).catch(() => null))
|
|
2780
|
+
);
|
|
2781
|
+
results = results.filter(Boolean);
|
|
2782
|
+
usedIndex = true;
|
|
2783
|
+
break;
|
|
2784
|
+
}
|
|
1487
2785
|
}
|
|
1488
|
-
|
|
2786
|
+
|
|
2787
|
+
// Fall back to full scan if no index available
|
|
2788
|
+
if (!usedIndex) {
|
|
2789
|
+
results = await this.getAll(options);
|
|
2790
|
+
if (Object.keys(filter).length > 0) {
|
|
2791
|
+
results = results.filter(doc => queryEngine.evaluate(doc, filter));
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// Apply sorting, pagination, projection
|
|
1489
2796
|
if (options.sort) results = aggregationPipeline.stages.$sort(results, options.sort);
|
|
1490
2797
|
if (options.skip) results = aggregationPipeline.stages.$skip(results, options.skip);
|
|
1491
2798
|
if (options.limit) results = aggregationPipeline.stages.$limit(results, options.limit);
|
|
1492
2799
|
if (options.projection) results = aggregationPipeline.stages.$project(results, options.projection);
|
|
1493
|
-
|
|
1494
|
-
if (this.performanceMonitor)
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
2800
|
+
|
|
2801
|
+
if (this.performanceMonitor) {
|
|
2802
|
+
this.performanceMonitor.recordOperation(
|
|
2803
|
+
usedIndex ? 'indexed-query' : 'full-scan-query',
|
|
2804
|
+
performance.now() - startTime
|
|
2805
|
+
);
|
|
1499
2806
|
}
|
|
2807
|
+
|
|
2808
|
+
// Store in cache
|
|
2809
|
+
this.cacheStrategy.set(cacheKey, results);
|
|
2810
|
+
|
|
1500
2811
|
return results;
|
|
1501
2812
|
}
|
|
1502
2813
|
|
|
@@ -1519,7 +2830,7 @@ class Collection {
|
|
|
1519
2830
|
return results;
|
|
1520
2831
|
}
|
|
1521
2832
|
|
|
1522
|
-
batchUpdate(updates, options) {
|
|
2833
|
+
async batchUpdate(updates, options) {
|
|
1523
2834
|
return Promise.all(updates.map(update =>
|
|
1524
2835
|
this.update(update.id, update.data, options)
|
|
1525
2836
|
.then(id => ({ success: true, id }))
|
|
@@ -1527,19 +2838,37 @@ class Collection {
|
|
|
1527
2838
|
));
|
|
1528
2839
|
}
|
|
1529
2840
|
|
|
1530
|
-
batchDelete(
|
|
1531
|
-
|
|
1532
|
-
|
|
2841
|
+
async batchDelete(items) {
|
|
2842
|
+
const normalizedItems = items.map(item => {
|
|
2843
|
+
if (typeof item === 'string') {
|
|
2844
|
+
return { id: item, options: {} };
|
|
2845
|
+
}
|
|
2846
|
+
return { id: item.id, options: item.options || {} };
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2849
|
+
return Promise.all(normalizedItems.map(({ id, options }) =>
|
|
2850
|
+
this.delete(id, options)
|
|
1533
2851
|
.then(() => ({ success: true, id }))
|
|
1534
2852
|
.catch(error => ({ success: false, id, error: error.message }))
|
|
1535
2853
|
));
|
|
1536
2854
|
}
|
|
1537
2855
|
|
|
1538
|
-
async clear() {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2856
|
+
async clear(options = {}) {
|
|
2857
|
+
if (options.force) {
|
|
2858
|
+
await this.indexedDB.clear(this.db, 'documents');
|
|
2859
|
+
this.metadata = new CollectionMetadata(this.name);
|
|
2860
|
+
this.database.metadata.setCollection(this.metadata);
|
|
2861
|
+
this.cacheStrategy.clear();
|
|
2862
|
+
|
|
2863
|
+
// Clear all indexes
|
|
2864
|
+
for (const indexName of this.indexManager.indexes.keys()) {
|
|
2865
|
+
await this.indexManager.rebuildIndex(indexName);
|
|
2866
|
+
}
|
|
2867
|
+
} else {
|
|
2868
|
+
const allDocs = await this.getAll();
|
|
2869
|
+
const nonPermanentDocs = allDocs.filter(doc => !doc._permanent);
|
|
2870
|
+
await this.batchDelete(nonPermanentDocs.map(doc => doc._id));
|
|
2871
|
+
}
|
|
1543
2872
|
}
|
|
1544
2873
|
|
|
1545
2874
|
async checkSpaceLimit() {
|
|
@@ -1575,7 +2904,9 @@ class Collection {
|
|
|
1575
2904
|
}
|
|
1576
2905
|
}
|
|
1577
2906
|
|
|
1578
|
-
clearCache() {
|
|
2907
|
+
clearCache() {
|
|
2908
|
+
this.cacheStrategy.clear();
|
|
2909
|
+
}
|
|
1579
2910
|
|
|
1580
2911
|
destroy() {
|
|
1581
2912
|
clearInterval(this.cleanupInterval);
|
|
@@ -1597,15 +2928,115 @@ class Database {
|
|
|
1597
2928
|
this.settings = null;
|
|
1598
2929
|
this.quickStore = null;
|
|
1599
2930
|
this.performanceMonitor = performanceMonitor;
|
|
2931
|
+
|
|
2932
|
+
// Database-level encryption
|
|
2933
|
+
this.encryption = null;
|
|
2934
|
+
this.isEncrypted = false;
|
|
1600
2935
|
}
|
|
1601
2936
|
|
|
1602
|
-
async init() {
|
|
2937
|
+
async init(options = {}) {
|
|
1603
2938
|
this.metadata = DatabaseMetadata.load(this.name);
|
|
1604
2939
|
this.settings = Settings.load(this.name);
|
|
1605
2940
|
this.quickStore = new QuickStore(this.name);
|
|
2941
|
+
|
|
2942
|
+
// Initialize encryption if PIN provided
|
|
2943
|
+
if (options.pin) {
|
|
2944
|
+
await this.initializeEncryption(options.pin, options.salt);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
1606
2947
|
return this;
|
|
1607
2948
|
}
|
|
1608
2949
|
|
|
2950
|
+
async initializeEncryption(pin, salt = null) {
|
|
2951
|
+
this.encryption = new SecureDatabaseEncryption();
|
|
2952
|
+
|
|
2953
|
+
// Check if we have existing salt in metadata
|
|
2954
|
+
const encMetaKey = `lacertadb_${this.name}_encryption`;
|
|
2955
|
+
let existingSalt = null;
|
|
2956
|
+
|
|
2957
|
+
if (!salt) {
|
|
2958
|
+
const stored = localStorage.getItem(encMetaKey);
|
|
2959
|
+
if (stored) {
|
|
2960
|
+
const metadata = JSON.parse(stored);
|
|
2961
|
+
existingSalt = metadata.salt;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
const saltBase64 = await this.encryption.initialize(
|
|
2966
|
+
pin,
|
|
2967
|
+
existingSalt ? base64.decode(existingSalt) : salt
|
|
2968
|
+
);
|
|
2969
|
+
|
|
2970
|
+
// Store salt for future sessions
|
|
2971
|
+
if (!existingSalt) {
|
|
2972
|
+
const metadata = this.encryption.exportMetadata();
|
|
2973
|
+
localStorage.setItem(encMetaKey, JSON.stringify(metadata));
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
this.isEncrypted = true;
|
|
2977
|
+
return saltBase64;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
async changePin(oldPin, newPin) {
|
|
2981
|
+
if (!this.isEncrypted) {
|
|
2982
|
+
throw new Error('Database is not encrypted');
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
const newSalt = await this.encryption.changePin(oldPin, newPin);
|
|
2986
|
+
|
|
2987
|
+
// Update stored metadata
|
|
2988
|
+
const encMetaKey = `lacertadb_${this.name}_encryption`;
|
|
2989
|
+
const metadata = this.encryption.exportMetadata();
|
|
2990
|
+
localStorage.setItem(encMetaKey, JSON.stringify(metadata));
|
|
2991
|
+
|
|
2992
|
+
return newSalt;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
async storePrivateKey(keyName, privateKey, additionalAuth = '') {
|
|
2996
|
+
if (!this.isEncrypted) {
|
|
2997
|
+
throw new Error('Database must be encrypted to store private keys');
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
const encryptedKey = await this.encryption.encryptPrivateKey(
|
|
3001
|
+
privateKey,
|
|
3002
|
+
additionalAuth
|
|
3003
|
+
);
|
|
3004
|
+
|
|
3005
|
+
// Store in a special collection
|
|
3006
|
+
let keyStore = await this.getCollection('__private_keys__').catch(() => null);
|
|
3007
|
+
if (!keyStore) {
|
|
3008
|
+
keyStore = await this.createCollection('__private_keys__', {
|
|
3009
|
+
encrypted: false // Already encrypted at database level
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
await keyStore.add({
|
|
3014
|
+
name: keyName,
|
|
3015
|
+
key: encryptedKey,
|
|
3016
|
+
createdAt: Date.now()
|
|
3017
|
+
}, {
|
|
3018
|
+
id: keyName,
|
|
3019
|
+
permanent: true
|
|
3020
|
+
});
|
|
3021
|
+
|
|
3022
|
+
return true;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
async getPrivateKey(keyName, additionalAuth = '') {
|
|
3026
|
+
if (!this.isEncrypted) {
|
|
3027
|
+
throw new Error('Database must be encrypted to retrieve private keys');
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
const keyStore = await this.getCollection('__private_keys__');
|
|
3031
|
+
const doc = await keyStore.get(keyName);
|
|
3032
|
+
|
|
3033
|
+
if (!doc) {
|
|
3034
|
+
throw new Error(`Private key '${keyName}' not found`);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
return await this.encryption.decryptPrivateKey(doc.key, additionalAuth);
|
|
3038
|
+
}
|
|
3039
|
+
|
|
1609
3040
|
async createCollection(name, options) {
|
|
1610
3041
|
if (this.collections.has(name)) {
|
|
1611
3042
|
throw new LacertaDBError(`Collection '${name}' already exists.`, 'COLLECTION_EXISTS');
|
|
@@ -1637,7 +3068,7 @@ class Database {
|
|
|
1637
3068
|
async dropCollection(name) {
|
|
1638
3069
|
if (this.collections.has(name)) {
|
|
1639
3070
|
const collection = this.collections.get(name);
|
|
1640
|
-
await collection.clear();
|
|
3071
|
+
await collection.clear({ force: true });
|
|
1641
3072
|
collection.destroy();
|
|
1642
3073
|
this.collections.delete(name);
|
|
1643
3074
|
}
|
|
@@ -1672,11 +3103,13 @@ class Database {
|
|
|
1672
3103
|
};
|
|
1673
3104
|
}
|
|
1674
3105
|
|
|
1675
|
-
updateSettings(newSettings) {
|
|
3106
|
+
updateSettings(newSettings) {
|
|
3107
|
+
this.settings.updateSettings(newSettings);
|
|
3108
|
+
}
|
|
1676
3109
|
|
|
1677
3110
|
async export(format = 'json', password = null) {
|
|
1678
3111
|
const data = {
|
|
1679
|
-
version: '
|
|
3112
|
+
version: '5.0.0',
|
|
1680
3113
|
database: this.name,
|
|
1681
3114
|
timestamp: Date.now(),
|
|
1682
3115
|
collections: {}
|
|
@@ -1739,6 +3172,9 @@ class Database {
|
|
|
1739
3172
|
destroy() {
|
|
1740
3173
|
this.collections.forEach(collection => collection.destroy());
|
|
1741
3174
|
this.collections.clear();
|
|
3175
|
+
if (this.encryption) {
|
|
3176
|
+
this.encryption.destroy();
|
|
3177
|
+
}
|
|
1742
3178
|
}
|
|
1743
3179
|
}
|
|
1744
3180
|
|
|
@@ -1752,15 +3188,19 @@ export class LacertaDB {
|
|
|
1752
3188
|
this.performanceMonitor = new PerformanceMonitor();
|
|
1753
3189
|
}
|
|
1754
3190
|
|
|
1755
|
-
async getDatabase(name) {
|
|
3191
|
+
async getDatabase(name, options = {}) {
|
|
1756
3192
|
if (!this.databases.has(name)) {
|
|
1757
3193
|
const db = new Database(name, this.performanceMonitor);
|
|
1758
|
-
await db.init();
|
|
3194
|
+
await db.init(options);
|
|
1759
3195
|
this.databases.set(name, db);
|
|
1760
3196
|
}
|
|
1761
3197
|
return this.databases.get(name);
|
|
1762
3198
|
}
|
|
1763
3199
|
|
|
3200
|
+
async getSecureDatabase(name, pin, salt = null) {
|
|
3201
|
+
return this.getDatabase(name, { pin, salt });
|
|
3202
|
+
}
|
|
3203
|
+
|
|
1764
3204
|
async dropDatabase(name) {
|
|
1765
3205
|
if (this.databases.has(name)) {
|
|
1766
3206
|
const db = this.databases.get(name);
|
|
@@ -1769,7 +3209,7 @@ export class LacertaDB {
|
|
|
1769
3209
|
this.databases.delete(name);
|
|
1770
3210
|
}
|
|
1771
3211
|
|
|
1772
|
-
['metadata', 'settings', 'version'].forEach(suffix => {
|
|
3212
|
+
['metadata', 'settings', 'version', 'encryption'].forEach(suffix => {
|
|
1773
3213
|
localStorage.removeItem(`lacertadb_${name}_${suffix}`);
|
|
1774
3214
|
});
|
|
1775
3215
|
const quickStore = new QuickStore(name);
|
|
@@ -1780,10 +3220,11 @@ export class LacertaDB {
|
|
|
1780
3220
|
const dbNames = new Set();
|
|
1781
3221
|
for (let i = 0; i < localStorage.length; i++) {
|
|
1782
3222
|
const key = localStorage.key(i);
|
|
1783
|
-
// Replaced optional chaining with `&&` for compatibility
|
|
1784
3223
|
if (key && key.startsWith('lacertadb_')) {
|
|
1785
|
-
const
|
|
1786
|
-
|
|
3224
|
+
const parts = key.split('_');
|
|
3225
|
+
if (parts.length >= 2) {
|
|
3226
|
+
dbNames.add(parts[1]);
|
|
3227
|
+
}
|
|
1787
3228
|
}
|
|
1788
3229
|
}
|
|
1789
3230
|
return [...dbNames];
|
|
@@ -1791,7 +3232,7 @@ export class LacertaDB {
|
|
|
1791
3232
|
|
|
1792
3233
|
async createBackup(password = null) {
|
|
1793
3234
|
const backup = {
|
|
1794
|
-
version: '
|
|
3235
|
+
version: '5.0.0',
|
|
1795
3236
|
timestamp: Date.now(),
|
|
1796
3237
|
databases: {}
|
|
1797
3238
|
};
|
|
@@ -1841,7 +3282,10 @@ export class LacertaDB {
|
|
|
1841
3282
|
}
|
|
1842
3283
|
}
|
|
1843
3284
|
|
|
1844
|
-
//
|
|
3285
|
+
// ========================
|
|
3286
|
+
// Export all components
|
|
3287
|
+
// ========================
|
|
3288
|
+
|
|
1845
3289
|
export {
|
|
1846
3290
|
Database,
|
|
1847
3291
|
Collection,
|
|
@@ -1849,5 +3293,17 @@ export {
|
|
|
1849
3293
|
MigrationManager,
|
|
1850
3294
|
PerformanceMonitor,
|
|
1851
3295
|
LacertaDBError,
|
|
1852
|
-
OPFSUtility
|
|
1853
|
-
|
|
3296
|
+
OPFSUtility,
|
|
3297
|
+
IndexManager,
|
|
3298
|
+
CacheStrategy,
|
|
3299
|
+
LRUCache,
|
|
3300
|
+
LFUCache,
|
|
3301
|
+
TTLCache,
|
|
3302
|
+
BTreeIndex,
|
|
3303
|
+
TextIndex,
|
|
3304
|
+
GeoIndex,
|
|
3305
|
+
SecureDatabaseEncryption
|
|
3306
|
+
};
|
|
3307
|
+
|
|
3308
|
+
// Default export
|
|
3309
|
+
export default LacertaDB;
|