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