22 * @fileoverview Utilities for data transformation, including header mapping and array processing
33 */
44
5- import { get as getPath , set as setPath } from 'lodash' ;
5+ import getPath from 'lodash/get' ;
6+ import setPath from 'lodash/set' ;
67import { CSVError } from './index' ;
78
89/**
910 * A path string using dot notation to access nested properties
10- * @template T - The target object type
1111 */
12- export type Path < T = any > = string ;
12+ export type Path = string ;
1313
1414/**
1515 * A function used for custom header mapping operations
@@ -24,14 +24,13 @@ export type HeaderMapFn<T = any> = (
2424
2525/**
2626 * Configuration for mapping multiple CSV columns to a single array property
27- * @template T - The target object type
2827 */
29- export interface CsvToArrayConfig < T = any > {
28+ export interface CsvToArrayConfig {
3029 /** Type identifier for the mapping configuration */
3130 _type : 'csvToTargetArray' ;
3231
3332 /** The target array property path in dot notation */
34- targetPath : Path < T > ;
33+ targetPath : Path ;
3534
3635 /** Option A: Explicit list of CSV column names in order */
3736 sourceCsvColumns ?: string [ ] ;
@@ -74,9 +73,9 @@ export interface ObjectArrayToCsvConfig {
7473 * @template T - The target object type
7574 */
7675export type HeaderMapValue < T = any > =
77- | Path < T > // Direct path like 'profile.name'
76+ | Path // Direct path like 'profile.name'
7877 | HeaderMapFn < T > // Custom mapping function
79- | CsvToArrayConfig < T > // CSV columns -> object array property configuration
78+ | CsvToArrayConfig // CSV columns -> object array property configuration
8079 | ObjectArrayToCsvConfig ; // Object array property -> CSV columns configuration
8180
8281/**
@@ -162,25 +161,33 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
162161 if ( ! headerMap || typeof headerMap !== 'object' ) {
163162 throw new CSVError ( 'Header map must be a non-null object' ) ;
164163 }
165-
164+
166165 if ( Object . keys ( headerMap ) . length === 0 ) {
167166 throw new CSVError ( 'Header map cannot be empty' ) ;
168167 }
169168 } ;
170-
169+
170+ // Create inverse map for toRowArr
171+ const inverseMap : HeaderMap < any > = { } ;
172+ for ( const [ key , value ] of Object . entries ( headerMap ) ) {
173+ if ( typeof value === 'string' ) {
174+ inverseMap [ value ] = isNaN ( Number ( key ) ) ? key : Number ( key ) ;
175+ }
176+ }
177+
171178 // Helper function to ensure array exists at path
172179 const ensureArrayAtPath = ( obj : any , path : string ) : any [ ] => {
173180 let current = obj ;
174181 const parts = path . split ( '.' ) ;
175-
182+
176183 // Navigate to the parent object
177184 for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
178185 if ( ! current [ parts [ i ] ] ) {
179186 current [ parts [ i ] ] = { } ;
180187 }
181188 current = current [ parts [ i ] ] ;
182189 }
183-
190+
184191 // Create array if it doesn't exist
185192 const lastPart = parts [ parts . length - 1 ] ;
186193 if ( ! current [ lastPart ] ) {
@@ -189,10 +196,10 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
189196 // Convert to array if not already one
190197 current [ lastPart ] = [ current [ lastPart ] ] ;
191198 }
192-
199+
193200 return current [ lastPart ] as any [ ] ;
194201 } ;
195-
202+
196203 // Validate once during creation
197204 validateHeaderMap ( ) ;
198205
@@ -211,14 +218,15 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
211218 fromRowArr : ( rowArr : RowArr | Record < string , any > , allHeaders ?: string [ ] ) : To & Record < string , any > => {
212219 const to = { } as To & Record < string , any > ;
213220 const handledCsvHeaders = new Set < string | number > ( ) ;
214-
221+
222+ // Determine if header map uses string keys
223+ const hasStringKeys = Object . keys ( headerMap ) . some ( k => isNaN ( Number ( k ) ) ) ;
224+
215225 // Convert array to object if needed and provide headers
216226 let rowObj : Record < string , any > ;
217227 if ( Array . isArray ( rowArr ) ) {
218228 // If we're dealing with a header-based mapping but have array data,
219229 // convert the array to an object using header names
220- const hasStringKeys = Object . keys ( headerMap ) . some ( k => isNaN ( Number ( k ) ) ) ;
221-
222230 if ( hasStringKeys && allHeaders ) {
223231 rowObj = { } ;
224232 for ( let i = 0 ; i < rowArr . length && i < allHeaders . length ; i ++ ) {
@@ -240,7 +248,7 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
240248
241249 // Check if rule is a CsvToArrayConfig
242250 if ( rule && typeof rule === 'object' && ( rule as any ) . _type === 'csvToTargetArray' ) {
243- const arrayRule = rule as CsvToArrayConfig < To > ;
251+ const arrayRule = rule as CsvToArrayConfig ;
244252 const collectedItems : { value : any ; sortKey ?: string | number ; sourceHeader : string | number } [ ] = [ ] ;
245253
246254 // Determine which headers to scan for matches
@@ -351,11 +359,13 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
351359 } ;
352360
353361 // Process all source keys
354- if ( Array . isArray ( rowArr ) ) {
362+ if ( Array . isArray ( rowArr ) && ! ( hasStringKeys && allHeaders ) ) {
363+ // Use array indices only when we didn't convert to object
355364 for ( let i = 0 ; i < rowArr . length ; i ++ ) {
356365 processHeaderMapping ( i , rowArr [ i ] ) ;
357366 }
358367 } else {
368+ // Use object keys when we have an object (either input was object or converted from array)
359369 for ( const [ key , value ] of Object . entries ( rowObj ) ) {
360370 processHeaderMapping ( key , value ) ;
361371 }
@@ -394,23 +404,23 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
394404 // Determine if mapping is index-based
395405 const isIndexBased = Object . keys ( headerMap ) . every ( k => ! isNaN ( Number ( k ) ) ) ;
396406
397- // Process array-to-csv mappings first
407+ // Process array-to-csv mappings first (using original headerMap)
398408 for ( const objectPath in headerMap ) {
399409 const mappingRule = headerMap [ objectPath ] ;
400-
410+
401411 // Handle array-to-csv mappings
402412 if ( typeof mappingRule === 'object' && mappingRule !== null && ( mappingRule as any ) . _type === 'targetArrayToCsv' ) {
403413 const arrayRule = mappingRule as ObjectArrayToCsvConfig ;
404414 const sourceArray = getPath ( objAfterMapWasApplied , objectPath ) ;
405-
415+
406416 if ( Array . isArray ( sourceArray ) ) {
407417 if ( arrayRule . targetCsvColumns ) {
408418 // Fixed column names
409419 for ( let i = 0 ; i < arrayRule . targetCsvColumns . length ; i ++ ) {
410420 const csvColName = arrayRule . targetCsvColumns [ i ] ;
411421 const value = i < sourceArray . length ? sourceArray [ i ] : null ;
412422 const outputValue = value ?? arrayRule . emptyCellOutput ?? '' ;
413-
423+
414424 if ( isIndexBased ) {
415425 // For index-based mapping, find the index of this column name
416426 for ( const [ idx , headerName ] of Object . entries ( headers ) ) {
@@ -425,15 +435,15 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
425435 }
426436 } else if ( arrayRule . targetCsvColumnPrefix ) {
427437 // Dynamic column names with prefix
428- const limit = arrayRule . maxColumns !== undefined
429- ? Math . min ( arrayRule . maxColumns , sourceArray . length )
438+ const limit = arrayRule . maxColumns !== undefined
439+ ? Math . min ( arrayRule . maxColumns , sourceArray . length )
430440 : sourceArray . length ;
431-
441+
432442 for ( let i = 0 ; i < limit ; i ++ ) {
433443 const csvColName = `${ arrayRule . targetCsvColumnPrefix } ${ i + 1 } ` ;
434444 const value = sourceArray [ i ] ;
435445 const outputValue = value ?? arrayRule . emptyCellOutput ?? '' ;
436-
446+
437447 if ( isIndexBased ) {
438448 // For index-based mapping, find the index of this column name
439449 for ( const [ idx , headerName ] of Object . entries ( headers ) ) {
@@ -447,40 +457,52 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
447457 }
448458 }
449459 }
450-
460+
451461 handledPaths . add ( objectPath ) ;
452462 }
453-
463+
454464 continue ;
455465 }
456-
457- // Skip non-array special rules during toRowArr
466+ }
467+
468+ // Process standard mappings
469+ for ( const objectPath in headerMap ) {
470+ const mappingRule = headerMap [ objectPath ] ;
471+
472+ // Skip array mappings (already handled above)
473+ if ( typeof mappingRule === 'object' && mappingRule !== null && ( mappingRule as any ) . _type === 'targetArrayToCsv' ) {
474+ continue ;
475+ }
476+
477+ // Skip csvToTargetArray mappings
458478 if ( typeof mappingRule === 'object' && mappingRule !== null && ( mappingRule as any ) . _type === 'csvToTargetArray' ) {
459479 continue ;
460480 }
461481
462482 // Handle standard direct mappings and function mappings
463483 if ( typeof mappingRule === 'string' ) {
464- // Direct path mapping: Field path -> CSV header name
465- const csvHeaderName = mappingRule ;
466- let value = getPath ( objAfterMapWasApplied , objectPath ) ;
467-
484+ let value = getPath ( objAfterMapWasApplied , mappingRule ) ;
485+
468486 if ( value !== undefined ) {
469487 value = processValueForOutput ( value , objectPath , transformFn ) ;
470-
488+
471489 if ( isIndexBased ) {
472- // For index-based mapping, find the numeric index of this column name
473- for ( const [ idx , headerName ] of Object . entries ( headers ) ) {
474- if ( headerName === csvHeaderName ) {
475- row [ Number ( idx ) ] = value ;
476- break ;
477- }
478- }
490+ // For index-based mapping, objectPath is the index
491+ row [ Number ( objectPath ) ] = value ;
479492 } else {
480- rowObj [ csvHeaderName ] = value ;
493+ // For header-based mapping, objectPath is the CSV header name
494+ rowObj [ objectPath ] = value ;
481495 }
482496 }
483-
497+
498+ handledPaths . add ( objectPath ) ;
499+ } else if ( typeof mappingRule === 'number' ) {
500+ // This shouldn't happen for standard mappings, but handle just in case
501+ let value = getPath ( objAfterMapWasApplied , objectPath ) ;
502+ if ( value !== undefined ) {
503+ value = processValueForOutput ( value , objectPath , transformFn ) ;
504+ row [ mappingRule ] = value ;
505+ }
484506 handledPaths . add ( objectPath ) ;
485507 } else if ( typeof mappingRule === 'function' ) {
486508 // Function mapping
@@ -499,9 +521,9 @@ export function createHeaderMapFns<To, RowArr extends any[] = any[]>(
499521 // For header-based output, convert the object to an array
500522 if ( ! isIndexBased ) {
501523 if ( ! headers || headers . length === 0 ) {
502- throw new CSVError ( 'Headers array is required for header-based mapping' ) ;
524+ headers = Object . keys ( headerMap ) ;
503525 }
504-
526+
505527 for ( let i = 0 ; i < headers . length ; i ++ ) {
506528 const headerName = headers [ i ] ;
507529 row [ i ] = rowObj [ headerName ] !== undefined ? rowObj [ headerName ] : '' ;
0 commit comments