convert-csv-to-json 3.28.0 → 4.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/CHANGELOG.md +41 -1
- package/README.md +173 -833
- package/docs/ASYNC.md +485 -0
- package/docs/BROWSER.md +425 -0
- package/docs/ERROR_HANDLING.md +367 -0
- package/docs/MAPROWS.md +196 -0
- package/docs/SYNC.md +239 -0
- package/index.d.ts +10 -8
- package/index.js +13 -10
- package/migration/RFC4180_MIGRATION_GUIDE.md +737 -0
- package/package.json +1 -1
- package/release-notes/RELEASE_NOTES_v4.0.0.md +320 -0
- package/src/browserApi.js +29 -6
- package/src/csvToJson.js +233 -82
- package/src/csvToJsonAsync.js +15 -1
- package/src/util/errors.js +227 -0
- package/src/util/fileUtils.js +18 -7
- package/src/util/jsonUtils.js +3 -1
- /package/{MIGRATION.md → migration/MIGRATION_TO_ASYNC.md} +0 -0
package/src/csvToJson.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const fileUtils = require('./util/fileUtils');
|
|
4
|
+
const stringUtils = require('./util/stringUtils');
|
|
5
|
+
const jsonUtils = require('./util/jsonUtils');
|
|
6
|
+
const {
|
|
7
|
+
ConfigurationError,
|
|
8
|
+
CsvFormatError,
|
|
9
|
+
JsonValidationError
|
|
10
|
+
} = require('./util/errors');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_FIELD_DELIMITER = ",";
|
|
13
|
+
const QUOTE_CHAR = '"';
|
|
14
|
+
const CRLF = '\r\n';
|
|
15
|
+
const LF = '\n';
|
|
16
|
+
const CR = '\r';
|
|
9
17
|
|
|
10
18
|
class CsvToJson {
|
|
11
19
|
|
|
@@ -30,8 +38,8 @@ class CsvToJson {
|
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
indexHeader(indexHeaderValue) {
|
|
33
|
-
if(isNaN(indexHeaderValue)){
|
|
34
|
-
|
|
41
|
+
if (isNaN(indexHeaderValue)) {
|
|
42
|
+
throw ConfigurationError.invalidHeaderIndex(indexHeaderValue);
|
|
35
43
|
}
|
|
36
44
|
this.indexHeaderValue = indexHeaderValue;
|
|
37
45
|
return this;
|
|
@@ -48,6 +56,19 @@ class CsvToJson {
|
|
|
48
56
|
return this;
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Sets a mapper function to transform each row after conversion
|
|
61
|
+
* @param {Function} mapperFn - Function that receives (row, index) and returns transformed row or null to filter out
|
|
62
|
+
* @returns {this} - For method chaining
|
|
63
|
+
*/
|
|
64
|
+
mapRows(mapperFn) {
|
|
65
|
+
if (typeof mapperFn !== 'function') {
|
|
66
|
+
throw new TypeError('mapperFn must be a function');
|
|
67
|
+
}
|
|
68
|
+
this.rowMapper = mapperFn;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
generateJsonFileFromCsv(fileInputName, fileOutputName) {
|
|
52
73
|
let jsonStringified = this.getJsonFromCsvStringified(fileInputName);
|
|
53
74
|
fileUtils.writeFile(jsonStringified, fileOutputName);
|
|
@@ -78,43 +99,142 @@ class CsvToJson {
|
|
|
78
99
|
|
|
79
100
|
csvToJson(parsedCsv) {
|
|
80
101
|
this.validateInputConfig();
|
|
81
|
-
|
|
102
|
+
|
|
103
|
+
// Parse CSV into individual records, respecting quoted fields that may contain newlines
|
|
104
|
+
let records = this.parseRecords(parsedCsv);
|
|
105
|
+
|
|
82
106
|
let fieldDelimiter = this.getFieldDelimiter();
|
|
83
107
|
let index = this.getIndexHeader();
|
|
84
108
|
let headers;
|
|
85
109
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
// Find the header row
|
|
111
|
+
while (index < records.length) {
|
|
112
|
+
if (this.isSupportQuotedField) {
|
|
113
|
+
headers = this.split(records[index]);
|
|
114
|
+
} else {
|
|
115
|
+
headers = records[index].split(fieldDelimiter);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (stringUtils.hasContent(headers)) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
index++;
|
|
90
122
|
}
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
headers = lines[index].split(fieldDelimiter);
|
|
124
|
+
if (!headers) {
|
|
125
|
+
throw CsvFormatError.missingHeader();
|
|
95
126
|
}
|
|
96
127
|
|
|
97
128
|
let jsonResult = [];
|
|
98
|
-
for (let i = (index + 1); i <
|
|
129
|
+
for (let i = (index + 1); i < records.length; i++) {
|
|
99
130
|
let currentLine;
|
|
100
|
-
if(this.isSupportQuotedField){
|
|
101
|
-
currentLine = this.split(
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
currentLine = lines[i].split(fieldDelimiter);
|
|
131
|
+
if (this.isSupportQuotedField) {
|
|
132
|
+
currentLine = this.split(records[i]);
|
|
133
|
+
} else {
|
|
134
|
+
currentLine = records[i].split(fieldDelimiter);
|
|
105
135
|
}
|
|
136
|
+
|
|
106
137
|
if (stringUtils.hasContent(currentLine)) {
|
|
107
|
-
|
|
138
|
+
let row = this.buildJsonResult(headers, currentLine);
|
|
139
|
+
|
|
140
|
+
// Apply row mapper if defined
|
|
141
|
+
if (this.rowMapper) {
|
|
142
|
+
row = this.rowMapper(row, i - (index + 1)); // Pass row and 0-based row index
|
|
143
|
+
// If mapper returns null/undefined, skip this row (allows filtering)
|
|
144
|
+
if (row != null) {
|
|
145
|
+
jsonResult.push(row);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
jsonResult.push(row);
|
|
149
|
+
}
|
|
108
150
|
}
|
|
109
|
-
|
|
151
|
+
}
|
|
110
152
|
return jsonResult;
|
|
111
153
|
}
|
|
112
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Parse CSV content into individual records, respecting quoted fields that may span multiple lines.
|
|
157
|
+
* RFC 4180 compliant parsing - handles quoted fields that may contain newlines.
|
|
158
|
+
* @param {string} csvContent - The raw CSV content
|
|
159
|
+
* @returns {string[]} Array of record strings
|
|
160
|
+
*/
|
|
161
|
+
parseRecords(csvContent) {
|
|
162
|
+
let records = [];
|
|
163
|
+
let currentRecord = '';
|
|
164
|
+
let insideQuotes = false;
|
|
165
|
+
let i = 0;
|
|
166
|
+
|
|
167
|
+
while (i < csvContent.length) {
|
|
168
|
+
let char = csvContent[i];
|
|
169
|
+
|
|
170
|
+
// Handle quote characters
|
|
171
|
+
if (char === QUOTE_CHAR) {
|
|
172
|
+
if (insideQuotes && i + 1 < csvContent.length && csvContent[i + 1] === QUOTE_CHAR) {
|
|
173
|
+
// Escaped quote: two consecutive quotes = single quote representation
|
|
174
|
+
currentRecord += QUOTE_CHAR + QUOTE_CHAR;
|
|
175
|
+
i += 2;
|
|
176
|
+
} else {
|
|
177
|
+
// Toggle quote state
|
|
178
|
+
insideQuotes = !insideQuotes;
|
|
179
|
+
currentRecord += char;
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle line endings (only outside quoted fields)
|
|
186
|
+
if (!insideQuotes) {
|
|
187
|
+
let lineEndingLength = this.getLineEndingLength(csvContent, i);
|
|
188
|
+
if (lineEndingLength > 0) {
|
|
189
|
+
records.push(currentRecord);
|
|
190
|
+
currentRecord = '';
|
|
191
|
+
i += lineEndingLength;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Regular character
|
|
197
|
+
currentRecord += char;
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Add the last record if not empty
|
|
202
|
+
if (currentRecord.length > 0) {
|
|
203
|
+
records.push(currentRecord);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validate matching quotes
|
|
207
|
+
if (insideQuotes) {
|
|
208
|
+
throw CsvFormatError.mismatchedQuotes('CSV');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return records;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get the length of line ending at current position (CRLF=2, LF=1, CR=1, or 0)
|
|
216
|
+
* @param {string} content - CSV content
|
|
217
|
+
* @param {number} index - Current index to check
|
|
218
|
+
* @returns {number} Length of line ending (0 if none)
|
|
219
|
+
*/
|
|
220
|
+
getLineEndingLength(content, index) {
|
|
221
|
+
if (content.slice(index, index + 2) === CRLF) {
|
|
222
|
+
return 2;
|
|
223
|
+
}
|
|
224
|
+
if (content[index] === LF) {
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
if (content[index] === CR && content[index + 1] !== LF) {
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
113
233
|
getFieldDelimiter() {
|
|
114
234
|
if (this.delimiter) {
|
|
115
235
|
return this.delimiter;
|
|
116
236
|
}
|
|
117
|
-
return
|
|
237
|
+
return DEFAULT_FIELD_DELIMITER;
|
|
118
238
|
}
|
|
119
239
|
|
|
120
240
|
getIndexHeader(){
|
|
@@ -168,74 +288,105 @@ class CsvToJson {
|
|
|
168
288
|
}
|
|
169
289
|
|
|
170
290
|
validateInputConfig(){
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
291
|
+
if(this.isSupportQuotedField) {
|
|
292
|
+
if(this.getFieldDelimiter() === '"'){
|
|
293
|
+
throw ConfigurationError.quotedFieldConflict('fieldDelimiter', '"');
|
|
294
|
+
}
|
|
295
|
+
if(this.parseSubArraySeparator === '"'){
|
|
296
|
+
throw ConfigurationError.quotedFieldConflict('parseSubArraySeparator', '"');
|
|
297
|
+
}
|
|
298
|
+
if(this.parseSubArrayDelimiter === '"'){
|
|
299
|
+
throw ConfigurationError.quotedFieldConflict('parseSubArrayDelimiter', '"');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
182
302
|
}
|
|
183
303
|
|
|
184
304
|
hasQuotes(line) {
|
|
185
305
|
return line.includes('"');
|
|
186
306
|
}
|
|
187
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Split a CSV record line into fields, respecting quoted fields per RFC 4180.
|
|
310
|
+
* Handles:
|
|
311
|
+
* - Quoted fields with embedded delimiters and newlines
|
|
312
|
+
* - Escaped quotes (double quotes within quoted fields)
|
|
313
|
+
* - Empty quoted fields
|
|
314
|
+
* @param {string} line - A single CSV record line
|
|
315
|
+
* @returns {string[]} Array of field values
|
|
316
|
+
*/
|
|
188
317
|
split(line) {
|
|
189
|
-
if(line.length
|
|
318
|
+
if (line.length === 0) {
|
|
190
319
|
return [];
|
|
191
320
|
}
|
|
192
|
-
|
|
193
|
-
let
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
subSplits[subIndex] = '';
|
|
214
|
-
return;
|
|
215
|
-
} else {
|
|
216
|
-
if (arr[i + 1] === '"') {
|
|
217
|
-
//Double quote
|
|
218
|
-
isDouble = true;
|
|
219
|
-
//subSplits[subIndex] += c; //Skip because this is escaped quote
|
|
220
|
-
} else {
|
|
221
|
-
if (!startQuote) {
|
|
222
|
-
startQuote = true;
|
|
223
|
-
//subSplits[subIndex] += c; //Skip because we don't want quotes wrapping value
|
|
224
|
-
} else {
|
|
225
|
-
//end
|
|
226
|
-
startQuote = false;
|
|
227
|
-
//subSplits[subIndex] += c; //Skip because we don't want quotes wrapping value
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
if(startQuote){
|
|
233
|
-
throw new Error('Row contains mismatched quotes!');
|
|
321
|
+
|
|
322
|
+
let fields = [];
|
|
323
|
+
let currentField = '';
|
|
324
|
+
let insideQuotes = false;
|
|
325
|
+
let delimiter = this.getFieldDelimiter();
|
|
326
|
+
|
|
327
|
+
for (let i = 0; i < line.length; i++) {
|
|
328
|
+
let char = line[i];
|
|
329
|
+
|
|
330
|
+
// Handle quote character
|
|
331
|
+
if (char === QUOTE_CHAR) {
|
|
332
|
+
if (this.isEscapedQuote(line, i, insideQuotes)) {
|
|
333
|
+
// Two consecutive quotes inside quoted field = escaped quote
|
|
334
|
+
currentField += QUOTE_CHAR;
|
|
335
|
+
i++; // Skip next quote
|
|
336
|
+
} else if (this.isEmptyQuotedField(line, i, insideQuotes, currentField, delimiter)) {
|
|
337
|
+
// Empty quoted field: "" at field start before delimiter/end
|
|
338
|
+
i++; // Skip closing quote
|
|
339
|
+
} else {
|
|
340
|
+
// Regular quote: toggle quoted state
|
|
341
|
+
insideQuotes = !insideQuotes;
|
|
234
342
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
343
|
+
} else if (char === delimiter && !insideQuotes) {
|
|
344
|
+
// Delimiter outside quotes marks field boundary
|
|
345
|
+
fields.push(currentField);
|
|
346
|
+
currentField = '';
|
|
347
|
+
} else {
|
|
348
|
+
// Regular character (including embedded newlines in quoted fields)
|
|
349
|
+
currentField += char;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Add final field
|
|
354
|
+
fields.push(currentField);
|
|
355
|
+
|
|
356
|
+
// Validate matching quotes
|
|
357
|
+
if (insideQuotes) {
|
|
358
|
+
throw CsvFormatError.mismatchedQuotes('row');
|
|
238
359
|
}
|
|
360
|
+
|
|
361
|
+
return fields;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Check if character at index is an escaped quote (double quote)
|
|
366
|
+
* @returns {boolean}
|
|
367
|
+
*/
|
|
368
|
+
isEscapedQuote(line, index, insideQuoted) {
|
|
369
|
+
return insideQuoted &&
|
|
370
|
+
index + 1 < line.length &&
|
|
371
|
+
line[index + 1] === QUOTE_CHAR;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if this is an empty quoted field: "" before delimiter or end of line
|
|
376
|
+
* @returns {boolean}
|
|
377
|
+
*/
|
|
378
|
+
isEmptyQuotedField(line, index, insideQuoted, currentField, delimiter) {
|
|
379
|
+
if (insideQuoted || currentField !== '' || index + 1 >= line.length) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let nextChar = line[index + 1];
|
|
384
|
+
if (nextChar !== QUOTE_CHAR) {
|
|
385
|
+
return false; // Not a quote pair
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let afterQuotes = index + 2;
|
|
389
|
+
return afterQuotes === line.length || line[afterQuotes] === delimiter;
|
|
239
390
|
}
|
|
240
391
|
}
|
|
241
392
|
|
package/src/csvToJsonAsync.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fileUtils = require('./util/fileUtils');
|
|
4
4
|
const csvToJson = require('./csvToJson');
|
|
5
|
+
const { InputValidationError } = require('./util/errors');
|
|
5
6
|
|
|
6
7
|
class CsvToJsonAsync {
|
|
7
8
|
constructor() {
|
|
@@ -57,6 +58,14 @@ class CsvToJsonAsync {
|
|
|
57
58
|
return this;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Set row mapper function to transform each row
|
|
63
|
+
*/
|
|
64
|
+
mapRows(mapperFn) {
|
|
65
|
+
this.csvToJson.mapRows(mapperFn);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
/**
|
|
61
70
|
* Set encoding
|
|
62
71
|
*/
|
|
@@ -87,7 +96,12 @@ class CsvToJsonAsync {
|
|
|
87
96
|
*/
|
|
88
97
|
async getJsonFromCsvAsync(inputFileNameOrCsv, options = {}) {
|
|
89
98
|
if (inputFileNameOrCsv === null || inputFileNameOrCsv === undefined) {
|
|
90
|
-
throw new
|
|
99
|
+
throw new InputValidationError(
|
|
100
|
+
'inputFileNameOrCsv',
|
|
101
|
+
'string (file path) or CSV string content',
|
|
102
|
+
`${typeof inputFileNameOrCsv}`,
|
|
103
|
+
'Either provide a valid file path or CSV content as a string.'
|
|
104
|
+
);
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
if (options.raw) {
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom error classes following clean code principles
|
|
5
|
+
* Provides clear, actionable error messages with context
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base class for all CSV parsing errors
|
|
10
|
+
* Provides consistent error formatting and context
|
|
11
|
+
*/
|
|
12
|
+
class CsvParsingError extends Error {
|
|
13
|
+
constructor(message, code, context = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'CsvParsingError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.context = context;
|
|
18
|
+
Error.captureStackTrace(this, this.constructor);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toString() {
|
|
22
|
+
let output = `${this.name}: ${this.message}`;
|
|
23
|
+
|
|
24
|
+
if (this.context && Object.keys(this.context).length > 0) {
|
|
25
|
+
output += '\n\nContext:';
|
|
26
|
+
Object.entries(this.context).forEach(([key, value]) => {
|
|
27
|
+
output += `\n ${key}: ${this.formatValue(value)}`;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
formatValue(value) {
|
|
35
|
+
if (value === null) return 'null';
|
|
36
|
+
if (value === undefined) return 'undefined';
|
|
37
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
38
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Input validation errors
|
|
45
|
+
*/
|
|
46
|
+
class InputValidationError extends CsvParsingError {
|
|
47
|
+
constructor(paramName, expectedType, receivedType, details = '') {
|
|
48
|
+
const message =
|
|
49
|
+
`Invalid input: Parameter '${paramName}' is required.\n` +
|
|
50
|
+
`Expected: ${expectedType}\n` +
|
|
51
|
+
`Received: ${receivedType}${details ? '\n' + details : ''}`;
|
|
52
|
+
|
|
53
|
+
super(message, 'INPUT_VALIDATION_ERROR', {
|
|
54
|
+
parameter: paramName,
|
|
55
|
+
expectedType,
|
|
56
|
+
receivedType
|
|
57
|
+
});
|
|
58
|
+
this.name = 'InputValidationError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Configuration-related errors
|
|
64
|
+
*/
|
|
65
|
+
class ConfigurationError extends CsvParsingError {
|
|
66
|
+
constructor(message, conflictingOptions = {}) {
|
|
67
|
+
super(message, 'CONFIGURATION_ERROR', conflictingOptions);
|
|
68
|
+
this.name = 'ConfigurationError';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static quotedFieldConflict(optionName, value) {
|
|
72
|
+
return new ConfigurationError(
|
|
73
|
+
`Configuration conflict: supportQuotedField() is enabled, but ${optionName} is set to '${value}'.\n` +
|
|
74
|
+
`The quote character (") cannot be used as a field delimiter, separator, or sub-array delimiter when quoted field support is active.\n\n` +
|
|
75
|
+
`Solutions:\n` +
|
|
76
|
+
` 1. Use a different character for ${optionName} (e.g., '|', '\\t', ';')\n` +
|
|
77
|
+
` 2. Disable supportQuotedField() if your CSV doesn't contain quoted fields\n` +
|
|
78
|
+
` 3. Refer to RFC 4180 for proper CSV formatting: https://tools.ietf.org/html/rfc4180`,
|
|
79
|
+
{ optionName, value, conflictingOption: 'supportQuotedField' }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static invalidHeaderIndex(value) {
|
|
84
|
+
return new ConfigurationError(
|
|
85
|
+
`Invalid configuration: indexHeader() expects a numeric value.\n` +
|
|
86
|
+
`Received: ${typeof value} (${value})\n\n` +
|
|
87
|
+
`Solutions:\n` +
|
|
88
|
+
` 1. Ensure indexHeader() receives a number: indexHeader(0), indexHeader(1), etc.\n` +
|
|
89
|
+
` 2. Headers are typically found on row 0 (first line)\n` +
|
|
90
|
+
` 3. Use indexHeader(2) if headers are on the 3rd line`,
|
|
91
|
+
{ parameterName: 'indexHeader', value, type: typeof value }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* CSV parsing errors with detailed context
|
|
98
|
+
*/
|
|
99
|
+
class CsvFormatError extends CsvParsingError {
|
|
100
|
+
constructor(message, context = {}) {
|
|
101
|
+
super(message, 'CSV_FORMAT_ERROR', context);
|
|
102
|
+
this.name = 'CsvFormatError';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static missingHeader() {
|
|
106
|
+
return new CsvFormatError(
|
|
107
|
+
`CSV parsing error: No header row found.\n` +
|
|
108
|
+
`The CSV file appears to be empty or has no valid header line.\n\n` +
|
|
109
|
+
`Solutions:\n` +
|
|
110
|
+
` 1. Ensure your CSV file contains at least one row (header row)\n` +
|
|
111
|
+
` 2. Verify the file is not empty or contains only whitespace\n` +
|
|
112
|
+
` 3. Check if you need to use indexHeader(n) to specify a non-standard header row\n` +
|
|
113
|
+
` 4. Refer to RFC 4180 for proper CSV format: https://tools.ietf.org/html/rfc4180`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static mismatchedQuotes(location = 'CSV') {
|
|
118
|
+
return new CsvFormatError(
|
|
119
|
+
`CSV parsing error: Mismatched quotes detected in ${location}.\n` +
|
|
120
|
+
`A quoted field was not properly closed with a matching quote character.\n\n` +
|
|
121
|
+
`RFC 4180 rules for quoted fields:\n` +
|
|
122
|
+
` • Fields containing delimiters or quotes MUST be enclosed in double quotes\n` +
|
|
123
|
+
` • To include a quote within a quoted field, use two consecutive quotes: ""\n` +
|
|
124
|
+
` • Example: "Smith, John" (name contains comma)\n` +
|
|
125
|
+
` • Example: "He said ""Hello""" (text contains quotes)\n\n` +
|
|
126
|
+
`Solutions:\n` +
|
|
127
|
+
` 1. Review your CSV for properly paired quote characters\n` +
|
|
128
|
+
` 2. Use double quotes ("") to escape quotes within quoted fields\n` +
|
|
129
|
+
` 3. Ensure all commas within field values are inside quotes\n` +
|
|
130
|
+
` 4. Enable supportQuotedField(true) if you're using quoted fields`,
|
|
131
|
+
{ location }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* File operation errors
|
|
138
|
+
*/
|
|
139
|
+
class FileOperationError extends CsvParsingError {
|
|
140
|
+
constructor(operation, filePath, originalError) {
|
|
141
|
+
const message =
|
|
142
|
+
`File operation error: Failed to ${operation} file.\n` +
|
|
143
|
+
`File path: ${filePath}\n` +
|
|
144
|
+
`Reason: ${originalError.message}\n\n` +
|
|
145
|
+
`Solutions:\n` +
|
|
146
|
+
` 1. Verify the file path is correct: ${filePath}\n` +
|
|
147
|
+
` 2. Check file permissions (read access for input, write access for output)\n` +
|
|
148
|
+
` 3. Ensure the directory exists and is writable for output files\n` +
|
|
149
|
+
` 4. Verify the file is not in use by another process`;
|
|
150
|
+
|
|
151
|
+
super(message, 'FILE_OPERATION_ERROR', {
|
|
152
|
+
operation,
|
|
153
|
+
filePath,
|
|
154
|
+
originalError: originalError.message
|
|
155
|
+
});
|
|
156
|
+
this.name = 'FileOperationError';
|
|
157
|
+
this.originalError = originalError;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* JSON validation errors
|
|
163
|
+
*/
|
|
164
|
+
class JsonValidationError extends CsvParsingError {
|
|
165
|
+
constructor(csvData, originalError) {
|
|
166
|
+
const message =
|
|
167
|
+
`JSON validation error: The parsed CSV data generated invalid JSON.\n` +
|
|
168
|
+
`This typically indicates malformed field names or values in the CSV.\n` +
|
|
169
|
+
`Original error: ${originalError.message}\n\n` +
|
|
170
|
+
`Solutions:\n` +
|
|
171
|
+
` 1. Check that field names are valid JavaScript identifiers (or will be converted safely)\n` +
|
|
172
|
+
` 2. Review the CSV data for special characters that aren't properly escaped\n` +
|
|
173
|
+
` 3. Enable supportQuotedField(true) for fields containing special characters\n` +
|
|
174
|
+
` 4. Verify that formatValueByType() isn't converting values incorrectly`;
|
|
175
|
+
|
|
176
|
+
super(message, 'JSON_VALIDATION_ERROR', {
|
|
177
|
+
originalError: originalError.message,
|
|
178
|
+
csvPreview: csvData ? csvData.substring(0, 200) : 'N/A'
|
|
179
|
+
});
|
|
180
|
+
this.name = 'JsonValidationError';
|
|
181
|
+
this.originalError = originalError;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Browser-specific errors
|
|
187
|
+
*/
|
|
188
|
+
class BrowserApiError extends CsvParsingError {
|
|
189
|
+
constructor(message, context = {}) {
|
|
190
|
+
super(message, 'BROWSER_API_ERROR', context);
|
|
191
|
+
this.name = 'BrowserApiError';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static fileReaderNotAvailable() {
|
|
195
|
+
return new BrowserApiError(
|
|
196
|
+
`Browser compatibility error: FileReader API is not available.\n` +
|
|
197
|
+
`Your browser does not support the FileReader API required for file parsing.\n\n` +
|
|
198
|
+
`Solutions:\n` +
|
|
199
|
+
` 1. Use a modern browser that supports FileReader (Chrome 13+, Firefox 10+, Safari 6+)\n` +
|
|
200
|
+
` 2. Consider using csvStringToJson() or csvStringToJsonAsync() for string-based parsing\n` +
|
|
201
|
+
` 3. Implement a polyfill or alternative file reading method`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static parseFileError(originalError) {
|
|
206
|
+
return new BrowserApiError(
|
|
207
|
+
`Browser file parsing error: Failed to read and parse the file.\n` +
|
|
208
|
+
`Error details: ${originalError.message}\n\n` +
|
|
209
|
+
`Solutions:\n` +
|
|
210
|
+
` 1. Verify the file is a valid CSV file\n` +
|
|
211
|
+
` 2. Check the file encoding (UTF-8 is recommended)\n` +
|
|
212
|
+
` 3. Try a smaller file to isolate the issue\n` +
|
|
213
|
+
` 4. Check browser console for additional error details`,
|
|
214
|
+
{ originalError: originalError.message }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
CsvParsingError,
|
|
221
|
+
InputValidationError,
|
|
222
|
+
ConfigurationError,
|
|
223
|
+
CsvFormatError,
|
|
224
|
+
FileOperationError,
|
|
225
|
+
JsonValidationError,
|
|
226
|
+
BrowserApiError
|
|
227
|
+
};
|