jexidb 1.1.0 → 2.0.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/index.js ADDED
@@ -0,0 +1,608 @@
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
+ * Compatibility method: readColumnIndex - gets unique values from indexed columns only
257
+ * Maintains compatibility with JexiDB v1 code
258
+ * @param {string} column - The column name to get unique values from
259
+ * @returns {Set} Set of unique values in the column (indexed columns only)
260
+ */
261
+ readColumnIndex(column) {
262
+ return super.readColumnIndex(column);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * JexiDB - Robust JSONL database
268
+ * Complete rewrite of JexiDB with JSONL architecture, fixing all critical bugs from version 1.x
269
+ *
270
+ * Features:
271
+ * - One file per table (pure JSONL)
272
+ * - Punctual reading (doesn't load everything in memory)
273
+ * - In-memory indexes for performance
274
+ * - Safe truncation after operations
275
+ * - Integrity validation
276
+ * - Event-driven architecture
277
+ *
278
+ * API similar to JexiDB 1.x:
279
+ * - new JexiDB('file.jsonl', { indexes: { id: 'number' } })
280
+ * - await db.init()
281
+ * - await db.insert({ id: 1, name: 'John' })
282
+ * - await db.find({ id: 1 })
283
+ * - await db.update({ id: 1 }, { name: 'John Smith' })
284
+ * - await db.delete({ id: 1 })
285
+ * - await db.save()
286
+ * - await db.destroy()
287
+ * - await db.walk() - Iterator for large volumes
288
+ * - await db.validateIntegrity() - Manual verification
289
+ */
290
+
291
+ // Export the compatibility wrapper as default
292
+ var _default = exports.default = JexiDBCompatibility; // Export auxiliary classes for advanced use
293
+ // Export useful constants
294
+ const OPERATORS = exports.OPERATORS = {
295
+ GT: '>',
296
+ GTE: '>=',
297
+ LT: '<',
298
+ LTE: '<=',
299
+ NE: '!=',
300
+ IN: 'in',
301
+ NIN: 'nin',
302
+ REGEX: 'regex',
303
+ CONTAINS: 'contains'
304
+ };
305
+
306
+ // Export utility functions
307
+ const utils = exports.utils = {
308
+ /**
309
+ * Creates a database with default settings
310
+ */
311
+ createDatabase(filePath, indexes = {}) {
312
+ return new _JSONLDatabase.default(filePath, {
313
+ indexes
314
+ });
315
+ },
316
+ /**
317
+ * Validates if a JSONL file is valid
318
+ * @param {string} filePath - Path to the JSONL file
319
+ * @returns {Promise<Object>} Validation result with errors and line count
320
+ */
321
+ async validateJSONLFile(filePath) {
322
+ const {
323
+ promises: fs
324
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
325
+ const readline = await Promise.resolve().then(() => _interopRequireWildcard(require('readline')));
326
+ const {
327
+ createReadStream
328
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
329
+ try {
330
+ const fileStream = createReadStream(filePath);
331
+ const rl = readline.createInterface({
332
+ input: fileStream,
333
+ crlfDelay: Infinity
334
+ });
335
+ let lineNumber = 0;
336
+ const errors = [];
337
+ for await (const line of rl) {
338
+ lineNumber++;
339
+ if (line.trim() !== '') {
340
+ try {
341
+ JSON.parse(line);
342
+ } catch (error) {
343
+ errors.push(`Line ${lineNumber}: ${error.message}`);
344
+ }
345
+ }
346
+ }
347
+ return {
348
+ isValid: errors.length === 0,
349
+ errors,
350
+ lineCount: lineNumber
351
+ };
352
+ } catch (error) {
353
+ return {
354
+ isValid: false,
355
+ errors: [`Error reading file: ${error.message}`],
356
+ lineCount: 0
357
+ };
358
+ }
359
+ },
360
+ /**
361
+ * Converts a JSON file to JSONL (basic conversion)
362
+ * @param {string} jsonFilePath - Path to the JSON file
363
+ * @param {string} jsonlFilePath - Path to the output JSONL file
364
+ * @returns {Promise<Object>} Conversion result
365
+ */
366
+ async convertJSONToJSONL(jsonFilePath, jsonlFilePath) {
367
+ const {
368
+ promises: fs
369
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
370
+ try {
371
+ const jsonData = await fs.readFile(jsonFilePath, 'utf8');
372
+ const data = JSON.parse(jsonData);
373
+ const records = Array.isArray(data) ? data : [data];
374
+ const jsonlContent = records.map(record => JSON.stringify(record)).join('\n') + '\n';
375
+ await fs.writeFile(jsonlFilePath, jsonlContent, 'utf8');
376
+ return {
377
+ success: true,
378
+ recordCount: records.length
379
+ };
380
+ } catch (error) {
381
+ return {
382
+ success: false,
383
+ error: error.message
384
+ };
385
+ }
386
+ },
387
+ /**
388
+ * Converts a JSONL file to JSON
389
+ * @param {string} jsonlFilePath - Path to the JSONL file
390
+ * @param {string} jsonFilePath - Path to the output JSON file
391
+ * @returns {Promise<Object>} Conversion result
392
+ */
393
+ async convertJSONLToJSON(jsonlFilePath, jsonFilePath) {
394
+ const {
395
+ promises: fs
396
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
397
+ const readline = await Promise.resolve().then(() => _interopRequireWildcard(require('readline')));
398
+ const {
399
+ createReadStream
400
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
401
+ try {
402
+ const fileStream = createReadStream(jsonlFilePath);
403
+ const rl = readline.createInterface({
404
+ input: fileStream,
405
+ crlfDelay: Infinity
406
+ });
407
+ const records = [];
408
+ for await (const line of rl) {
409
+ if (line.trim() !== '') {
410
+ try {
411
+ const record = JSON.parse(line);
412
+ records.push(record);
413
+ } catch (error) {
414
+ console.warn(`Line ignored: ${error.message}`);
415
+ }
416
+ }
417
+ }
418
+ const jsonContent = JSON.stringify(records, null, 2);
419
+ await fs.writeFile(jsonFilePath, jsonContent, 'utf8');
420
+ return {
421
+ success: true,
422
+ recordCount: records.length
423
+ };
424
+ } catch (error) {
425
+ return {
426
+ success: false,
427
+ error: error.message
428
+ };
429
+ }
430
+ },
431
+ /**
432
+ * Creates a JexiDB database from a JSON file with automatic index detection
433
+ * @param {string} jsonFilePath - Path to the JSON file
434
+ * @param {string} dbFilePath - Path to the output JexiDB file
435
+ * @param {Object} options - Options for database creation
436
+ * @param {Object} options.indexes - Manual index configuration
437
+ * @param {boolean} options.autoDetectIndexes - Auto-detect common index fields (default: true)
438
+ * @param {Array<string>} options.autoIndexFields - Fields to auto-index (default: ['id', '_id', 'email', 'name'])
439
+ * @returns {Promise<Object>} Database creation result
440
+ */
441
+ async createDatabaseFromJSON(jsonFilePath, dbFilePath, options = {}) {
442
+ const {
443
+ promises: fs
444
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
445
+ try {
446
+ // Read JSON data
447
+ const jsonData = await fs.readFile(jsonFilePath, 'utf8');
448
+ const data = JSON.parse(jsonData);
449
+ const records = Array.isArray(data) ? data : [data];
450
+ if (records.length === 0) {
451
+ return {
452
+ success: false,
453
+ error: 'No records found in JSON file'
454
+ };
455
+ }
456
+
457
+ // Auto-detect indexes if enabled
458
+ let indexes = options.indexes || {};
459
+ if (options.autoDetectIndexes !== false) {
460
+ const autoIndexFields = options.autoIndexFields || ['id', '_id', 'email', 'name', 'username'];
461
+ const sampleRecord = records[0];
462
+ for (const field of autoIndexFields) {
463
+ if (sampleRecord.hasOwnProperty(field)) {
464
+ const value = sampleRecord[field];
465
+ if (typeof value === 'number') {
466
+ indexes[field] = 'number';
467
+ } else if (typeof value === 'string') {
468
+ indexes[field] = 'string';
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ // Create database
475
+ const db = new _JSONLDatabase.default(dbFilePath, {
476
+ indexes,
477
+ autoSave: false,
478
+ validateOnInit: false
479
+ });
480
+ await db.init();
481
+
482
+ // Insert all records
483
+ for (const record of records) {
484
+ await db.insert(record);
485
+ }
486
+
487
+ // Save and close the database
488
+ await db.save();
489
+ await db.close();
490
+ return {
491
+ success: true,
492
+ recordCount: records.length,
493
+ indexes: Object.keys(indexes),
494
+ dbPath: dbFilePath
495
+ };
496
+ } catch (error) {
497
+ return {
498
+ success: false,
499
+ error: error.message
500
+ };
501
+ }
502
+ },
503
+ /**
504
+ * Analyzes a JSON file and suggests optimal indexes
505
+ * @param {string} jsonFilePath - Path to the JSON file
506
+ * @param {number} sampleSize - Number of records to analyze (default: 100)
507
+ * @returns {Promise<Object>} Index suggestions
508
+ */
509
+ async analyzeJSONForIndexes(jsonFilePath, sampleSize = 100) {
510
+ const {
511
+ promises: fs
512
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
513
+ try {
514
+ const jsonData = await fs.readFile(jsonFilePath, 'utf8');
515
+ const data = JSON.parse(jsonData);
516
+ const records = Array.isArray(data) ? data : [data];
517
+ if (records.length === 0) {
518
+ return {
519
+ success: false,
520
+ error: 'No records found in JSON file'
521
+ };
522
+ }
523
+
524
+ // Analyze sample records
525
+ const sample = records.slice(0, Math.min(sampleSize, records.length));
526
+ const fieldAnalysis = {};
527
+
528
+ // First, collect all possible fields from all records
529
+ const allFields = new Set();
530
+ for (const record of sample) {
531
+ for (const field of Object.keys(record)) {
532
+ allFields.add(field);
533
+ }
534
+ }
535
+
536
+ // Initialize analysis for all fields
537
+ for (const field of allFields) {
538
+ fieldAnalysis[field] = {
539
+ type: 'unknown',
540
+ uniqueValues: new Set(),
541
+ nullCount: 0,
542
+ totalCount: 0
543
+ };
544
+ }
545
+
546
+ // Analyze each field across all records
547
+ for (const record of sample) {
548
+ for (const field of allFields) {
549
+ const value = record[field];
550
+ fieldAnalysis[field].totalCount++;
551
+ if (value === null || value === undefined) {
552
+ fieldAnalysis[field].nullCount++;
553
+ } else {
554
+ if (fieldAnalysis[field].type === 'unknown') {
555
+ fieldAnalysis[field].type = typeof value;
556
+ }
557
+ fieldAnalysis[field].uniqueValues.add(value);
558
+ }
559
+ }
560
+ }
561
+
562
+ // Generate suggestions
563
+ const suggestions = {
564
+ recommended: [],
565
+ optional: [],
566
+ notRecommended: []
567
+ };
568
+ for (const [field, analysis] of Object.entries(fieldAnalysis)) {
569
+ const coverage = (analysis.totalCount - analysis.nullCount) / analysis.totalCount;
570
+ const uniqueness = analysis.uniqueValues.size / analysis.totalCount;
571
+ const suggestion = {
572
+ field,
573
+ type: analysis.type,
574
+ coverage: Math.round(coverage * 100),
575
+ uniqueness: Math.round(uniqueness * 100),
576
+ uniqueValues: analysis.uniqueValues.size
577
+ };
578
+
579
+ // Recommendation logic
580
+ if (coverage > 0.9 && uniqueness > 0.8) {
581
+ suggestions.recommended.push(suggestion);
582
+ } else if (coverage > 0.7 && uniqueness > 0.5) {
583
+ suggestions.optional.push(suggestion);
584
+ } else {
585
+ suggestions.notRecommended.push(suggestion);
586
+ }
587
+ }
588
+
589
+ // Convert suggestions to the expected format for tests
590
+ const suggestedIndexes = {};
591
+ for (const suggestion of suggestions.recommended) {
592
+ suggestedIndexes[suggestion.field] = suggestion.type;
593
+ }
594
+ return {
595
+ success: true,
596
+ totalRecords: records.length,
597
+ analyzedRecords: sample.length,
598
+ suggestedIndexes,
599
+ suggestions
600
+ };
601
+ } catch (error) {
602
+ return {
603
+ success: false,
604
+ error: error.message
605
+ };
606
+ }
607
+ }
608
+ };