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