jexidb 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,598 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "FileHandler", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _FileHandler.default;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "IndexManager", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _IndexManager.default;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "IntegrityChecker", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _IntegrityChecker.default;
22
+ }
23
+ });
24
+ exports.utils = exports.default = exports.OPERATORS = void 0;
25
+ var _JSONLDatabase = _interopRequireDefault(require("./JSONLDatabase.js"));
26
+ var _FileHandler = _interopRequireDefault(require("./FileHandler.js"));
27
+ var _IndexManager = _interopRequireDefault(require("./IndexManager.js"));
28
+ var _IntegrityChecker = _interopRequireDefault(require("./IntegrityChecker.js"));
29
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
30
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
31
+ /**
32
+ * JexiDB Compatibility Wrapper
33
+ * Extends JSONLDatabase to provide backward compatibility with JexiDB 1.x test expectations
34
+ */
35
+ class JexiDBCompatibility extends _JSONLDatabase.default {
36
+ constructor(filePath, options = {}) {
37
+ // Support both .jdb and .jsonl extensions
38
+ // .jdb is the preferred extension for JexiDB databases
39
+ let normalizedPath = filePath;
40
+
41
+ // If no extension is provided, default to .jdb
42
+ if (!filePath.includes('.')) {
43
+ normalizedPath = filePath + '.jdb';
44
+ }
45
+
46
+ // If .jdb extension is used, it's internally stored as JSONL format
47
+ // but the user sees .jdb for better branding
48
+ if (normalizedPath.endsWith('.jdb')) {
49
+ // Store internally as .jsonl but present as .jdb to user
50
+ const jsonlPath = normalizedPath.replace('.jdb', '.jsonl');
51
+ super(jsonlPath, options);
52
+ this.userPath = normalizedPath; // Keep track of user's preferred path
53
+ } else {
54
+ super(normalizedPath, options);
55
+ this.userPath = normalizedPath;
56
+ }
57
+ this.isDestroyed = false;
58
+ }
59
+
60
+ /**
61
+ * Get the user's preferred file path (with .jdb extension if used)
62
+ */
63
+ get userFilePath() {
64
+ return this.userPath;
65
+ }
66
+
67
+ /**
68
+ * Compatibility method: destroy() -> close()
69
+ */
70
+ async destroy() {
71
+ this.isDestroyed = true;
72
+ return this.close();
73
+ }
74
+
75
+ /**
76
+ * Compatibility method: findOne() -> find() with limit 1
77
+ */
78
+ async findOne(criteria = {}) {
79
+ const results = await this.find(criteria, {
80
+ limit: 1
81
+ });
82
+ return results.length > 0 ? results[0] : null;
83
+ }
84
+
85
+ /**
86
+ * Enhanced find method with options support
87
+ */
88
+ async find(criteria = {}, options = {}) {
89
+ let results = await super.find(criteria);
90
+
91
+ // Apply sorting
92
+ if (options.sort) {
93
+ results = this.sortResults(results, options.sort);
94
+ }
95
+
96
+ // Apply skip
97
+ if (options.skip) {
98
+ results = results.slice(options.skip);
99
+ }
100
+
101
+ // Apply limit
102
+ if (options.limit) {
103
+ results = results.slice(0, options.limit);
104
+ }
105
+ return results;
106
+ }
107
+
108
+ /**
109
+ * Retrocompatibility method: query() -> find()
110
+ * Supports the same API as JexiDB 1.x
111
+ */
112
+ async query(criteria = {}, options = {}) {
113
+ // Handle caseInsensitive option from JexiDB 1.x
114
+ if (options.caseInsensitive) {
115
+ // For case insensitive queries, we need to modify the criteria
116
+ const caseInsensitiveCriteria = {};
117
+ for (const [key, value] of Object.entries(criteria)) {
118
+ if (typeof value === 'string') {
119
+ // Convert string values to regex for case insensitive matching
120
+ caseInsensitiveCriteria[key] = {
121
+ $regex: value,
122
+ $options: 'i'
123
+ };
124
+ } else {
125
+ caseInsensitiveCriteria[key] = value;
126
+ }
127
+ }
128
+ criteria = caseInsensitiveCriteria;
129
+ }
130
+ return await this.find(criteria, options);
131
+ }
132
+
133
+ /**
134
+ * Sort results based on criteria
135
+ */
136
+ sortResults(results, sortCriteria) {
137
+ return results.sort((a, b) => {
138
+ for (const [field, direction] of Object.entries(sortCriteria)) {
139
+ const aValue = this.getNestedValue(a, field);
140
+ const bValue = this.getNestedValue(b, field);
141
+ if (aValue < bValue) return direction === 1 ? -1 : 1;
142
+ if (aValue > bValue) return direction === 1 ? 1 : -1;
143
+ }
144
+ return 0;
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Get nested value from record (copied from parent class)
150
+ */
151
+ getNestedValue(record, field) {
152
+ const parts = field.split('.');
153
+ let value = record;
154
+ for (const part of parts) {
155
+ if (value && typeof value === 'object' && part in value) {
156
+ value = value[part];
157
+ } else {
158
+ return undefined;
159
+ }
160
+ }
161
+ return value;
162
+ }
163
+
164
+ /**
165
+ * Compatibility method: insertMany() -> multiple insert() calls
166
+ */
167
+ async insertMany(records) {
168
+ const results = [];
169
+ for (const record of records) {
170
+ const result = await this.insert(record);
171
+ results.push(result);
172
+ }
173
+ return results;
174
+ }
175
+
176
+ /**
177
+ * Override insert to add _updated field for compatibility
178
+ */
179
+ async insert(data) {
180
+ const record = await super.insert(data);
181
+ record._updated = record._created; // Set _updated to same as _created for new records
182
+ return record;
183
+ }
184
+
185
+ /**
186
+ * Compatibility method: count() -> find() with length
187
+ */
188
+ async count(criteria = {}) {
189
+ const results = await this.find(criteria);
190
+ return results.length;
191
+ }
192
+
193
+ /**
194
+ * Compatibility method: getStats() -> stats getter
195
+ */
196
+ async getStats() {
197
+ // Call the parent class getStats method to get actual file size
198
+ return await super.getStats();
199
+ }
200
+
201
+ /**
202
+ * Compatibility method: validateIntegrity() -> basic validation
203
+ */
204
+ async validateIntegrity() {
205
+ // Call the parent class validateIntegrity method to get actual file integrity check
206
+ return await super.validateIntegrity();
207
+ }
208
+
209
+ /**
210
+ * Compatibility method: walk() -> find() with async iteration
211
+ */
212
+ async *walk(options = {}) {
213
+ const results = await this.find({}, options);
214
+ for (const record of results) {
215
+ yield record;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Compatibility property: indexStats
221
+ */
222
+ get indexStats() {
223
+ const stats = this.stats;
224
+ return {
225
+ recordCount: stats.recordCount,
226
+ indexCount: stats.indexedFields.length
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Override update to return array format for compatibility
232
+ */
233
+ async update(criteria, updates) {
234
+ const result = await super.update(criteria, updates);
235
+ // Convert { updatedCount: n } to array format for tests
236
+ if (typeof result === 'object' && result.updatedCount !== undefined) {
237
+ const updatedRecords = await this.find(criteria);
238
+ return updatedRecords;
239
+ }
240
+ return result;
241
+ }
242
+
243
+ /**
244
+ * Override delete to return number format for compatibility
245
+ */
246
+ async delete(criteria) {
247
+ const result = await super.delete(criteria);
248
+ // Convert { deletedCount: n } to number format for tests
249
+ if (typeof result === 'object' && result.deletedCount !== undefined) {
250
+ return result.deletedCount;
251
+ }
252
+ return result;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * JexiDB - Robust JSONL database
258
+ * Complete rewrite of JexiDB with JSONL architecture, fixing all critical bugs from version 1.x
259
+ *
260
+ * Features:
261
+ * - One file per table (pure JSONL)
262
+ * - Punctual reading (doesn't load everything in memory)
263
+ * - In-memory indexes for performance
264
+ * - Safe truncation after operations
265
+ * - Integrity validation
266
+ * - Event-driven architecture
267
+ *
268
+ * API similar to JexiDB 1.x:
269
+ * - new JexiDB('file.jsonl', { indexes: { id: 'number' } })
270
+ * - await db.init()
271
+ * - await db.insert({ id: 1, name: 'John' })
272
+ * - await db.find({ id: 1 })
273
+ * - await db.update({ id: 1 }, { name: 'John Smith' })
274
+ * - await db.delete({ id: 1 })
275
+ * - await db.save()
276
+ * - await db.destroy()
277
+ * - await db.walk() - Iterator for large volumes
278
+ * - await db.validateIntegrity() - Manual verification
279
+ */
280
+
281
+ // Export the compatibility wrapper as default
282
+ var _default = exports.default = JexiDBCompatibility; // Export auxiliary classes for advanced use
283
+ // Export useful constants
284
+ const OPERATORS = exports.OPERATORS = {
285
+ GT: '>',
286
+ GTE: '>=',
287
+ LT: '<',
288
+ LTE: '<=',
289
+ NE: '!=',
290
+ IN: 'in',
291
+ NIN: 'nin',
292
+ REGEX: 'regex',
293
+ CONTAINS: 'contains'
294
+ };
295
+
296
+ // Export utility functions
297
+ const utils = exports.utils = {
298
+ /**
299
+ * Creates a database with default settings
300
+ */
301
+ createDatabase(filePath, indexes = {}) {
302
+ return new _JSONLDatabase.default(filePath, {
303
+ indexes
304
+ });
305
+ },
306
+ /**
307
+ * Validates if a JSONL file is valid
308
+ * @param {string} filePath - Path to the JSONL file
309
+ * @returns {Promise<Object>} Validation result with errors and line count
310
+ */
311
+ async validateJSONLFile(filePath) {
312
+ const {
313
+ promises: fs
314
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
315
+ const readline = await Promise.resolve().then(() => _interopRequireWildcard(require('readline')));
316
+ const {
317
+ createReadStream
318
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
319
+ try {
320
+ const fileStream = createReadStream(filePath);
321
+ const rl = readline.createInterface({
322
+ input: fileStream,
323
+ crlfDelay: Infinity
324
+ });
325
+ let lineNumber = 0;
326
+ const errors = [];
327
+ for await (const line of rl) {
328
+ lineNumber++;
329
+ if (line.trim() !== '') {
330
+ try {
331
+ JSON.parse(line);
332
+ } catch (error) {
333
+ errors.push(`Line ${lineNumber}: ${error.message}`);
334
+ }
335
+ }
336
+ }
337
+ return {
338
+ isValid: errors.length === 0,
339
+ errors,
340
+ lineCount: lineNumber
341
+ };
342
+ } catch (error) {
343
+ return {
344
+ isValid: false,
345
+ errors: [`Error reading file: ${error.message}`],
346
+ lineCount: 0
347
+ };
348
+ }
349
+ },
350
+ /**
351
+ * Converts a JSON file to JSONL (basic conversion)
352
+ * @param {string} jsonFilePath - Path to the JSON file
353
+ * @param {string} jsonlFilePath - Path to the output JSONL file
354
+ * @returns {Promise<Object>} Conversion result
355
+ */
356
+ async convertJSONToJSONL(jsonFilePath, jsonlFilePath) {
357
+ const {
358
+ promises: fs
359
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
360
+ try {
361
+ const jsonData = await fs.readFile(jsonFilePath, 'utf8');
362
+ const data = JSON.parse(jsonData);
363
+ const records = Array.isArray(data) ? data : [data];
364
+ const jsonlContent = records.map(record => JSON.stringify(record)).join('\n') + '\n';
365
+ await fs.writeFile(jsonlFilePath, jsonlContent, 'utf8');
366
+ return {
367
+ success: true,
368
+ recordCount: records.length
369
+ };
370
+ } catch (error) {
371
+ return {
372
+ success: false,
373
+ error: error.message
374
+ };
375
+ }
376
+ },
377
+ /**
378
+ * Converts a JSONL file to JSON
379
+ * @param {string} jsonlFilePath - Path to the JSONL file
380
+ * @param {string} jsonFilePath - Path to the output JSON file
381
+ * @returns {Promise<Object>} Conversion result
382
+ */
383
+ async convertJSONLToJSON(jsonlFilePath, jsonFilePath) {
384
+ const {
385
+ promises: fs
386
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
387
+ const readline = await Promise.resolve().then(() => _interopRequireWildcard(require('readline')));
388
+ const {
389
+ createReadStream
390
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
391
+ try {
392
+ const fileStream = createReadStream(jsonlFilePath);
393
+ const rl = readline.createInterface({
394
+ input: fileStream,
395
+ crlfDelay: Infinity
396
+ });
397
+ const records = [];
398
+ for await (const line of rl) {
399
+ if (line.trim() !== '') {
400
+ try {
401
+ const record = JSON.parse(line);
402
+ records.push(record);
403
+ } catch (error) {
404
+ console.warn(`Line ignored: ${error.message}`);
405
+ }
406
+ }
407
+ }
408
+ const jsonContent = JSON.stringify(records, null, 2);
409
+ await fs.writeFile(jsonFilePath, jsonContent, 'utf8');
410
+ return {
411
+ success: true,
412
+ recordCount: records.length
413
+ };
414
+ } catch (error) {
415
+ return {
416
+ success: false,
417
+ error: error.message
418
+ };
419
+ }
420
+ },
421
+ /**
422
+ * Creates a JexiDB database from a JSON file with automatic index detection
423
+ * @param {string} jsonFilePath - Path to the JSON file
424
+ * @param {string} dbFilePath - Path to the output JexiDB file
425
+ * @param {Object} options - Options for database creation
426
+ * @param {Object} options.indexes - Manual index configuration
427
+ * @param {boolean} options.autoDetectIndexes - Auto-detect common index fields (default: true)
428
+ * @param {Array<string>} options.autoIndexFields - Fields to auto-index (default: ['id', '_id', 'email', 'name'])
429
+ * @returns {Promise<Object>} Database creation result
430
+ */
431
+ async createDatabaseFromJSON(jsonFilePath, dbFilePath, options = {}) {
432
+ const {
433
+ promises: fs
434
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
435
+ try {
436
+ // Read JSON data
437
+ const jsonData = await fs.readFile(jsonFilePath, 'utf8');
438
+ const data = JSON.parse(jsonData);
439
+ const records = Array.isArray(data) ? data : [data];
440
+ if (records.length === 0) {
441
+ return {
442
+ success: false,
443
+ error: 'No records found in JSON file'
444
+ };
445
+ }
446
+
447
+ // Auto-detect indexes if enabled
448
+ let indexes = options.indexes || {};
449
+ if (options.autoDetectIndexes !== false) {
450
+ const autoIndexFields = options.autoIndexFields || ['id', '_id', 'email', 'name', 'username'];
451
+ const sampleRecord = records[0];
452
+ for (const field of autoIndexFields) {
453
+ if (sampleRecord.hasOwnProperty(field)) {
454
+ const value = sampleRecord[field];
455
+ if (typeof value === 'number') {
456
+ indexes[field] = 'number';
457
+ } else if (typeof value === 'string') {
458
+ indexes[field] = 'string';
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ // Create database
465
+ const db = new _JSONLDatabase.default(dbFilePath, {
466
+ indexes,
467
+ autoSave: false,
468
+ validateOnInit: false
469
+ });
470
+ await db.init();
471
+
472
+ // Insert all records
473
+ for (const record of records) {
474
+ await db.insert(record);
475
+ }
476
+
477
+ // Save and close the database
478
+ await db.save();
479
+ await db.close();
480
+ return {
481
+ success: true,
482
+ recordCount: records.length,
483
+ indexes: Object.keys(indexes),
484
+ dbPath: dbFilePath
485
+ };
486
+ } catch (error) {
487
+ return {
488
+ success: false,
489
+ error: error.message
490
+ };
491
+ }
492
+ },
493
+ /**
494
+ * Analyzes a JSON file and suggests optimal indexes
495
+ * @param {string} jsonFilePath - Path to the JSON file
496
+ * @param {number} sampleSize - Number of records to analyze (default: 100)
497
+ * @returns {Promise<Object>} Index suggestions
498
+ */
499
+ async analyzeJSONForIndexes(jsonFilePath, sampleSize = 100) {
500
+ const {
501
+ promises: fs
502
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
503
+ try {
504
+ const jsonData = await fs.readFile(jsonFilePath, 'utf8');
505
+ const data = JSON.parse(jsonData);
506
+ const records = Array.isArray(data) ? data : [data];
507
+ if (records.length === 0) {
508
+ return {
509
+ success: false,
510
+ error: 'No records found in JSON file'
511
+ };
512
+ }
513
+
514
+ // Analyze sample records
515
+ const sample = records.slice(0, Math.min(sampleSize, records.length));
516
+ const fieldAnalysis = {};
517
+
518
+ // First, collect all possible fields from all records
519
+ const allFields = new Set();
520
+ for (const record of sample) {
521
+ for (const field of Object.keys(record)) {
522
+ allFields.add(field);
523
+ }
524
+ }
525
+
526
+ // Initialize analysis for all fields
527
+ for (const field of allFields) {
528
+ fieldAnalysis[field] = {
529
+ type: 'unknown',
530
+ uniqueValues: new Set(),
531
+ nullCount: 0,
532
+ totalCount: 0
533
+ };
534
+ }
535
+
536
+ // Analyze each field across all records
537
+ for (const record of sample) {
538
+ for (const field of allFields) {
539
+ const value = record[field];
540
+ fieldAnalysis[field].totalCount++;
541
+ if (value === null || value === undefined) {
542
+ fieldAnalysis[field].nullCount++;
543
+ } else {
544
+ if (fieldAnalysis[field].type === 'unknown') {
545
+ fieldAnalysis[field].type = typeof value;
546
+ }
547
+ fieldAnalysis[field].uniqueValues.add(value);
548
+ }
549
+ }
550
+ }
551
+
552
+ // Generate suggestions
553
+ const suggestions = {
554
+ recommended: [],
555
+ optional: [],
556
+ notRecommended: []
557
+ };
558
+ for (const [field, analysis] of Object.entries(fieldAnalysis)) {
559
+ const coverage = (analysis.totalCount - analysis.nullCount) / analysis.totalCount;
560
+ const uniqueness = analysis.uniqueValues.size / analysis.totalCount;
561
+ const suggestion = {
562
+ field,
563
+ type: analysis.type,
564
+ coverage: Math.round(coverage * 100),
565
+ uniqueness: Math.round(uniqueness * 100),
566
+ uniqueValues: analysis.uniqueValues.size
567
+ };
568
+
569
+ // Recommendation logic
570
+ if (coverage > 0.9 && uniqueness > 0.8) {
571
+ suggestions.recommended.push(suggestion);
572
+ } else if (coverage > 0.7 && uniqueness > 0.5) {
573
+ suggestions.optional.push(suggestion);
574
+ } else {
575
+ suggestions.notRecommended.push(suggestion);
576
+ }
577
+ }
578
+
579
+ // Convert suggestions to the expected format for tests
580
+ const suggestedIndexes = {};
581
+ for (const suggestion of suggestions.recommended) {
582
+ suggestedIndexes[suggestion.field] = suggestion.type;
583
+ }
584
+ return {
585
+ success: true,
586
+ totalRecords: records.length,
587
+ analyzedRecords: sample.length,
588
+ suggestedIndexes,
589
+ suggestions
590
+ };
591
+ } catch (error) {
592
+ return {
593
+ success: false,
594
+ error: error.message
595
+ };
596
+ }
597
+ }
598
+ };