(params: {
csvHeaders: string[]
mapping: CsvHeaderMapping
tableSchema: TableSchema
})
| 272 | * are not covered by the CSV. Returns the normalized header -> column map. |
| 273 | */ |
| 274 | export function validateMapping(params: { |
| 275 | csvHeaders: string[] |
| 276 | mapping: CsvHeaderMapping |
| 277 | tableSchema: TableSchema |
| 278 | }): CsvMappingValidationResult { |
| 279 | const { csvHeaders, mapping, tableSchema } = params |
| 280 | const columnByName = new Map(tableSchema.columns.map((c) => [c.name, c])) |
| 281 | |
| 282 | const unknownHeaders = Object.keys(mapping).filter((h) => !csvHeaders.includes(h)) |
| 283 | if (unknownHeaders.length > 0) { |
| 284 | throw new CsvImportValidationError( |
| 285 | `Mapping references unknown CSV headers: ${unknownHeaders.join(', ')}`, |
| 286 | { unknownHeaders } |
| 287 | ) |
| 288 | } |
| 289 | |
| 290 | const invalidTargets = Object.entries(mapping).filter( |
| 291 | ([, target]) => target !== null && typeof target !== 'string' |
| 292 | ) |
| 293 | if (invalidTargets.length > 0) { |
| 294 | throw new CsvImportValidationError( |
| 295 | `Mapping values must be a column name (string) or null, got: ${invalidTargets |
| 296 | .map(([header]) => header) |
| 297 | .join(', ')}` |
| 298 | ) |
| 299 | } |
| 300 | |
| 301 | const targetsSeen = new Map<string, string[]>() |
| 302 | const unknownColumns: string[] = [] |
| 303 | const effectiveMap = new Map<string, string>() |
| 304 | const skippedHeaders: string[] = [] |
| 305 | |
| 306 | for (const header of csvHeaders) { |
| 307 | const target = header in mapping ? mapping[header] : undefined |
| 308 | if (target === null || target === undefined) { |
| 309 | skippedHeaders.push(header) |
| 310 | continue |
| 311 | } |
| 312 | if (!columnByName.has(target)) { |
| 313 | unknownColumns.push(target) |
| 314 | continue |
| 315 | } |
| 316 | const existing = targetsSeen.get(target) ?? [] |
| 317 | existing.push(header) |
| 318 | targetsSeen.set(target, existing) |
| 319 | effectiveMap.set(header, target) |
| 320 | } |
| 321 | |
| 322 | if (unknownColumns.length > 0) { |
| 323 | throw new CsvImportValidationError( |
| 324 | `Mapping references columns that do not exist on the table: ${unknownColumns.join(', ')}`, |
| 325 | { unknownColumns } |
| 326 | ) |
| 327 | } |
| 328 | |
| 329 | const duplicateTargets = [...targetsSeen.entries()] |
| 330 | .filter(([, headers]) => headers.length > 1) |
| 331 | .map(([col]) => col) |
no test coverage detected