liekodb 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1227 -0
  3. package/liekodb.js +2014 -0
  4. package/package.json +29 -0
package/liekodb.js ADDED
@@ -0,0 +1,2014 @@
1
+ const fs = require('fs').promises;
2
+ const fsSync = require('fs');
3
+ const path = require('path');
4
+
5
+ const http = require('http');
6
+ const https = require('https');
7
+ const { URL } = require('url');
8
+
9
+ class CollectionCache {
10
+ constructor(options = {}) {
11
+ this.storagePath = options.storagePath || './storage';
12
+ this.debug = options.debug || false;
13
+
14
+ this.cache = new Map();
15
+ this.locks = new Map();
16
+ this.dirty = new Set();
17
+ this.pendingSaves = new Set();
18
+
19
+ this.isShuttingDown = false;
20
+
21
+ const autoSaveInterval = options.autoSaveInterval || 5000;
22
+
23
+ if (autoSaveInterval > 0) {
24
+ this.autoSaveTimer = setInterval(() => {
25
+ this._autoSave().catch(console.error);
26
+ }, autoSaveInterval);
27
+ this.autoSaveTimer.unref();
28
+ }
29
+
30
+ this._setupShutdown();
31
+ }
32
+
33
+ async _withLock(name, fn) {
34
+ const previous = this.locks.get(name) || Promise.resolve();
35
+
36
+ const next = previous
37
+ .then(fn)
38
+ .catch(err => {
39
+ console.error(`[CollectionCache] ${name} error:`, err);
40
+ throw err;
41
+ });
42
+
43
+ this.locks.set(name, next);
44
+ return next;
45
+ }
46
+
47
+ async _loadUnlocked(name) {
48
+ if (this.cache.has(name)) {
49
+ return this.cache.get(name);
50
+ }
51
+
52
+ const start = Utils.startTimer();
53
+
54
+ const filePath = path.join(this.storagePath, `${name}.json`);
55
+
56
+ try {
57
+ const raw = await fs.readFile(filePath, 'utf8');
58
+
59
+ let documents = [];
60
+ try {
61
+ documents = JSON.parse(raw) || [];
62
+ } catch (e) {
63
+ console.error(`[CollectionCache] Corrupted file ${name}, starting fresh`);
64
+ }
65
+
66
+ const idIndex = new Map();
67
+ documents.forEach((doc, i) => {
68
+ if (doc?.id !== undefined) {
69
+ idIndex.set(doc.id, i);
70
+ }
71
+ });
72
+
73
+ const data = { documents, idIndex, dirty: false };
74
+ this.cache.set(name, data);
75
+
76
+ if (this.debug) {
77
+ console.log(`[CollectionCache] Loaded ${name} with ${data.documents.length} documents in ${Format.formatDuration(Utils.endTimer(start))}`);
78
+ }
79
+
80
+ return data;
81
+
82
+ } catch (err) {
83
+ if (err.code === 'ENOENT') {
84
+ const data = {
85
+ documents: [],
86
+ idIndex: new Map(),
87
+ dirty: false
88
+ };
89
+ this.cache.set(name, data);
90
+ return data;
91
+ }
92
+ throw err;
93
+ }
94
+ }
95
+
96
+ reorderDocumentFields = (document) => {
97
+ if (!document || typeof document !== 'object') return document;
98
+
99
+ const orderedDocument = {};
100
+ const reservedFields = ['id', 'createdAt', 'updatedAt'];
101
+
102
+ if (document.id !== undefined) {
103
+ orderedDocument.id = document.id;
104
+ }
105
+
106
+ const normalFields = Object.keys(document)
107
+ .filter(key => !reservedFields.includes(key))
108
+ .sort();
109
+
110
+ for (const key of normalFields) {
111
+ orderedDocument[key] = structuredClone(document[key]);
112
+ }
113
+
114
+ if (document.createdAt !== undefined) {
115
+ orderedDocument.createdAt = document.createdAt;
116
+ }
117
+ if (document.updatedAt !== undefined) {
118
+ orderedDocument.updatedAt = document.updatedAt;
119
+ }
120
+
121
+ return orderedDocument;
122
+ }
123
+
124
+ async _saveUnlocked(name, data) {
125
+ const filePath = path.join(this.storagePath, `${name}.json`);
126
+ const tmpPath = `${filePath}.${Date.now()}.tmp`;
127
+
128
+ this.pendingSaves.add(tmpPath);
129
+
130
+ try {
131
+ await fs.mkdir(this.storagePath, { recursive: true });
132
+
133
+ const content =
134
+ '[\n' +
135
+ data.documents.map(doc => JSON.stringify(this.reorderDocumentFields(doc))).join(',\n') +
136
+ '\n]';
137
+
138
+ await fs.writeFile(tmpPath, content, 'utf8');
139
+
140
+ // sanity check
141
+ JSON.parse(await fs.readFile(tmpPath, 'utf8'));
142
+
143
+ await fs.rename(tmpPath, filePath);
144
+ } finally {
145
+ this.pendingSaves.delete(tmpPath);
146
+ await fs.unlink(tmpPath).catch(() => { });
147
+ }
148
+ }
149
+
150
+ async get(name) {
151
+ return this._withLock(name, async () => {
152
+ return this._loadUnlocked(name);
153
+ });
154
+ }
155
+
156
+ async update(name, updateFn) {
157
+ return this._withLock(name, async () => {
158
+ const data = await this._loadUnlocked(name);
159
+ const changed = await updateFn(data);
160
+
161
+ if (changed !== false) {
162
+ data.dirty = true;
163
+ this.dirty.add(name);
164
+ }
165
+
166
+ return changed;
167
+ });
168
+ }
169
+
170
+ async updateDocument(name, id, updateFn) {
171
+ return this._withLock(name, async () => {
172
+ const data = await this._loadUnlocked(name);
173
+
174
+ const idx = data.idIndex.get(id);
175
+ if (idx === undefined) {
176
+ throw new Error(`[CollectionCache.updateDocument] Document ${id} not found in ${name}`);
177
+ }
178
+
179
+ const updated = {
180
+ ...data.documents[idx],
181
+ ...updateFn({ ...data.documents[idx] })
182
+ };
183
+
184
+ if (updated.id !== id) {
185
+ throw new Error('[CollectionCache.update] Updated document id cannot be changed');
186
+ }
187
+
188
+ data.documents[idx] = updated;
189
+ data.dirty = true;
190
+ this.dirty.add(name);
191
+
192
+ return updated;
193
+ });
194
+ }
195
+
196
+ async removeDocument(name, id) {
197
+ return this._withLock(name, async () => {
198
+ const data = await this._loadUnlocked(name);
199
+
200
+ const idx = data.idIndex.get(id);
201
+ if (idx === undefined) return false;
202
+
203
+ data.documents.splice(idx, 1);
204
+ data.idIndex.delete(id);
205
+
206
+ for (let i = idx; i < data.documents.length; i++) {
207
+ const doc = data.documents[i];
208
+ if (doc?.id !== undefined) {
209
+ data.idIndex.set(doc.id, i);
210
+ }
211
+ }
212
+
213
+ data.dirty = true;
214
+ this.dirty.add(name);
215
+
216
+ return true;
217
+ });
218
+ }
219
+
220
+ async save(name) {
221
+ const start = Utils.startTimer();
222
+
223
+ return this._withLock(name, async () => {
224
+ const data = this.cache.get(name);
225
+ if (!data || !data.dirty) return false;
226
+
227
+ await this._saveUnlocked(name, data);
228
+
229
+ data.dirty = false;
230
+ this.dirty.delete(name);
231
+
232
+ console.log(`[CollectionCache] Saved ${name}.json with ${data.documents.length} documents in ${Format.formatDuration(Utils.endTimer(start))}`);
233
+ return true;
234
+ });
235
+ }
236
+
237
+ async _autoSave() {
238
+ if (this.isShuttingDown) return;
239
+ if (this.dirty.size === 0) return;
240
+
241
+ for (const name of Array.from(this.dirty)) {
242
+ try {
243
+ await this.save(name);
244
+ } catch (err) {
245
+ console.error(`[CollectionCache] Autosave failed for ${name}`, err);
246
+ }
247
+ }
248
+ }
249
+
250
+ async flushAll() {
251
+ this.isShuttingDown = true;
252
+ const start = Utils.startTimer();
253
+
254
+ const savePromises = Array.from(this.dirty).map(name =>
255
+ this.save(name).catch(console.error)
256
+ );
257
+
258
+ await Promise.all(savePromises);
259
+
260
+ console.log(`[CollectionCache] Flushed ${savePromises.length} collections in ${Format.formatDuration(Utils.endTimer(start))}`);
261
+ }
262
+
263
+ _setupShutdown() {
264
+ if (typeof process === 'undefined') return;
265
+
266
+ const shutdown = async (signal) => {
267
+ if (this.debug) {
268
+ console.log(`[CollectionCache] ${signal} received`);
269
+ }
270
+ await this.flushAll();
271
+ };
272
+
273
+ process.once('SIGINT', () => shutdown('SIGINT'));
274
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
275
+ process.once('beforeExit', () => this.flushAll());
276
+ }
277
+ }
278
+
279
+ class QueryEngine {
280
+
281
+ static applyFilters(data, filter) {
282
+ if (!filter || Object.keys(filter).length === 0) return data;
283
+
284
+ return data.filter(item => this.matchesFilter(item, filter));
285
+ }
286
+
287
+ static compareValue(actual, expected) {
288
+ return actual === expected;
289
+ }
290
+
291
+ static matchesFilter(item, filter) {
292
+ if (!filter) return true;
293
+ if (filter.$and) return filter.$and.every(f => this.matchesFilter(item, f));
294
+ if (filter.$or) return filter.$or.some(f => this.matchesFilter(item, f));
295
+ if (filter.$nor) return !filter.$nor.some(f => this.matchesFilter(item, f));
296
+ if (filter.$not) return !this.matchesFilter(item, filter.$not);
297
+
298
+ for (const key of Object.keys(filter)) {
299
+ if (key.startsWith('$')) continue;
300
+
301
+ const expected = filter[key];
302
+ const value = this.getValue(item, key);
303
+
304
+ if (
305
+ typeof expected === 'object' &&
306
+ expected !== null &&
307
+ !Array.isArray(expected)
308
+ ) {
309
+ if (!this.matchesOperators(value, expected)) return false;
310
+ } else {
311
+ if (Array.isArray(value)) {
312
+ if (!value.includes(expected)) return false;
313
+ } else if (value !== expected) {
314
+ return false;
315
+ }
316
+ }
317
+ }
318
+
319
+ return true;
320
+ }
321
+
322
+ static getValue(item, path) {
323
+ if (!path.includes('.')) return item[path];
324
+
325
+ const parts = path.split('.');
326
+ let cur = item;
327
+
328
+ for (let i = 0; i < parts.length; i++) {
329
+ const p = parts[i];
330
+
331
+ if (Array.isArray(cur)) {
332
+ const idx = parseInt(p, 10);
333
+ if (!isNaN(idx) && idx >= 0 && idx < cur.length) {
334
+ cur = cur[idx];
335
+ continue;
336
+ }
337
+
338
+ const remainingPath = parts.slice(i).join('.');
339
+ let results = [];
340
+
341
+ for (let el of cur) {
342
+ const v = this.getValue(el, remainingPath);
343
+ if (v !== undefined) {
344
+ if (Array.isArray(v)) {
345
+ results.push(...v);
346
+ } else {
347
+ results.push(v);
348
+ }
349
+ }
350
+ }
351
+
352
+ return results.length > 0 ? results : undefined;
353
+ }
354
+
355
+ if (cur == null || typeof cur !== 'object') return undefined;
356
+ cur = cur[p];
357
+ }
358
+
359
+ return cur;
360
+ }
361
+
362
+ static matchesOperators(actual, ops) {
363
+ for (const [op, expected] of Object.entries(ops)) {
364
+ if (op === '$options') continue;
365
+
366
+ if (actual === undefined) {
367
+ switch (op) {
368
+ case '$exists':
369
+ if (expected === true) return false;
370
+ if (expected === false) return true;
371
+ break;
372
+ case '$ne':
373
+ return true;
374
+ default:
375
+ return false;
376
+ }
377
+ continue;
378
+ }
379
+
380
+ switch (op) {
381
+ case '$eq':
382
+ if (Array.isArray(actual)) {
383
+ if (!actual.includes(expected)) return false;
384
+ } else if (actual !== expected) return false;
385
+ break;
386
+
387
+ case '$ne':
388
+ if (Array.isArray(actual)) {
389
+ if (actual.includes(expected)) return false;
390
+ } else if (actual === expected) return false;
391
+ break;
392
+
393
+ case '$gt':
394
+ if (Array.isArray(actual)) {
395
+ if (!actual.some(v => v > expected)) return false;
396
+ } else if (!(actual > expected)) return false;
397
+ break;
398
+
399
+ case '$gte':
400
+ if (Array.isArray(actual)) {
401
+ if (!actual.some(v => v >= expected)) return false;
402
+ } else if (!(actual >= expected)) return false;
403
+ break;
404
+
405
+ case '$lt':
406
+ if (Array.isArray(actual)) {
407
+ if (!actual.some(v => v < expected)) return false;
408
+ } else if (!(actual < expected)) return false;
409
+ break;
410
+
411
+ case '$lte':
412
+ if (Array.isArray(actual)) {
413
+ if (!actual.some(v => v <= expected)) return false;
414
+ } else if (!(actual <= expected)) return false;
415
+ break;
416
+
417
+ case '$in':
418
+ if (Array.isArray(actual)) {
419
+ if (!actual.some(v => expected.includes(v))) return false;
420
+ } else {
421
+ if (!expected.includes(actual)) return false;
422
+ }
423
+ break;
424
+
425
+ case '$nin':
426
+ if (Array.isArray(actual)) {
427
+ if (actual.some(v => expected.includes(v))) return false;
428
+ } else {
429
+ if (expected.includes(actual)) return false;
430
+ }
431
+ break;
432
+
433
+ case '$exists':
434
+ if (expected === true && actual === undefined) return false;
435
+ if (expected === false && actual !== undefined) return false;
436
+ break;
437
+
438
+ case '$not':
439
+ return !this.matchesOperators(actual, expected);
440
+
441
+ case '$regex':
442
+ try {
443
+ const pattern = expected instanceof RegExp ? expected : new RegExp(expected, ops.$options || '');
444
+ if (Array.isArray(actual)) {
445
+ if (!actual.some(v => pattern.test(String(v)))) return false;
446
+ } else {
447
+ if (!pattern.test(String(actual))) return false;
448
+ }
449
+ } catch (e) {
450
+ console.warn('Invalid regex pattern:', expected);
451
+ return false;
452
+ }
453
+ break;
454
+
455
+ case '$mod':
456
+ if (!Array.isArray(expected) || expected.length !== 2) {
457
+ console.warn("Invalid $mod operator:", expected);
458
+ return false;
459
+ }
460
+ const [div, rem] = expected;
461
+ if (Array.isArray(actual)) {
462
+ if (!actual.some(v => typeof v === 'number' && v % div === rem)) return false;
463
+ } else {
464
+ if (typeof actual !== 'number') return false;
465
+ if (actual % div !== rem) return false;
466
+ }
467
+ break;
468
+
469
+ default:
470
+ if (this.debug) console.warn("Unknown operator:", op);
471
+ continue;
472
+ }
473
+ }
474
+ return true;
475
+ }
476
+
477
+ static count(documents, filters = {}) {
478
+ if (!filters || Object.keys(filters).length === 0) {
479
+ return documents.length;
480
+ }
481
+ return this.applyFilters(documents, filters).length;
482
+ }
483
+
484
+ static sortResults(data, sortSpec) {
485
+ return data.sort((a, b) => {
486
+ for (const [field, direction] of Object.entries(sortSpec)) {
487
+ const aVal = this.getValue(a, field);
488
+ const bVal = this.getValue(b, field);
489
+
490
+ if (aVal < bVal) return direction === 1 ? -1 : 1;
491
+ if (aVal > bVal) return direction === 1 ? 1 : -1;
492
+ }
493
+ return 0;
494
+ });
495
+ }
496
+
497
+ static selectFields(data, projection) {
498
+ if (!projection || Object.keys(projection).length === 0) {
499
+ return data;
500
+ }
501
+
502
+ if (data == null) {
503
+ return data;
504
+ }
505
+
506
+ const hasIncludeFields = Object.values(projection).some(v => v === 1 || v === true);
507
+ const hasExcludeFields = Object.values(projection).some(v => v === -1 || v === false);
508
+
509
+ const projectSingleDoc = (doc) => {
510
+ if (!doc || typeof doc !== 'object') {
511
+ return doc;
512
+ }
513
+
514
+ if (hasIncludeFields && !hasExcludeFields) {
515
+ const result = {};
516
+ for (const [field, include] of Object.entries(projection)) {
517
+ if (include === 1 || include === true) {
518
+ result[field] = this.getValue(doc, field);
519
+ }
520
+ }
521
+ return result;
522
+ }
523
+
524
+ if (hasExcludeFields && !hasIncludeFields) {
525
+ const result = { ...doc };
526
+ for (const [field, exclude] of Object.entries(projection)) {
527
+ if (exclude === -1 || exclude === false) {
528
+ const parts = field.split('.');
529
+ if (parts.length === 1) {
530
+ delete result[field];
531
+ } else {
532
+ this.removeFieldByPath(result, field);
533
+ }
534
+ }
535
+ }
536
+ return result;
537
+ }
538
+
539
+ console.warn('Mixed inclusion/exclusion in projection not supported. Returning full document.');
540
+ return doc;
541
+ };
542
+
543
+ if (Array.isArray(data)) {
544
+ return data.map(projectSingleDoc);
545
+ } else if (typeof data === 'object') {
546
+ return projectSingleDoc(data);
547
+ } else {
548
+ return data;
549
+ }
550
+ }
551
+
552
+ static removeFieldByPath(obj, path) {
553
+ const parts = path.split('.');
554
+ let current = obj;
555
+
556
+ for (let i = 0; i < parts.length - 1; i++) {
557
+ const part = parts[i];
558
+ if (current[part] === undefined || typeof current[part] !== 'object') {
559
+ return;
560
+ }
561
+ current = current[part];
562
+ }
563
+
564
+ const lastPart = parts[parts.length - 1];
565
+ delete current[lastPart];
566
+ }
567
+
568
+ static applyUpdateToDoc(doc, update) {
569
+ if (!update) return;
570
+
571
+ const applyNestedOperation = (obj, path, operation, value) => {
572
+ const parts = path.split('.');
573
+ let current = obj;
574
+
575
+ for (let i = 0; i < parts.length - 1; i++) {
576
+ const part = parts[i];
577
+ if (current[part] === undefined || typeof current[part] !== 'object') {
578
+ current[part] = {};
579
+ }
580
+ current = current[part];
581
+ }
582
+
583
+ const lastPart = parts[parts.length - 1];
584
+
585
+ switch (operation) {
586
+ case 'set':
587
+ current[lastPart] = value;
588
+ break;
589
+ case 'unset':
590
+ delete current[lastPart];
591
+ break;
592
+ case 'inc':
593
+ current[lastPart] = (typeof current[lastPart] === 'number' ? current[lastPart] : 0) + value;
594
+ break;
595
+ case 'push':
596
+ if (!Array.isArray(current[lastPart])) current[lastPart] = [];
597
+ current[lastPart].push(value);
598
+ break;
599
+ case 'addToSet':
600
+ if (!Array.isArray(current[lastPart])) current[lastPart] = [];
601
+ if (!current[lastPart].includes(value)) {
602
+ current[lastPart].push(value);
603
+ }
604
+ break;
605
+ case 'pull':
606
+ if (Array.isArray(current[lastPart])) {
607
+ current[lastPart] = current[lastPart].filter(item => item !== value);
608
+ }
609
+ break;
610
+ }
611
+ };
612
+
613
+ const hasRootLevelOperators =
614
+ '$set' in update ||
615
+ '$unset' in update ||
616
+ '$inc' in update ||
617
+ '$push' in update ||
618
+ '$pull' in update ||
619
+ '$addToSet' in update;
620
+
621
+ if (hasRootLevelOperators) {
622
+ if (update.$set) {
623
+ for (const [k, v] of Object.entries(update.$set)) {
624
+ if (k.includes('.')) {
625
+ applyNestedOperation(doc, k, 'set', v);
626
+ } else {
627
+ doc[k] = v;
628
+ }
629
+ }
630
+ }
631
+
632
+ if (update.$unset) {
633
+ for (const k of Object.keys(update.$unset)) {
634
+ if (k.includes('.')) {
635
+ applyNestedOperation(doc, k, 'unset', null);
636
+ } else {
637
+ delete doc[k];
638
+ }
639
+ }
640
+ }
641
+
642
+ if (update.$inc) {
643
+ for (const [k, v] of Object.entries(update.$inc)) {
644
+ if (k.includes('.')) {
645
+ applyNestedOperation(doc, k, 'inc', v);
646
+ } else {
647
+ doc[k] = (typeof doc[k] === 'number' ? doc[k] : 0) + v;
648
+ }
649
+ }
650
+ }
651
+
652
+ if (update.$push) {
653
+ for (const [k, v] of Object.entries(update.$push)) {
654
+ if (k.includes('.')) {
655
+ applyNestedOperation(doc, k, 'push', v);
656
+ } else {
657
+ if (!Array.isArray(doc[k])) doc[k] = [];
658
+ doc[k].push(v);
659
+ }
660
+ }
661
+ }
662
+
663
+ if (update.$addToSet) {
664
+ for (const [k, v] of Object.entries(update.$addToSet)) {
665
+ if (k.includes('.')) {
666
+ applyNestedOperation(doc, k, 'addToSet', v);
667
+ } else {
668
+ if (!Array.isArray(doc[k])) doc[k] = [];
669
+ if (v && typeof v === 'object' && v.$each) {
670
+ for (const item of v.$each) {
671
+ if (!doc[k].includes(item)) {
672
+ doc[k].push(item);
673
+ }
674
+ }
675
+ } else {
676
+ if (!doc[k].includes(v)) {
677
+ doc[k].push(v);
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+
684
+ if (update.$pull) {
685
+ for (const [k, v] of Object.entries(update.$pull)) {
686
+ if (k.includes('.')) {
687
+ applyNestedOperation(doc, k, 'pull', v);
688
+ } else {
689
+ if (Array.isArray(doc[k])) {
690
+ doc[k] = doc[k].filter(item => {
691
+ if (typeof v === 'object' && v.$in) {
692
+ return !v.$in.includes(item);
693
+ }
694
+ return item !== v;
695
+ });
696
+ }
697
+ }
698
+ }
699
+ }
700
+ } else {
701
+ Object.assign(doc, update);
702
+ }
703
+
704
+ doc.updatedAt = new Date().toISOString();
705
+ }
706
+ }
707
+
708
+ class HTTPAdapter {
709
+ constructor(opts = {}) {
710
+ this.poolSize = opts.poolSize || 10;
711
+ this.requestQueue = [];
712
+ this.activeRequests = 0;
713
+ this.maxRetries = opts.maxRetries || 3;
714
+ this.timeout = opts.timeout || 15000;
715
+ this.databaseUrl = opts.databaseUrl || 'http://127.0.0.1:8050';
716
+ this.token = opts.token || null;
717
+ this.parsedBaseUrl = new URL(this.databaseUrl);
718
+ this.isHttps = this.parsedBaseUrl.protocol === 'https:';
719
+ this.hostname = ['localhost', '127.0.0.1'].includes(this.parsedBaseUrl.hostname)
720
+ ? '127.0.0.1'
721
+ : this.parsedBaseUrl.hostname;
722
+
723
+ const isLocal = ['localhost', '127.0.0.1'].includes(this.parsedBaseUrl.hostname);
724
+
725
+ const agentOptions = {
726
+ keepAlive: !isLocal, // désactivé en local → pas utile
727
+ keepAliveMsecs: 1000,
728
+ maxSockets: this.poolSize,
729
+ maxFreeSockets: this.poolSize,
730
+ timeout: this.timeout,
731
+ scheduling: 'lifo'
732
+ };
733
+
734
+ this.httpAgent = new http.Agent(agentOptions);
735
+ this.httpsAgent = new https.Agent(agentOptions);
736
+
737
+ // Configuration des sockets
738
+ const setupSocket = (socket) => {
739
+ socket.setNoDelay(true); // toujours utile (même en local)
740
+ if (!isLocal) {
741
+ socket.setKeepAlive(true, 1000); // seulement en distant
742
+ }
743
+ };
744
+
745
+ this.httpAgent.on('socket', setupSocket);
746
+ this.httpsAgent.on('socket', setupSocket);
747
+
748
+ this.baseHeaders = {
749
+ "Content-Type": "application/json"
750
+ };
751
+
752
+ if (this.token) {
753
+ this.baseHeaders.Authorization = `Bearer ${this.token}`;
754
+ }
755
+ }
756
+
757
+ async request(method, endpoint, data = {}) {
758
+ return new Promise((resolve, reject) => {
759
+ this._enqueue({ method, endpoint, data, resolve, reject, retries: 0 });
760
+ });
761
+ }
762
+
763
+ _enqueue(req) {
764
+ this.requestQueue.push(req);
765
+ this._processQueue();
766
+ }
767
+
768
+ _processQueue() {
769
+ if (this.activeRequests >= this.poolSize || this.requestQueue.length === 0) return;
770
+
771
+ const req = this.requestQueue.shift();
772
+ this.activeRequests++;
773
+
774
+ this._execute(req)
775
+ .then(req.resolve)
776
+ .catch(err => {
777
+ if (req.retries < this.maxRetries && this._retryable(err)) {
778
+ req.retries++;
779
+ this.requestQueue.unshift(req);
780
+ } else {
781
+ req.reject(err);
782
+ }
783
+ })
784
+ .finally(() => {
785
+ this.activeRequests--;
786
+ setImmediate(() => this._processQueue());
787
+ });
788
+ }
789
+
790
+ async _execute(req) {
791
+ let path = req.endpoint;
792
+ let body = null;
793
+
794
+ const headers = { ...this.baseHeaders };
795
+
796
+ // 🔹 GET / HEAD → query string
797
+ if (req.method === "GET" || req.method === "HEAD") {
798
+ if (req.data && Object.keys(req.data).length > 0) {
799
+ const query = new URLSearchParams();
800
+
801
+ for (const [key, value] of Object.entries(req.data)) {
802
+ query.append(
803
+ key,
804
+ typeof value === "object"
805
+ ? JSON.stringify(value)
806
+ : String(value)
807
+ );
808
+ }
809
+
810
+ path += `?${query.toString()}`;
811
+ }
812
+ }
813
+ // 🔹 Autres méthodes → body JSON
814
+ else {
815
+ body = JSON.stringify(req.data || {});
816
+ headers["Content-Length"] = Buffer.byteLength(body);
817
+ }
818
+
819
+ const options = {
820
+ method: req.method,
821
+ hostname: this.hostname,
822
+ port: this.parsedBaseUrl.port,
823
+ path,
824
+ headers,
825
+ agent: this.isHttps ? this.httpsAgent : this.httpAgent
826
+ };
827
+
828
+ return new Promise((resolve, reject) => {
829
+ const start = performance.now();
830
+ const transport = this.isHttps ? https : http;
831
+
832
+ const request = transport.request(options, res => {
833
+ const chunks = [];
834
+
835
+ res.on("data", c => chunks.push(c));
836
+ res.on("end", () => {
837
+ const raw = Buffer.concat(chunks);
838
+ const size = raw.length;
839
+
840
+ let parsed = raw.toString();
841
+ if (res.headers["content-type"]?.includes("application/json")) {
842
+ try { parsed = JSON.parse(parsed); } catch { }
843
+ }
844
+
845
+ this._log(req, start, size, res.statusCode);
846
+
847
+ if (res.statusCode >= 200 && res.statusCode < 300) {
848
+ resolve(parsed);
849
+ } else {
850
+ const RED = '\x1b[31m';
851
+ const RESET = '\x1b[0m';
852
+
853
+ reject(`${RED}[ERROR] ${parsed.error?.code} ${parsed.error?.message || `HTTP ${res.statusCode}`}${RESET}`);
854
+ /*
855
+ reject(
856
+ new Error(
857
+ `HTTP ${res.statusCode}: ${parsed?.error.message || parsed
858
+ }`
859
+ )
860
+ );*/
861
+ }
862
+ });
863
+ });
864
+
865
+ request.on("error", err => {
866
+ this._log(req, start, 0, "ERROR", err.message);
867
+ reject(err);
868
+ });
869
+
870
+ request.setTimeout(this.timeout, () => {
871
+ request.destroy();
872
+ reject(new Error("Request timeout"));
873
+ });
874
+
875
+ if (body) request.end(body);
876
+ else request.end();
877
+ });
878
+ }
879
+
880
+ _retryable(err) {
881
+ if (!err || !err.message) return false;
882
+ return (
883
+ err.message.includes("timeout") ||
884
+ err.message.includes("ECONNRESET") ||
885
+ err.message.includes("ECONNREFUSED") ||
886
+ err.message.includes("EAI_AGAIN")
887
+ );
888
+ }
889
+
890
+ _log(req, start, size, status, error = null) {
891
+ const ms = (Math.round((performance.now() - start) * 1000) / 1000).toFixed(2);
892
+ const op = req.endpoint.split("/")[2]?.toUpperCase() || "REQUEST";
893
+
894
+ if (status === "ERROR") {
895
+ console.log(
896
+ `[HTTP] ${op} ${req.endpoint} | Error: ${error} | Duration: ${ms}ms`
897
+ );
898
+ } else {
899
+ console.log(
900
+ `[HTTP] ${op} | ${req.method} -> ${req.endpoint} | Status: ${status} | Duration: ${ms}ms | Size: ${size}B`
901
+ );
902
+ }
903
+ }
904
+
905
+ close() {
906
+ this.httpAgent.destroy();
907
+ this.httpsAgent.destroy();
908
+ }
909
+ }
910
+
911
+ class LocalAdapter {
912
+ constructor(options = {}) {
913
+ this.storagePath = options.storagePath || './storage';
914
+ this.debug = options.debug || false;
915
+
916
+ this.cache = new CollectionCache({
917
+ storagePath: this.storagePath,
918
+ autoSaveInterval: options.autoSaveInterval || 5000,
919
+ debug: options.debug
920
+ });
921
+
922
+ this.collectionName = null;
923
+ }
924
+
925
+ listCollections() {
926
+ const collections = [];
927
+
928
+ try {
929
+ const files = fsSync.readdirSync(this.storagePath);
930
+
931
+ for (const file of files) {
932
+ if (file.endsWith('.json')) {
933
+ const collectionName = path.basename(file, '.json');
934
+ const filePath = path.join(this.storagePath, file);
935
+
936
+ let totalDocuments = 0;
937
+ let fileSize = 0;
938
+
939
+ try {
940
+ const stats = fsSync.statSync(filePath);
941
+ fileSize = stats.size;
942
+
943
+ const content = fsSync.readFileSync(filePath, 'utf-8');
944
+ if (content.trim() === '') {
945
+ totalDocuments = 0;
946
+ } else {
947
+ const data = JSON.parse(content);
948
+ if (Array.isArray(data)) {
949
+ totalDocuments = data.length;
950
+ } else {
951
+ totalDocuments = Object.keys(data).length;
952
+ }
953
+ }
954
+ } catch (parseErr) {
955
+ console.error(`Error while reading ${collectionName}:`, parseErr);
956
+ totalDocuments = 'error';
957
+ fileSize = 'error';
958
+ }
959
+
960
+ collections.push({
961
+ name: collectionName,
962
+ totalDocuments,
963
+ sizeBytes: fileSize,
964
+ sizeFormatted: Format.formatBytes(fileSize)
965
+ });
966
+ }
967
+ }
968
+ } catch (err) {
969
+ console.error(`Error while reading storage path (${this.storagePath})`, err);
970
+ }
971
+
972
+ return collections.sort((a, b) => a.name.localeCompare(b.name));
973
+ }
974
+
975
+ async status() {
976
+ const collections = this.listCollections();
977
+
978
+ const totalDocuments = collections.reduce((sum, col) => {
979
+ return sum + (typeof col.totalDocuments === 'number' ? col.totalDocuments : 0);
980
+ }, 0);
981
+
982
+ const totalCollectionsSize = collections.reduce((sum, col) => {
983
+ return sum + (typeof col.sizeBytes === 'number' ? col.sizeBytes : 0);
984
+ }, 0);
985
+
986
+ return {
987
+ storagePath: this.storagePath,
988
+ collections,
989
+ totalCollections: collections.length,
990
+ totalDocuments,
991
+ totalCollectionsSize: totalCollectionsSize,
992
+ totalCollectionsSizeFormatted: Format.formatBytes(totalCollectionsSize)
993
+ };
994
+ }
995
+
996
+ async request(method, endpoint, payload = {}) {
997
+ const parts = endpoint.split("/").filter(Boolean);
998
+ // Payload can contains filters, options, data, update
999
+
1000
+ this.collectionName = parts[1];
1001
+ const param = parts[2];
1002
+
1003
+ if (method === "GET" && !param) return this.find(payload);
1004
+ if (method === "GET" && param === "count") return this.count(payload);
1005
+ if (method === "GET" && param) return this.findById(param, payload);
1006
+
1007
+ if (method === "POST") return this.insert(payload);
1008
+
1009
+ if (method === "PATCH" && param) return this.updateById(param, payload);
1010
+ if (method === "PATCH") return this.update(payload);
1011
+
1012
+ if (method === "DELETE" && param === "drop") return this.dropCollection();
1013
+ if (method === "DELETE" && param) return this.deleteById(param);
1014
+ if (method === "DELETE") return this.delete(payload);
1015
+
1016
+ throw new Error(`Unsupported endpoint: ${method} ${endpoint}`);
1017
+ }
1018
+
1019
+ async count({ filters = {} } = {}) {
1020
+ try {
1021
+ const start = Utils.startTimer();
1022
+ const collection = await this.cache.get(this.collectionName);
1023
+ const count = QueryEngine.count(collection.documents, filters);
1024
+
1025
+ const duration = Utils.endTimer(start);
1026
+ const details = `Filters: ${Format.formatFilters(filters)} | Count: ${count}`;
1027
+ Utils.logRequest('count', details, duration, Utils.getDataSize(count));
1028
+
1029
+ return {
1030
+ data: count
1031
+ };
1032
+
1033
+ } catch (err) {
1034
+ Utils.logError('count', err);
1035
+ return {
1036
+ error: {
1037
+ message: err.message || 'Failed to count documents',
1038
+ code: 500
1039
+ }
1040
+ };
1041
+ }
1042
+ }
1043
+
1044
+ async find({ filters = {}, options = {} } = {}) {
1045
+ const start = Utils.startTimer();
1046
+ try {
1047
+ const collection = await this.cache.get(this.collectionName);
1048
+
1049
+ let limitNum = null;
1050
+ let skipNum = 0;
1051
+
1052
+ if (options.limit) {
1053
+ limitNum = parseInt(options.limit, 10);
1054
+ if (isNaN(limitNum) || limitNum < 0) {
1055
+ throw new Error('Limit must be a positive number');
1056
+ }
1057
+
1058
+ if (options.skip !== undefined) {
1059
+ skipNum = parseInt(options.skip, 10);
1060
+ if (isNaN(skipNum) || skipNum < 0) {
1061
+ throw new Error('Skip cannot be negative');
1062
+ }
1063
+ } else if (options.page !== undefined) {
1064
+ const pageNum = parseInt(options.page, 10);
1065
+ if (isNaN(pageNum) || pageNum < 1) {
1066
+ throw new Error('Page must be >= 1');
1067
+ }
1068
+ skipNum = (pageNum - 1) * limitNum;
1069
+ }
1070
+ }
1071
+
1072
+ const filteredDocuments = QueryEngine.applyFilters(
1073
+ collection.documents,
1074
+ filters
1075
+ );
1076
+
1077
+ let documents = [...filteredDocuments];
1078
+
1079
+ if (options.sort) {
1080
+ if (typeof options.sort !== 'object') {
1081
+ throw new Error('Sort must be an object');
1082
+ }
1083
+ documents = QueryEngine.sortResults(documents, options.sort);
1084
+ }
1085
+
1086
+ if (limitNum !== null) {
1087
+ documents = documents.slice(skipNum, skipNum + limitNum);
1088
+ }
1089
+
1090
+ if (options.fields) {
1091
+ if (typeof options.fields !== 'object') {
1092
+ throw new Error('Fields must be an object');
1093
+ }
1094
+ documents = QueryEngine.selectFields(documents, options.fields);
1095
+ }
1096
+
1097
+ const totalFilteredDocuments = filteredDocuments.length;
1098
+ const returnedCount = documents.length;
1099
+ const duration = Utils.endTimer(start);
1100
+
1101
+ const response = {
1102
+ data: {
1103
+ foundCount: documents.length,
1104
+ foundDocuments: documents,
1105
+ totalDocuments: collection.documents.length
1106
+ }
1107
+ };
1108
+
1109
+ if (limitNum !== null) {
1110
+ const page = Math.floor(skipNum / limitNum) + 1;
1111
+ const totalPages = Math.max(1, Math.ceil(totalFilteredDocuments / limitNum));
1112
+
1113
+ response.data.isPaginated = true;
1114
+ response.data.pagination = {
1115
+ page,
1116
+ limit: limitNum,
1117
+ skip: skipNum,
1118
+
1119
+ total: totalFilteredDocuments,
1120
+ totalPages,
1121
+
1122
+ hasNext: page < totalPages,
1123
+ hasPrev: page > 1,
1124
+
1125
+ nextPage: page < totalPages ? page + 1 : null,
1126
+ prevPage: page > 1 ? page - 1 : null,
1127
+
1128
+ startIndex: totalFilteredDocuments > 0 ? skipNum + 1 : 0,
1129
+ endIndex: Math.min(skipNum + returnedCount, totalFilteredDocuments)
1130
+ };
1131
+ }
1132
+
1133
+ let pageInfo = '';
1134
+
1135
+ if (limitNum !== null) {
1136
+ const page = Math.floor(skipNum / limitNum) + 1;
1137
+ const totalPages = Math.max(1, Math.ceil(totalFilteredDocuments / limitNum));
1138
+ pageInfo = ` | Page: ${page}/${totalPages}`;
1139
+ }
1140
+
1141
+ const details =
1142
+ `Filters: ${Format.formatFilters(filters)}` +
1143
+ `${Format.formatOptions(options)}` +
1144
+ `${pageInfo} | Found: ${totalFilteredDocuments} | Returned: ${returnedCount}`;
1145
+
1146
+ Utils.logRequest(
1147
+ 'find',
1148
+ details,
1149
+ duration,
1150
+ Utils.getDataSize(response)
1151
+ );
1152
+
1153
+ return response;
1154
+
1155
+ } catch (err) {
1156
+ Utils.logError('find', err);
1157
+
1158
+ return {
1159
+ error: {
1160
+ message: err.message || 'An unexpected error occurred during find',
1161
+ code: 500
1162
+ }
1163
+ };
1164
+ }
1165
+ }
1166
+
1167
+ async findById(id, { options = {} } = {}) {
1168
+ try {
1169
+ const start = Utils.startTimer();
1170
+ const collectionData = await this.cache.get(this.collectionName);
1171
+ let details = '';
1172
+
1173
+ let document = null;
1174
+ if (collectionData.idIndex?.has(id)) {
1175
+ const idx = collectionData.idIndex.get(id);
1176
+ document = collectionData.documents[idx];
1177
+ }
1178
+
1179
+ if (options.fields && document) {
1180
+ document = QueryEngine.selectFields(document, options.fields);
1181
+ details = `ID: ${id} | ${Format.formatFieldsLog(options.fields)} | Found: ${document ? 'Yes' : 'No'}`;
1182
+ } else {
1183
+ details = `ID: ${id} | Found: ${document ? 'Yes' : 'No'}`;
1184
+ }
1185
+
1186
+ const duration = Utils.endTimer(start);
1187
+ Utils.logRequest('find_By_Id', details, duration, Utils.getDataSize(document));
1188
+
1189
+ return {
1190
+ data: document ?? null
1191
+ };
1192
+
1193
+ } catch (err) {
1194
+ Utils.logError('findById', err);
1195
+ return {
1196
+ error: {
1197
+ message: err.message || 'Internal error during findById',
1198
+ code: 500
1199
+ }
1200
+ };
1201
+ }
1202
+ }
1203
+
1204
+ async insert({ documents }) {
1205
+ const start = Utils.startTimer();
1206
+ try {
1207
+ const documentsToInsert = Array.isArray(documents) ? documents : [documents];
1208
+ const now = new Date().toISOString();
1209
+
1210
+ let totalDocuments = 0;
1211
+ const inserted = [];
1212
+ const updated = [];
1213
+
1214
+ await this.cache.update(this.collectionName, async (data) => {
1215
+ totalDocuments = data.documents.length;
1216
+ const insertCount = documentsToInsert.length;
1217
+ const useSequentialIds = insertCount >= 2;
1218
+
1219
+ let prefix = null;
1220
+ let sequence = 0;
1221
+
1222
+ if (useSequentialIds) {
1223
+ prefix = Date.now().toString(36);
1224
+ }
1225
+
1226
+ for (let document of documentsToInsert) {
1227
+ let docId = document.id;
1228
+
1229
+ if (!docId) {
1230
+ docId = useSequentialIds
1231
+ ? `${prefix}_${++sequence}`
1232
+ : Utils.generateId();
1233
+ document.id = String(docId);
1234
+ } else {
1235
+ document.id = String(docId);
1236
+ }
1237
+
1238
+ if (data.idIndex.has(document.id)) {
1239
+ // UPDATE existing document
1240
+ const existingIndex = data.idIndex.get(document.id);
1241
+ const existingDoc = data.documents[existingIndex];
1242
+ const originalCreatedAt = existingDoc.createdAt;
1243
+
1244
+ for (const [key, value] of Object.entries(document)) {
1245
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
1246
+ existingDoc[key] = {
1247
+ ...(existingDoc[key] || {}),
1248
+ ...value
1249
+ };
1250
+ } else {
1251
+ existingDoc[key] = value;
1252
+ }
1253
+ }
1254
+
1255
+ existingDoc.createdAt = originalCreatedAt;
1256
+ existingDoc.updatedAt = now;
1257
+
1258
+ updated.push(existingDoc);
1259
+ } else {
1260
+ // INSERT new document
1261
+ document.createdAt = now;
1262
+ document.updatedAt = now;
1263
+
1264
+ const newIndex = data.documents.length;
1265
+ data.documents.push(document);
1266
+ data.idIndex.set(document.id, newIndex);
1267
+
1268
+ inserted.push(document);
1269
+ }
1270
+ }
1271
+
1272
+ return { inserted, updated };
1273
+ });
1274
+
1275
+ const responseData = {
1276
+ insertedCount: inserted.length,
1277
+ updatedCount: updated.length,
1278
+ totalDocuments: totalDocuments + inserted.length
1279
+ };
1280
+
1281
+ if (inserted.length > 0) {
1282
+ if (inserted.length > 20) {
1283
+ responseData.firstId = inserted[0].id;
1284
+ responseData.lastId = inserted[inserted.length - 1].id;
1285
+ } else {
1286
+ responseData.insertedIds = inserted.map(d => d.id);
1287
+ }
1288
+ }
1289
+
1290
+ const duration = Utils.endTimer(start);
1291
+ const details = updated.length > 0
1292
+ ? `Inserted: ${inserted.length}, Updated: ${updated.length}`
1293
+ : `Inserted: ${inserted.length}`;
1294
+ Utils.logRequest('insert', details, duration, Utils.getDataSize(responseData));
1295
+
1296
+ return { data: responseData };
1297
+
1298
+ } catch (err) {
1299
+ Utils.logError('insert', err);
1300
+ return {
1301
+ error: {
1302
+ message: err.message || 'Failed to insert documents',
1303
+ code: 500
1304
+ }
1305
+ };
1306
+ }
1307
+ }
1308
+
1309
+ async update({ filters = {}, update = {}, options = {} } = {}) {
1310
+ try {
1311
+ const start = Utils.startTimer();
1312
+
1313
+ const normalizedUpdate = update.$set || update.$inc ||
1314
+ update.$push || update.$pull ||
1315
+ update.$unset || update.$addToSet
1316
+ ? update
1317
+ : { $set: update };
1318
+
1319
+ const returnType = options.returnType || 'count';
1320
+ const maxReturn = options.maxReturn ?? 50;
1321
+
1322
+ let updated = 0;
1323
+ const allUpdatedDocs = [];
1324
+
1325
+ const result = await this.cache.update(this.collectionName, async (data) => {
1326
+ for (let i = 0; i < data.documents.length; i++) {
1327
+ if (QueryEngine.matchesFilter(data.documents[i], filters)) {
1328
+ const before = returnType !== 'count'
1329
+ ? JSON.parse(JSON.stringify(data.documents[i]))
1330
+ : null;
1331
+
1332
+ QueryEngine.applyUpdateToDoc(data.documents[i], normalizedUpdate);
1333
+ data.documents[i].updatedAt = new Date().toISOString();
1334
+
1335
+ updated++;
1336
+
1337
+ if (returnType !== 'count') {
1338
+ allUpdatedDocs.push({
1339
+ before,
1340
+ after: data.documents[i]
1341
+ });
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ return {
1347
+ totalDocuments: data.documents.length,
1348
+ updated,
1349
+ allUpdatedDocs
1350
+ };
1351
+ });
1352
+
1353
+ let responseData = {
1354
+ updatedCount: result.updated,
1355
+ totalDocuments: result.totalDocuments
1356
+ };
1357
+
1358
+ if (returnType === 'ids' && allUpdatedDocs.length > 0) {
1359
+ const ids = allUpdatedDocs.map(item => item.after.id);
1360
+ responseData.updatedIds = ids.slice(0, maxReturn);
1361
+ if (ids.length > maxReturn) {
1362
+ responseData.truncated = true;
1363
+ responseData.maxReturn = maxReturn;
1364
+ }
1365
+ } else if (returnType === 'documents' && allUpdatedDocs.length > 0) {
1366
+ let documents = allUpdatedDocs.map(item => item.after);
1367
+
1368
+ if (options.fields) {
1369
+ documents = QueryEngine.selectFields(documents, options.fields);
1370
+ }
1371
+
1372
+ responseData.updatedDocuments = documents.slice(0, maxReturn);
1373
+ responseData.updatedDocuments = responseData.updatedDocuments;
1374
+
1375
+ if (documents.length > maxReturn) {
1376
+ responseData.truncated = true;
1377
+ responseData.maxReturn = maxReturn;
1378
+ }
1379
+ }
1380
+
1381
+ const duration = Utils.endTimer(start);
1382
+ const details = `Filters: ${Format.formatFilters(filters)} | Updated: ${updated} | ReturnType: ${returnType}`;
1383
+ Utils.logRequest('update', details, duration, Utils.getDataSize(responseData));
1384
+
1385
+ return { data: responseData };
1386
+
1387
+ } catch (err) {
1388
+ Utils.logError('update', err);
1389
+ return {
1390
+ error: {
1391
+ message: err.message || 'Failed to update documents',
1392
+ code: 500
1393
+ }
1394
+ };
1395
+ }
1396
+ }
1397
+
1398
+ async updateById(id, { update = {}, options = {} } = {}) {
1399
+ const start = Utils.startTimer();
1400
+ try {
1401
+ const returnType = options.returnType || 'document';
1402
+
1403
+ const updatedDoc = await this.cache.updateDocument(
1404
+ this.collectionName,
1405
+ id,
1406
+ (doc) => {
1407
+ const normalizedUpdate = Object.keys(update).some(k => k.startsWith('$'))
1408
+ ? update
1409
+ : { $set: update };
1410
+
1411
+ QueryEngine.applyUpdateToDoc(doc, normalizedUpdate);
1412
+ doc.updatedAt = new Date().toISOString();
1413
+ return doc;
1414
+ }
1415
+ );
1416
+
1417
+ const responseData = {
1418
+ updatedCount: 1,
1419
+ updatedId: id
1420
+ };
1421
+
1422
+ if (returnType === 'document') {
1423
+ let returnedDoc = updatedDoc;
1424
+ if (options.fields) {
1425
+ returnedDoc = QueryEngine.selectFields(updatedDoc, options.fields);
1426
+ }
1427
+ responseData.document = returnedDoc;
1428
+ }
1429
+
1430
+ const duration = Utils.endTimer(start);
1431
+ const details = `ID: ${id} | ReturnType: ${returnType} | Updated: 1`;
1432
+ Utils.logRequest('update_By_Id', details, duration, Utils.getDataSize(responseData));
1433
+
1434
+ return { data: responseData };
1435
+
1436
+ } catch (err) {
1437
+ Utils.logError('update_By_Id', err);
1438
+ return {
1439
+ error: {
1440
+ message: err.message || 'Failed to update document by ID',
1441
+ code: 500
1442
+ }
1443
+ };
1444
+ }
1445
+ }
1446
+
1447
+ async delete({ filters = {} }) {
1448
+
1449
+ if (filters === undefined || Object.keys(filters).length === 0) {
1450
+ throw new Error(
1451
+ `Filters can't be empty.` +
1452
+ `To clear collection, please use .drop() or dropCollection() method.`
1453
+ );
1454
+
1455
+ }
1456
+ try {
1457
+ const start = Utils.startTimer();
1458
+ let deletedCount = 0;
1459
+
1460
+ await this.cache.update(this.collectionName, async (data) => {
1461
+ const before = data.documents.length;
1462
+
1463
+ const documentsToKeep = data.documents.filter(
1464
+ document => !QueryEngine.matchesFilter(document, filters)
1465
+ );
1466
+
1467
+ deletedCount = before - documentsToKeep.length;
1468
+
1469
+ if (deletedCount > 0) {
1470
+ data.documents = documentsToKeep;
1471
+
1472
+ data.idIndex.clear();
1473
+ documentsToKeep.forEach((doc, idx) => {
1474
+ if (doc.id !== undefined) {
1475
+ data.idIndex.set(doc.id, idx);
1476
+ }
1477
+ });
1478
+ }
1479
+ });
1480
+
1481
+ const duration = Utils.endTimer(start);
1482
+ const details = deletedCount === 0
1483
+ ? `Filters: ${Format.formatFilters(filters)} | No documents matched`
1484
+ : `Filters: ${Format.formatFilters(filters)} | Deleted: ${deletedCount}`;
1485
+
1486
+ Utils.logRequest('delete', details, duration, Utils.getDataSize({ deletedCount }));
1487
+
1488
+ return {
1489
+ data: {
1490
+ collectionName: this.collectionName,
1491
+ deletedCount,
1492
+ ...(deletedCount === 0 && { details: 'No documents matched filters' })
1493
+ }
1494
+ };
1495
+
1496
+ } catch (err) {
1497
+ Utils.logError('delete', err);
1498
+ return {
1499
+ error: {
1500
+ message: err.message || 'Failed to delete documents',
1501
+ code: 500
1502
+ }
1503
+ };
1504
+ }
1505
+ }
1506
+
1507
+ async deleteById(id) {
1508
+ const start = Utils.startTimer();
1509
+ try {
1510
+ const deleted = await this.cache.removeDocument(this.collectionName, id);
1511
+
1512
+ const duration = Utils.endTimer(start);
1513
+
1514
+ if (!deleted) {
1515
+ Utils.logRequest('delete_By_Id', `ID: ${id} | Not found`, duration);
1516
+ return {
1517
+ data: {
1518
+ deletedCount: 0,
1519
+ details: 'Document not found'
1520
+ }
1521
+ };
1522
+ }
1523
+
1524
+ Utils.logRequest('delete_By_Id', `ID: ${id} | Deleted`, duration);
1525
+
1526
+ return {
1527
+ data: {
1528
+ deletedCount: 1,
1529
+ deletedId: id
1530
+ }
1531
+ };
1532
+
1533
+ } catch (err) {
1534
+ Utils.logError('delete_By_Id', err);
1535
+ return {
1536
+ error: {
1537
+ message: err.message || 'Failed to delete document by ID',
1538
+ code: 500
1539
+ }
1540
+ };
1541
+ }
1542
+ }
1543
+
1544
+ async dropCollection() {
1545
+ try {
1546
+ const start = Utils.startTimer();
1547
+
1548
+ const data = this.cache.cache.get(this.collectionName);
1549
+ if (data) {
1550
+ if (data.dirty) {
1551
+ await this.cache.save(this.collectionName);
1552
+ }
1553
+
1554
+ this.cache.cache.delete(this.collectionName);
1555
+ this.cache.dirty.delete(this.collectionName);
1556
+ this.cache.locks.delete(this.collectionName);
1557
+ }
1558
+
1559
+ const filePath = path.join(this.storagePath, `${this.collectionName}.json`);
1560
+ try {
1561
+ await fs.unlink(filePath);
1562
+ } catch (err) {
1563
+ if (err.code !== 'ENOENT') {
1564
+ throw err;
1565
+ }
1566
+ }
1567
+
1568
+ const duration = Utils.endTimer(start);
1569
+ Utils.logRequest('dropCollection', 'Success', duration);
1570
+
1571
+ return {
1572
+ data: {
1573
+ collectionName: this.collectionName,
1574
+ dropped: true
1575
+ }
1576
+ };
1577
+
1578
+ } catch (err) {
1579
+ Utils.logError('dropCollection', err);
1580
+ return {
1581
+ error: {
1582
+ message: err.message || 'Unexpected error during dropCollection',
1583
+ code: 500
1584
+ }
1585
+ };
1586
+ }
1587
+ }
1588
+
1589
+ async close() {
1590
+ await this.cache.flushAll();
1591
+ return true;
1592
+ }
1593
+ }
1594
+
1595
+ class Collection {
1596
+ constructor(adapter, collectionName) {
1597
+ this.adapter = adapter;
1598
+ this.collectionName = collectionName;
1599
+ }
1600
+
1601
+ async count(filters = {}) {
1602
+ Validator.validateFilters(filters);
1603
+
1604
+ return this.adapter.request('GET', `/collections/${encodeURIComponent(this.collectionName)}/count`, {
1605
+ filters
1606
+ });
1607
+ }
1608
+
1609
+ async find(filters = {}, options = {}) {
1610
+ Validator.validateFilters(filters);
1611
+ Validator.validateOptions(options);
1612
+
1613
+ return this.adapter.request('GET', `/collections/${encodeURIComponent(this.collectionName)}`, {
1614
+ filters,
1615
+ options
1616
+ });
1617
+ }
1618
+
1619
+ async findById(id, options = {}) {
1620
+ Validator.validateOptions(options);
1621
+
1622
+ return this.adapter.request('GET', `/collections/${encodeURIComponent(this.collectionName)}/${id}`, {
1623
+ options
1624
+ });
1625
+ }
1626
+
1627
+ async findOne(filters = {}, options = {}) {
1628
+ Validator.validateFilters(filters);
1629
+ Validator.validateOptions(options);
1630
+
1631
+ try {
1632
+ const { error, data } = await this.adapter.request('GET', `/collections/${encodeURIComponent(this.collectionName)}`, {
1633
+ filters,
1634
+ options: { limit: 1, sort: { updatedAt: -1 }, ...options }
1635
+ });
1636
+
1637
+ if (error) return { error };
1638
+
1639
+ if (data.foundDocuments.length > 0) {
1640
+ return {
1641
+ data: data.foundDocuments[0]
1642
+ };
1643
+ }
1644
+
1645
+ return {
1646
+ data: null
1647
+ };
1648
+
1649
+ } catch (err) {
1650
+ console.error(err);
1651
+ return {
1652
+ error: {
1653
+ message: err.message || 'Failed to find one document',
1654
+ code: 500
1655
+ }
1656
+ };
1657
+ }
1658
+ }
1659
+
1660
+ async insert(documents) {
1661
+ return this.adapter.request('POST', `/collections/${encodeURIComponent(this.collectionName)}`, {
1662
+ documents
1663
+ });
1664
+ }
1665
+
1666
+ async update(filters = {}, update = {}, options = {}) {
1667
+ Validator.validateFilters(filters);
1668
+ Validator.validateOptions(options);
1669
+
1670
+ return this.adapter.request('PATCH', `/collections/${encodeURIComponent(this.collectionName)}`, {
1671
+ filters,
1672
+ update,
1673
+ options
1674
+ });
1675
+ }
1676
+
1677
+ async updateById(id, update = {}, options = {}) {
1678
+ Validator.validateOptions(options);
1679
+
1680
+ return this.adapter.request('PATCH', `/collections/${encodeURIComponent(this.collectionName)}/${id}`, {
1681
+ update,
1682
+ options
1683
+ });
1684
+ }
1685
+
1686
+ async delete(filters) {
1687
+ if (!filters) {
1688
+ throw new Error('Delete operation requires filters to prevent accidental deletion of all documents. {} or use .drop() to delete entire collection.');
1689
+ }
1690
+
1691
+ Validator.validateFilters(filters);
1692
+
1693
+ return this.adapter.request('DELETE', `/collections/${encodeURIComponent(this.collectionName)}`, {
1694
+ filters
1695
+ });
1696
+ }
1697
+
1698
+ async deleteById(id) {
1699
+ if (!id) {
1700
+ throw new Error('deleteById operation requires a valid document ID.');
1701
+ }
1702
+ return this.adapter.request('DELETE', `/collections/${encodeURIComponent(this.collectionName)}/${id}`);
1703
+ }
1704
+
1705
+ async drop() {
1706
+ return this.adapter.request('DELETE', `/collections/${encodeURIComponent(this.collectionName)}/drop`);
1707
+ }
1708
+ }
1709
+
1710
+ class LiekoDB {
1711
+ constructor(options = {}) {
1712
+ this.debug = options.debug || false;
1713
+
1714
+ const isBrowser =
1715
+ typeof window !== 'undefined' &&
1716
+ typeof window.document !== 'undefined';
1717
+
1718
+ if (isBrowser && !options.token) {
1719
+ throw new Error(
1720
+ 'LiekoDB: A token is required when used in a browser environment. ' +
1721
+ 'LocalAdapter is only available in Node.js.'
1722
+ );
1723
+ }
1724
+ this.adapter = this._createAdapter(options);
1725
+ }
1726
+
1727
+ _createAdapter(options) {
1728
+ if (options.token) {
1729
+ return new HTTPAdapter(options);
1730
+ }
1731
+
1732
+ if (typeof process !== 'undefined' && process.versions?.node) {
1733
+ return new LocalAdapter(options);
1734
+ }
1735
+
1736
+ throw new Error(
1737
+ 'LiekoDB: LocalAdapter cannot be used outside of Node.js'
1738
+ );
1739
+ }
1740
+
1741
+ collection(name) {
1742
+ Validator.validateCollectionName(name);
1743
+ return new Collection(this.adapter, name);
1744
+ }
1745
+
1746
+ async listCollections() {
1747
+ return this.adapter.listCollections();
1748
+ }
1749
+
1750
+ async dropCollection(name) {
1751
+ Validator.validateCollectionName(name);
1752
+ return this.adapter.dropCollection(name);
1753
+ }
1754
+
1755
+ async status() {
1756
+ return this.adapter.status();
1757
+ }
1758
+
1759
+ async close() {
1760
+ return this.adapter.close();
1761
+ }
1762
+ }
1763
+
1764
+ class Format {
1765
+
1766
+ static formatBytes(bytes) {
1767
+ if (bytes === 0) return '0 B';
1768
+ if (bytes < 1024) return `${bytes} B`;
1769
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
1770
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
1771
+ }
1772
+
1773
+ static formatDuration(ms) {
1774
+ if (ms < 0.001) return `${(ms * 1000).toFixed(2)} µs`;
1775
+ if (ms < 1) return `${(ms * 1000).toFixed(0)} µs`;
1776
+ if (ms < 1000) return `${ms.toFixed(2)} ms`;
1777
+ return `${(ms / 1000).toFixed(2)} s`;
1778
+ }
1779
+
1780
+ /**
1781
+ * TODO: when filter is regex, log is only {}
1782
+ * {{ email: { '$regex': /@google\.com$/ } }
1783
+ * [LiekoDB] FIND users | Duration: 101µs | Response Size: 175 B | Filters: {email:{$regex:{}}} | Found: 1}
1784
+ */
1785
+ static formatFilters(filters) {
1786
+ if (!filters || Object.keys(filters).length === 0) return '{}';
1787
+ const formatted = JSON.stringify(filters, null, 0)
1788
+ .replace(/"/g, '')
1789
+ .replace(/,/g, ', ');
1790
+ if (formatted.length > 80) {
1791
+ return formatted.substring(0, 77) + '...';
1792
+ }
1793
+ return formatted;
1794
+ }
1795
+
1796
+ static formatOptions(options) {
1797
+ if (!options || Object.keys(options).length === 0) return '';
1798
+ const parts = [];
1799
+ if (options.sort) parts.push(`sort: ${JSON.stringify(options.sort).replace(/"/g, '')}`);
1800
+ if (options.limit) parts.push(`limit: ${options.limit}`);
1801
+ if (options.skip) parts.push(`skip: ${options.skip}`);
1802
+ if (options.fields) parts.push(`fields: ${JSON.stringify(options.fields).replace(/"/g, '')}`);
1803
+ return parts.length > 0 ? ` | ${parts.join(', ')}` : '';
1804
+ }
1805
+
1806
+ static formatFieldsLog(fields) {
1807
+ if (!fields || typeof fields !== 'object') return '';
1808
+
1809
+ const formatted = Object.entries(fields)
1810
+ .map(([key, value]) => {
1811
+ if (value === 1 || value === true) return `+${key}`;
1812
+ if (value === -1 || value === false) return `-${key}`;
1813
+ return `${key}:${value}`;
1814
+ })
1815
+ .join(', ');
1816
+
1817
+ return `Fields: {${formatted}}`;
1818
+ }
1819
+ }
1820
+
1821
+ class Validator {
1822
+
1823
+ static validateCollectionName(name) {
1824
+ if (!name || typeof name !== 'string') {
1825
+ throw new Error(`Collection name must be a non-empty string, got: ${typeof name}`);
1826
+ }
1827
+
1828
+ if (name.length < 1) {
1829
+ throw new Error('Collection name cannot be empty');
1830
+ }
1831
+
1832
+ if (name.length > 64) {
1833
+ throw new Error(`Collection name too long (${name.length} > 64 characters)`);
1834
+ }
1835
+
1836
+ const validNameRegex = /^[a-zA-Z0-9_-]+$/;
1837
+ if (!validNameRegex.test(name)) {
1838
+ throw new Error(
1839
+ `Invalid collection name: "${name}". ` +
1840
+ `Only alphanumeric characters, underscores (_) and hyphens (-) are allowed.`
1841
+ );
1842
+ }
1843
+
1844
+ if (/^[0-9_-]/.test(name)) {
1845
+ throw new Error('Collection name cannot start with a number, underscore or hyphen');
1846
+ }
1847
+
1848
+ const invalidPatterns = [
1849
+ /\.\./,
1850
+ /\/|\\/,
1851
+ /^\./,
1852
+ /\s/,
1853
+ /[<>:"|?*]/,
1854
+ ];
1855
+
1856
+ for (const pattern of invalidPatterns) {
1857
+ if (pattern.test(name)) {
1858
+ throw new Error(`Collection name contains invalid characters: "${name}"`);
1859
+ }
1860
+ }
1861
+
1862
+ return true;
1863
+ }
1864
+
1865
+ static validateFilters(filters) {
1866
+ if (filters == null || typeof filters !== 'object' || Array.isArray(filters)) {
1867
+ throw new Error('Filters must be a non-null plain object');
1868
+ }
1869
+
1870
+ const validOperators = ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin',
1871
+ '$exists', '$regex', '$and', '$or', '$not'];
1872
+
1873
+ for (const key in filters) {
1874
+ if (key.startsWith('$') && !validOperators.includes(key)) {
1875
+ throw new Error(`Invalid query operator: >> ${key} << \nValid operators: ${validOperators.join(', ')}`);
1876
+ }
1877
+
1878
+ const value = filters[key];
1879
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1880
+ this.validateFilters(value);
1881
+ } else if (Array.isArray(value) && (key === '$and' || key === '$or' || key === '$in' || key === '$nin')) {
1882
+ value.forEach(item => {
1883
+ if (typeof item === 'object' && item !== null) {
1884
+ this.validateFilters(item);
1885
+ }
1886
+ });
1887
+ }
1888
+ }
1889
+ }
1890
+
1891
+ static validateOptions(options) {
1892
+ if (options == null || typeof options !== 'object' || Array.isArray(options)) {
1893
+ throw new Error('Options must be a non-null plain object');
1894
+ }
1895
+
1896
+ const validKeys = ['sort', 'skip', 'limit', 'fields', 'page', 'returnType', 'maxReturn'];
1897
+
1898
+ for (const key in options) {
1899
+ if (!validKeys.includes(key)) {
1900
+ throw new Error(`Invalid option key: >> ${key} << \nValid keys: ${validKeys.join(', ')}`);
1901
+ }
1902
+
1903
+ if (key === 'sort') {
1904
+ if (typeof options[key] !== 'object' || options[key] == null || Array.isArray(options[key])) {
1905
+ throw new Error('Sort must be a non-null plain object');
1906
+ }
1907
+ for (const sortKey in options[key]) {
1908
+ const val = options[key][sortKey];
1909
+ if (![1, -1, true, false].includes(val)) {
1910
+ throw new Error(`Invalid sort value for ${sortKey}: ${val}. Must be 1, -1, true, or false`);
1911
+ }
1912
+ }
1913
+ } else if (key === 'skip') {
1914
+ if (typeof options[key] !== 'number' || options[key] < 0) {
1915
+ throw new Error('Skip must be a non-negative number');
1916
+ }
1917
+ } else if (key === 'limit') {
1918
+ if (typeof options[key] === 'number') {
1919
+ if (options[key] < 0) {
1920
+ throw new Error('Limit must be a non-negative number');
1921
+ }
1922
+ } else {
1923
+ throw new Error('Limit must be a non-negative number');
1924
+ }
1925
+ } else if (key === 'fields') {
1926
+ if (typeof options[key] !== 'object' || options[key] == null || Array.isArray(options[key])) {
1927
+ throw new Error('Fields must be a non-null plain object');
1928
+ }
1929
+ for (const fieldKey in options[key]) {
1930
+ const val = options[key][fieldKey];
1931
+ if (![1, -1, true, false].includes(val)) {
1932
+ throw new Error(`Invalid field value for ${fieldKey}: ${val}. Must be 1, -1, true, or false`);
1933
+ }
1934
+ }
1935
+ } else if (key === 'page') {
1936
+ if (typeof options[key] !== 'number' || options[key] <= 0) {
1937
+ throw new Error('Page must be a positive number');
1938
+ }
1939
+ } else if (key === 'returnType') {
1940
+ const validTypes = ['count', 'ids', 'documents'];
1941
+ if (!validTypes.includes(options[key])) {
1942
+ throw new Error(
1943
+ `Invalid returnType: "${options[key]}". ` +
1944
+ `Must be one of: ${validTypes.join(', ')}`
1945
+ );
1946
+ }
1947
+ } else if (key === 'maxReturn') {
1948
+ if (typeof options[key] !== 'number' || options[key] < 0 || !Number.isInteger(options[key])) {
1949
+ throw new Error('maxReturn must be a non-negative integer');
1950
+ }
1951
+ }
1952
+ }
1953
+ }
1954
+ }
1955
+
1956
+ class Utils {
1957
+ static startTimer() {
1958
+ return process.hrtime.bigint();
1959
+ }
1960
+
1961
+ static endTimer(start) {
1962
+ const end = process.hrtime.bigint();
1963
+ const diffNs = end - start;
1964
+ return Number(diffNs) / 1_000_000; // ms
1965
+ }
1966
+
1967
+ static generateId() {
1968
+ return require('crypto').randomBytes(8).toString('hex');
1969
+ }
1970
+
1971
+ static log(...args) {
1972
+ if (this.debug) console.log('[LiekoDB]', ...args);
1973
+ }
1974
+
1975
+ static logRequest(operation, details, duration, responseSize) {
1976
+ if (!this.debug) return;
1977
+
1978
+ const durationFormatted = Format.formatDuration(duration);
1979
+
1980
+ let sizePart = '';
1981
+ if (typeof responseSize === 'number' && Number.isFinite(responseSize) && responseSize > 0) {
1982
+ sizePart = ` | Response Size: ${Format.formatBytes(responseSize)}`;
1983
+ }
1984
+
1985
+ this.log(
1986
+ `${operation.toUpperCase()} | Collection: ${this.collectionName} | ` +
1987
+ `Duration: ${durationFormatted}` +
1988
+ sizePart +
1989
+ (details ? ` | ${details}` : '')
1990
+ );
1991
+ }
1992
+
1993
+ static logError(operation, err) {
1994
+ console.error(
1995
+ `[LiekoDB] ERROR - ${operation.toUpperCase()} | Collection: ${this.collectionName}` +
1996
+ (err ? ` | ${err}` : '')
1997
+ );
1998
+ console.error(err)
1999
+ }
2000
+
2001
+ static getDataSize(data) {
2002
+ try {
2003
+ return Buffer.byteLength(JSON.stringify(data), 'utf8');
2004
+ } catch (e) {
2005
+ return 0;
2006
+ }
2007
+ }
2008
+ }
2009
+
2010
+ module.exports = LiekoDB;
2011
+
2012
+ if (typeof window !== 'undefined') {
2013
+ window.LiekoDB = LiekoDB;
2014
+ }