(content, schema = {})
| 44 | * @returns {{ rows: Object[]|null, error: string|null }} |
| 45 | */ |
| 46 | export function parseImportCSV(content, schema = {}) { |
| 47 | if (!content || content.trim().length === 0) { |
| 48 | return { rows: null, error: 'File is empty.' }; |
| 49 | } |
| 50 | |
| 51 | const lines = parseCSVLines(content); |
| 52 | if (lines.length === 0) { |
| 53 | return { rows: null, error: 'File is empty.' }; |
| 54 | } |
| 55 | |
| 56 | const headers = lines[0]; |
| 57 | if (lines.length < 2) { |
| 58 | return { rows: null, error: 'No data rows found. The file contains only headers.' }; |
| 59 | } |
| 60 | |
| 61 | const rows = []; |
| 62 | for (let i = 1; i < lines.length; i++) { |
| 63 | const values = lines[i]; |
| 64 | const row = {}; |
| 65 | for (let j = 0; j < headers.length; j++) { |
| 66 | const header = headers[j]; |
| 67 | const raw = j < values.length ? values[j] : ''; |
| 68 | if (raw === '') { |
| 69 | continue; // omit empty cells |
| 70 | } |
| 71 | const colSchema = schema[header]; |
| 72 | const type = colSchema ? colSchema.type : 'String'; |
| 73 | const converted = convertCSVValue(raw, type, colSchema); |
| 74 | if (converted === undefined) { |
| 75 | continue; |
| 76 | } |
| 77 | if (type === 'Number' && Number.isNaN(converted)) { |
| 78 | return { |
| 79 | rows: null, |
| 80 | error: `Invalid number in row ${i}, column "${header}".`, |
| 81 | }; |
| 82 | } |
| 83 | if (type === 'Boolean' && raw.toLowerCase() !== 'true' && raw.toLowerCase() !== 'false') { |
| 84 | return { |
| 85 | rows: null, |
| 86 | error: `Invalid boolean in row ${i}, column "${header}".`, |
| 87 | }; |
| 88 | } |
| 89 | row[header] = converted; |
| 90 | } |
| 91 | if (Object.keys(row).length === 0) { |
| 92 | continue; |
| 93 | } |
| 94 | rows.push(row); |
| 95 | } |
| 96 | |
| 97 | return { rows, error: null }; |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Parse CSV content into an array of arrays (rows of fields). |
no test coverage detected