jtcsv 1.1.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 +21 -0
- package/README.md +393 -0
- package/bin/jtcsv.js +394 -0
- package/cli-tui.js +0 -0
- package/csv-to-json.js +492 -0
- package/errors.js +188 -0
- package/index.d.ts +348 -0
- package/index.js +44 -0
- package/json-save.js +248 -0
- package/json-to-csv.js +430 -0
- package/package.json +87 -0
- package/stream-csv-to-json.js +613 -0
- package/stream-json-to-csv.js +532 -0
package/csv-to-json.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV to JSON Converter - Node.js Module
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, efficient module for converting CSV data to JSON format
|
|
5
|
+
* with proper parsing and error handling.
|
|
6
|
+
*
|
|
7
|
+
* @module csv-to-json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
ValidationError,
|
|
12
|
+
SecurityError,
|
|
13
|
+
FileSystemError,
|
|
14
|
+
ParsingError,
|
|
15
|
+
LimitError,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
safeExecute
|
|
18
|
+
} = require('./errors');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates CSV input and options
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
function validateCsvInput(csv, options) {
|
|
25
|
+
// Validate CSV input
|
|
26
|
+
if (typeof csv !== 'string') {
|
|
27
|
+
throw new ValidationError('Input must be a CSV string');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate options
|
|
31
|
+
if (options && typeof options !== 'object') {
|
|
32
|
+
throw new ConfigurationError('Options must be an object');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate delimiter
|
|
36
|
+
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
37
|
+
throw new ConfigurationError('Delimiter must be a string');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
41
|
+
throw new ConfigurationError('Delimiter must be a single character');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate maxRows
|
|
45
|
+
if (options?.maxRows && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
|
|
46
|
+
throw new ConfigurationError('maxRows must be a positive number');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parses a single CSV line with proper escaping
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
function parseCsvLine(line, lineNumber, delimiter) {
|
|
57
|
+
const fields = [];
|
|
58
|
+
let currentField = '';
|
|
59
|
+
let insideQuotes = false;
|
|
60
|
+
let escapeNext = false;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < line.length; i++) {
|
|
63
|
+
const char = line[i];
|
|
64
|
+
|
|
65
|
+
if (escapeNext) {
|
|
66
|
+
currentField += char;
|
|
67
|
+
escapeNext = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (char === '\\') {
|
|
72
|
+
escapeNext = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (char === '"') {
|
|
77
|
+
if (insideQuotes) {
|
|
78
|
+
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
79
|
+
// Could be escaped quote ("") or double quote at end ("")
|
|
80
|
+
if (i + 2 === line.length) {
|
|
81
|
+
// This is the pattern "" at the end of the line
|
|
82
|
+
// First quote is part of field, second is closing quote
|
|
83
|
+
currentField += '"';
|
|
84
|
+
i++; // Skip the closing quote
|
|
85
|
+
insideQuotes = false;
|
|
86
|
+
} else {
|
|
87
|
+
// Escaped quote inside quotes ("" -> ")
|
|
88
|
+
currentField += '"';
|
|
89
|
+
i++; // Skip next quote
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// Check if this is really the end of the quoted field
|
|
93
|
+
// Look ahead to see if next char is delimiter or end of line
|
|
94
|
+
let isEndOfField = false;
|
|
95
|
+
let j = i + 1;
|
|
96
|
+
// Skip whitespace
|
|
97
|
+
while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
|
|
98
|
+
j++;
|
|
99
|
+
}
|
|
100
|
+
if (j === line.length || line[j] === delimiter) {
|
|
101
|
+
isEndOfField = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isEndOfField) {
|
|
105
|
+
// This is the closing quote
|
|
106
|
+
insideQuotes = false;
|
|
107
|
+
} else {
|
|
108
|
+
// This quote is part of the field content
|
|
109
|
+
currentField += '"';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Start of quoted field
|
|
114
|
+
insideQuotes = true;
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!insideQuotes && char === delimiter) {
|
|
120
|
+
// End of field
|
|
121
|
+
fields.push(currentField);
|
|
122
|
+
currentField = '';
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
currentField += char;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add last field
|
|
130
|
+
fields.push(currentField);
|
|
131
|
+
|
|
132
|
+
// Check for unclosed quotes
|
|
133
|
+
if (insideQuotes) {
|
|
134
|
+
throw new ParsingError('Unclosed quotes in CSV', lineNumber);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Validate field count consistency
|
|
138
|
+
if (fields.length === 0) {
|
|
139
|
+
throw new ParsingError('No fields found', lineNumber);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return fields;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parses a value based on options
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
function parseCsvValue(value, options) {
|
|
150
|
+
const { trim = true, parseNumbers = false, parseBooleans = false } = options;
|
|
151
|
+
|
|
152
|
+
let result = value;
|
|
153
|
+
|
|
154
|
+
if (trim) {
|
|
155
|
+
result = result.trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Remove Excel formula protection
|
|
159
|
+
if (result.startsWith("'")) {
|
|
160
|
+
result = result.substring(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Parse numbers
|
|
164
|
+
if (parseNumbers && /^-?\d+(\.\d+)?$/.test(result)) {
|
|
165
|
+
const num = parseFloat(result);
|
|
166
|
+
if (!isNaN(num)) {
|
|
167
|
+
return num;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse booleans
|
|
172
|
+
if (parseBooleans) {
|
|
173
|
+
const lowerValue = result.toLowerCase();
|
|
174
|
+
if (lowerValue === 'true') {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (lowerValue === 'false') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Parse empty strings as null
|
|
183
|
+
if (result === '') {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Converts CSV string to JSON array
|
|
192
|
+
*
|
|
193
|
+
* @param {string} csv - CSV string to convert
|
|
194
|
+
* @param {Object} [options] - Configuration options
|
|
195
|
+
* @param {string} [options.delimiter=';'] - CSV delimiter character
|
|
196
|
+
* @param {boolean} [options.hasHeaders=true] - Whether CSV has headers row
|
|
197
|
+
* @param {Object} [options.renameMap={}] - Map for renaming column headers (newKey: oldKey)
|
|
198
|
+
* @param {boolean} [options.trim=true] - Trim whitespace from values
|
|
199
|
+
* @param {boolean} [options.parseNumbers=false] - Parse numeric values
|
|
200
|
+
* @param {boolean} [options.parseBooleans=false] - Parse boolean values
|
|
201
|
+
* @param {number} [options.maxRows=1000000] - Maximum number of rows to process
|
|
202
|
+
* @returns {Array<Object>} JSON array
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* const { csvToJson } = require('./csv-to-json');
|
|
206
|
+
*
|
|
207
|
+
* const csv = `id;name;email\n1;John;john@example.com\n2;Jane;jane@example.com`;
|
|
208
|
+
* const json = csvToJson(csv, {
|
|
209
|
+
* delimiter: ';',
|
|
210
|
+
* parseNumbers: true
|
|
211
|
+
* });
|
|
212
|
+
*/
|
|
213
|
+
function csvToJson(csv, options = {}) {
|
|
214
|
+
return safeExecute(() => {
|
|
215
|
+
// Validate input
|
|
216
|
+
validateCsvInput(csv, options);
|
|
217
|
+
|
|
218
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
219
|
+
|
|
220
|
+
const {
|
|
221
|
+
delimiter = ';',
|
|
222
|
+
hasHeaders = true,
|
|
223
|
+
renameMap = {},
|
|
224
|
+
trim = true,
|
|
225
|
+
parseNumbers = false,
|
|
226
|
+
parseBooleans = false,
|
|
227
|
+
maxRows = 1000000
|
|
228
|
+
} = opts;
|
|
229
|
+
|
|
230
|
+
// Handle empty CSV
|
|
231
|
+
if (csv.trim() === '') {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Parse CSV with proper handling of quotes and newlines
|
|
236
|
+
const lines = [];
|
|
237
|
+
let currentLine = '';
|
|
238
|
+
let insideQuotes = false;
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < csv.length; i++) {
|
|
241
|
+
const char = csv[i];
|
|
242
|
+
|
|
243
|
+
if (char === '"') {
|
|
244
|
+
if (insideQuotes && i + 1 < csv.length && csv[i + 1] === '"') {
|
|
245
|
+
// Escaped quote inside quotes ("" -> ")
|
|
246
|
+
currentLine += '"';
|
|
247
|
+
i++; // Skip next quote
|
|
248
|
+
} else {
|
|
249
|
+
// Toggle quote mode
|
|
250
|
+
insideQuotes = !insideQuotes;
|
|
251
|
+
}
|
|
252
|
+
currentLine += char;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (char === '\n' && !insideQuotes) {
|
|
257
|
+
// End of line (outside quotes)
|
|
258
|
+
lines.push(currentLine);
|
|
259
|
+
currentLine = '';
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (char === '\r') {
|
|
264
|
+
// Ignore carriage return, will be handled by \n
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
currentLine += char;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Add the last line
|
|
272
|
+
if (currentLine !== '' || insideQuotes) {
|
|
273
|
+
lines.push(currentLine);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check for unclosed quotes
|
|
277
|
+
if (insideQuotes) {
|
|
278
|
+
throw new ParsingError('Unclosed quotes in CSV', lines.length);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (lines.length === 0) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Limit rows to prevent OOM
|
|
286
|
+
if (lines.length > maxRows) {
|
|
287
|
+
throw new LimitError(
|
|
288
|
+
`CSV size exceeds maximum limit of ${maxRows} rows`,
|
|
289
|
+
maxRows,
|
|
290
|
+
lines.length
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let headers = [];
|
|
295
|
+
let startIndex = 0;
|
|
296
|
+
|
|
297
|
+
// Parse headers if present
|
|
298
|
+
if (hasHeaders && lines.length > 0) {
|
|
299
|
+
try {
|
|
300
|
+
headers = parseCsvLine(lines[0], 1, delimiter).map(header => {
|
|
301
|
+
const trimmed = trim ? header.trim() : header;
|
|
302
|
+
// Apply rename map
|
|
303
|
+
return renameMap[trimmed] || trimmed;
|
|
304
|
+
});
|
|
305
|
+
startIndex = 1;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof ParsingError) {
|
|
308
|
+
throw new ParsingError(`Failed to parse headers: ${error.message}`, 1);
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
// Generate numeric headers from first line
|
|
314
|
+
try {
|
|
315
|
+
const firstLineFields = parseCsvLine(lines[0], 1, delimiter);
|
|
316
|
+
headers = firstLineFields.map((_, index) => `column${index + 1}`);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error instanceof ParsingError) {
|
|
319
|
+
throw new ParsingError(`Failed to parse first line: ${error.message}`, 1);
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Parse data rows
|
|
326
|
+
const result = [];
|
|
327
|
+
|
|
328
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
329
|
+
const line = lines[i];
|
|
330
|
+
|
|
331
|
+
// Skip empty lines
|
|
332
|
+
if (line.trim() === '') {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const fields = parseCsvLine(line, i + 1, delimiter);
|
|
338
|
+
|
|
339
|
+
// Handle mismatched field count
|
|
340
|
+
const row = {};
|
|
341
|
+
const fieldCount = Math.min(fields.length, headers.length);
|
|
342
|
+
|
|
343
|
+
for (let j = 0; j < fieldCount; j++) {
|
|
344
|
+
row[headers[j]] = parseCsvValue(fields[j], { trim, parseNumbers, parseBooleans });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Warn about extra fields
|
|
348
|
+
if (fields.length > headers.length && process.env.NODE_ENV === 'development') {
|
|
349
|
+
console.warn(`[jtcsv] Line ${i + 1}: ${fields.length - headers.length} extra fields ignored`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
result.push(row);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error instanceof ParsingError) {
|
|
355
|
+
throw new ParsingError(`Line ${i + 1}: ${error.message}`, i + 1);
|
|
356
|
+
}
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return result;
|
|
362
|
+
}, 'PARSE_FAILED', { function: 'csvToJson' });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Validates file path for CSV reading
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
function validateCsvFilePath(filePath) {
|
|
370
|
+
const path = require('path');
|
|
371
|
+
|
|
372
|
+
// Basic validation
|
|
373
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
374
|
+
throw new ValidationError('File path must be a non-empty string');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Ensure file has .csv extension
|
|
378
|
+
if (!filePath.toLowerCase().endsWith('.csv')) {
|
|
379
|
+
throw new ValidationError('File must have .csv extension');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Prevent directory traversal attacks
|
|
383
|
+
const normalizedPath = path.normalize(filePath);
|
|
384
|
+
if (normalizedPath.includes('..') ||
|
|
385
|
+
/\\\\.\\.\\|\/\\.\\.\//.test(filePath) ||
|
|
386
|
+
filePath.startsWith('..') ||
|
|
387
|
+
filePath.includes('/..')) {
|
|
388
|
+
throw new SecurityError('Directory traversal detected in file path');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return path.resolve(filePath);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Reads CSV file and converts it to JSON array
|
|
396
|
+
*
|
|
397
|
+
* @param {string} filePath - Path to CSV file
|
|
398
|
+
* @param {Object} [options] - Configuration options (same as csvToJson)
|
|
399
|
+
* @returns {Promise<Array<Object>>} Promise that resolves to JSON array
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* const { readCsvAsJson } = require('./csv-to-json');
|
|
403
|
+
*
|
|
404
|
+
* const json = await readCsvAsJson('./data.csv', {
|
|
405
|
+
* delimiter: ',',
|
|
406
|
+
* parseNumbers: true
|
|
407
|
+
* });
|
|
408
|
+
*/
|
|
409
|
+
async function readCsvAsJson(filePath, options = {}) {
|
|
410
|
+
const fs = require('fs').promises;
|
|
411
|
+
|
|
412
|
+
// Validate file path
|
|
413
|
+
const safePath = validateCsvFilePath(filePath);
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
// Read file
|
|
417
|
+
const csvContent = await fs.readFile(safePath, 'utf8');
|
|
418
|
+
|
|
419
|
+
// Parse CSV
|
|
420
|
+
return csvToJson(csvContent, options);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
// Re-throw parsing errors as-is
|
|
423
|
+
if (error instanceof ParsingError || error instanceof ValidationError || error instanceof LimitError) {
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Wrap file system errors
|
|
428
|
+
if (error.code === 'ENOENT') {
|
|
429
|
+
throw new FileSystemError(`File not found: ${safePath}`, error);
|
|
430
|
+
}
|
|
431
|
+
if (error.code === 'EACCES') {
|
|
432
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
433
|
+
}
|
|
434
|
+
if (error.code === 'EISDIR') {
|
|
435
|
+
throw new FileSystemError(`Path is a directory: ${safePath}`, error);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
throw new FileSystemError(`Failed to read CSV file: ${error.message}`, error);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Synchronously reads CSV file and converts it to JSON array
|
|
444
|
+
*
|
|
445
|
+
* @param {string} filePath - Path to CSV file
|
|
446
|
+
* @param {Object} [options] - Configuration options (same as csvToJson)
|
|
447
|
+
* @returns {Array<Object>} JSON array
|
|
448
|
+
*/
|
|
449
|
+
function readCsvAsJsonSync(filePath, options = {}) {
|
|
450
|
+
const fs = require('fs');
|
|
451
|
+
|
|
452
|
+
// Validate file path
|
|
453
|
+
const safePath = validateCsvFilePath(filePath);
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
// Read file
|
|
457
|
+
const csvContent = fs.readFileSync(safePath, 'utf8');
|
|
458
|
+
|
|
459
|
+
// Parse CSV
|
|
460
|
+
return csvToJson(csvContent, options);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
// Re-throw parsing errors as-is
|
|
463
|
+
if (error instanceof ParsingError || error instanceof ValidationError || error instanceof LimitError) {
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Wrap file system errors
|
|
468
|
+
if (error.code === 'ENOENT') {
|
|
469
|
+
throw new FileSystemError(`File not found: ${safePath}`, error);
|
|
470
|
+
}
|
|
471
|
+
if (error.code === 'EACCES') {
|
|
472
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
473
|
+
}
|
|
474
|
+
if (error.code === 'EISDIR') {
|
|
475
|
+
throw new FileSystemError(`Path is a directory: ${safePath}`, error);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
throw new FileSystemError(`Failed to read CSV file: ${error.message}`, error);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Export the functions
|
|
483
|
+
module.exports = {
|
|
484
|
+
csvToJson,
|
|
485
|
+
readCsvAsJson,
|
|
486
|
+
readCsvAsJsonSync
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// For ES6 module compatibility
|
|
490
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
491
|
+
module.exports.default = csvToJson;
|
|
492
|
+
}
|
package/errors.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for jtcsv
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base error class for jtcsv
|
|
7
|
+
*/
|
|
8
|
+
class JtcsvError extends Error {
|
|
9
|
+
constructor(message, code = 'JTCSV_ERROR') {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'JtcsvError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
|
|
14
|
+
// Maintains proper stack trace for where our error was thrown
|
|
15
|
+
if (Error.captureStackTrace) {
|
|
16
|
+
Error.captureStackTrace(this, JtcsvError);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Error for invalid input data
|
|
23
|
+
*/
|
|
24
|
+
class ValidationError extends JtcsvError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message, 'VALIDATION_ERROR');
|
|
27
|
+
this.name = 'ValidationError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Error for security violations
|
|
33
|
+
*/
|
|
34
|
+
class SecurityError extends JtcsvError {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message, 'SECURITY_ERROR');
|
|
37
|
+
this.name = 'SecurityError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Error for file system operations
|
|
43
|
+
*/
|
|
44
|
+
class FileSystemError extends JtcsvError {
|
|
45
|
+
constructor(message, originalError = null) {
|
|
46
|
+
super(message, 'FILE_SYSTEM_ERROR');
|
|
47
|
+
this.name = 'FileSystemError';
|
|
48
|
+
this.originalError = originalError;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Error for parsing/formatting issues
|
|
54
|
+
*/
|
|
55
|
+
class ParsingError extends JtcsvError {
|
|
56
|
+
constructor(message, lineNumber = null, column = null) {
|
|
57
|
+
super(message, 'PARSING_ERROR');
|
|
58
|
+
this.name = 'ParsingError';
|
|
59
|
+
this.lineNumber = lineNumber;
|
|
60
|
+
this.column = column;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Error for size/limit violations
|
|
66
|
+
*/
|
|
67
|
+
class LimitError extends JtcsvError {
|
|
68
|
+
constructor(message, limit, actual) {
|
|
69
|
+
super(message, 'LIMIT_ERROR');
|
|
70
|
+
this.name = 'LimitError';
|
|
71
|
+
this.limit = limit;
|
|
72
|
+
this.actual = actual;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Error for configuration issues
|
|
78
|
+
*/
|
|
79
|
+
class ConfigurationError extends JtcsvError {
|
|
80
|
+
constructor(message) {
|
|
81
|
+
super(message, 'CONFIGURATION_ERROR');
|
|
82
|
+
this.name = 'ConfigurationError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Utility function to create standardized error messages
|
|
88
|
+
*/
|
|
89
|
+
function createErrorMessage(type, details) {
|
|
90
|
+
const messages = {
|
|
91
|
+
INVALID_INPUT: `Invalid input: ${details}`,
|
|
92
|
+
SECURITY_VIOLATION: `Security violation: ${details}`,
|
|
93
|
+
FILE_NOT_FOUND: `File not found: ${details}`,
|
|
94
|
+
PARSE_FAILED: `Parse failed: ${details}`,
|
|
95
|
+
SIZE_LIMIT: `Size limit exceeded: ${details}`,
|
|
96
|
+
INVALID_CONFIG: `Invalid configuration: ${details}`,
|
|
97
|
+
UNKNOWN_ERROR: `Unknown error: ${details}`
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return messages[type] || messages.UNKNOWN_ERROR;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Error handler utility
|
|
105
|
+
*/
|
|
106
|
+
function handleError(error, context = {}) {
|
|
107
|
+
// Log error in development
|
|
108
|
+
if (process.env.NODE_ENV === 'development') {
|
|
109
|
+
console.error(`[jtcsv] Error in ${context.function || 'unknown'}:`, {
|
|
110
|
+
message: error.message,
|
|
111
|
+
code: error.code,
|
|
112
|
+
stack: error.stack,
|
|
113
|
+
context
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Re-throw the error
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Safe execution wrapper for async functions
|
|
123
|
+
*/
|
|
124
|
+
async function safeExecuteAsync(fn, errorType, context = {}) {
|
|
125
|
+
try {
|
|
126
|
+
return await fn();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof JtcsvError) {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Wrap unknown errors
|
|
133
|
+
const message = createErrorMessage(errorType, error.message);
|
|
134
|
+
const wrappedError = new JtcsvError(message, errorType);
|
|
135
|
+
wrappedError.originalError = error;
|
|
136
|
+
|
|
137
|
+
handleError(wrappedError, context);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Safe execution wrapper for sync functions
|
|
143
|
+
*/
|
|
144
|
+
function safeExecuteSync(fn, errorType, context = {}) {
|
|
145
|
+
try {
|
|
146
|
+
return fn();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof JtcsvError) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Wrap unknown errors
|
|
153
|
+
const message = createErrorMessage(errorType, error.message);
|
|
154
|
+
const wrappedError = new JtcsvError(message, errorType);
|
|
155
|
+
wrappedError.originalError = error;
|
|
156
|
+
|
|
157
|
+
handleError(wrappedError, context);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Safe execution wrapper (auto-detects async/sync)
|
|
163
|
+
*/
|
|
164
|
+
function safeExecute(fn, errorType, context = {}) {
|
|
165
|
+
const result = fn();
|
|
166
|
+
|
|
167
|
+
// Check if function returns a promise
|
|
168
|
+
if (result && typeof result.then === 'function') {
|
|
169
|
+
return safeExecuteAsync(async () => result, errorType, context);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return safeExecuteSync(() => result, errorType, context);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
JtcsvError,
|
|
177
|
+
ValidationError,
|
|
178
|
+
SecurityError,
|
|
179
|
+
FileSystemError,
|
|
180
|
+
ParsingError,
|
|
181
|
+
LimitError,
|
|
182
|
+
ConfigurationError,
|
|
183
|
+
createErrorMessage,
|
|
184
|
+
handleError,
|
|
185
|
+
safeExecute,
|
|
186
|
+
safeExecuteAsync,
|
|
187
|
+
safeExecuteSync
|
|
188
|
+
};
|