@@ -14,6 +14,12 @@ export type HeaderMap<T = any> = {
1414 [ K in string | number ] : keyof T | string
1515} ;
1616
17+ /**
18+ * Type for the merge function that transforms values during mapping
19+ * @template T - The type of the target object
20+ */
21+ export type MergeFn < T > = ( obj : Partial < T > , key : string , value : any ) => any ;
22+
1723/**
1824 * Options for retry logic
1925 */
@@ -30,6 +36,7 @@ export interface RetryOptions {
3036 * Creates functions to map between row arrays and structured objects
3137 * @template T - The type of the target object
3238 * @param headerMap - Mapping between array indices or header names and object properties
39+ * @param mergeFn - Optional function to customize how values are merged into the target object
3340 * @returns Object containing mapping functions
3441 * @example
3542 * ```typescript
@@ -44,11 +51,16 @@ export interface RetryOptions {
4451 * 'last_name': 'profile.lastName'
4552 * };
4653 *
47- * const { fromRowArr, toRowArr } = createHeaderMapFns<User>(headerMap);
54+ * // With custom merge function to trim strings
55+ * const { fromRowArr, toRowArr } = createHeaderMapFns<User>(
56+ * headerMap,
57+ * (obj, key, value) => typeof value === 'string' ? value.trim() : value
58+ * );
4859 * ```
4960 */
5061export function createHeaderMapFns < To extends Record < string , any > , RowArr extends any [ ] = any [ ] > (
51- headerMap : { [ K in number | string ] : keyof To | string }
62+ headerMap : HeaderMap < To > ,
63+ mergeFn ?: MergeFn < To >
5264) {
5365 // Validate the header map
5466 const validateHeaderMap = ( ) => {
@@ -84,15 +96,19 @@ export function createHeaderMapFns<To extends Record<string, any>, RowArr extend
8496 for ( let i = 0 ; i < rowArr . length ; i ++ ) {
8597 const toKey = headerMap [ i ] ;
8698 if ( toKey ) {
87- setPath ( to , toKey as string , rowArr [ i ] ) ;
99+ const targetPath = String ( toKey ) ; // Ensure it's a string
100+ const value = mergeFn ? mergeFn ( to , targetPath , rowArr [ i ] ) : rowArr [ i ] ;
101+ setPath ( to , targetPath , value ) ;
88102 }
89103 }
90104 } else if ( typeof rowArr === 'object' && rowArr !== null ) {
91105 // Handle object input
92106 for ( let [ key , value ] of Object . entries ( rowArr ) ) {
93- const toKey = headerMap ?. [ key ] ;
107+ const toKey = headerMap [ key ] ;
94108 if ( toKey ) {
95- setPath ( to , toKey as string , value ) ;
109+ const targetPath = String ( toKey ) ; // Ensure it's a string
110+ const processedValue = mergeFn ? mergeFn ( to , targetPath , value ) : value ;
111+ setPath ( to , targetPath , processedValue ) ;
96112 }
97113 }
98114 } else {
@@ -106,15 +122,25 @@ export function createHeaderMapFns<To extends Record<string, any>, RowArr extend
106122 * Convert a structured object back to a row array
107123 * @param objAfterMapWasApplied - Structured object
108124 * @param headers - Array of header names in order (required for header-based mapping)
125+ * @param transformFn - Optional function to transform values when converting from object to row
109126 * @returns Row data as an array
110127 * @example
111128 * ```typescript
112129 * const user = { id: '123', profile: { firstName: 'John', lastName: 'Doe' } };
113130 * const row = toRowArr(user, ['user_id', 'first_name', 'last_name']);
114131 * // row = ['123', 'John', 'Doe']
132+ *
133+ * // With transform function
134+ * const csvRow = toRowArr(user, ['user_id', 'first_name', 'last_name'],
135+ * (value) => typeof value === 'string' ? value.toUpperCase() : value);
136+ * // csvRow = ['123', 'JOHN', 'DOE']
115137 * ```
116138 */
117- toRowArr : ( objAfterMapWasApplied : To , headers : string [ ] = [ ] ) : RowArr => {
139+ toRowArr : (
140+ objAfterMapWasApplied : To ,
141+ headers : string [ ] = [ ] ,
142+ transformFn ?: ( value : any , key : string ) => any
143+ ) : RowArr => {
118144 // Validate input
119145 if ( ! objAfterMapWasApplied || typeof objAfterMapWasApplied !== 'object' ) {
120146 throw new CSVError ( 'Object must be a non-null object' ) ;
@@ -126,13 +152,12 @@ export function createHeaderMapFns<To extends Record<string, any>, RowArr extend
126152 if ( isIndexBased ) {
127153 // Index-based mapping
128154 for ( let [ rowIdx , path ] of Object . entries ( headerMap ) ) {
129- let value = getPath ( objAfterMapWasApplied , path as string ) ;
155+ const targetPath = String ( path ) ;
156+ let value = getPath ( objAfterMapWasApplied , targetPath ) ;
157+
130158 if ( typeof value !== 'undefined' ) {
131- // Handle special types
132- if ( value !== null && typeof value === 'object' && ! Array . isArray ( value ) ) {
133- // @ts -expect-error
134- value = JSON . stringify ( value ) ;
135- }
159+ // Process value
160+ value = processValueForOutput ( value , targetPath , transformFn ) ;
136161 row [ parseInt ( rowIdx ) ] = value ;
137162 }
138163 }
@@ -145,14 +170,14 @@ export function createHeaderMapFns<To extends Record<string, any>, RowArr extend
145170 for ( let i = 0 ; i < headers . length ; i ++ ) {
146171 const headerName = headers [ i ] ;
147172 const path = headerMap [ headerName ] ;
173+
148174 if ( path ) {
149- let value = getPath ( objAfterMapWasApplied , path as string ) ;
175+ const targetPath = String ( path ) ;
176+ let value = getPath ( objAfterMapWasApplied , targetPath ) ;
177+
150178 if ( typeof value !== 'undefined' ) {
151- // Handle special types
152- if ( value !== null && typeof value === 'object' && ! Array . isArray ( value ) ) {
153- // @ts -expect-error
154- value = JSON . stringify ( value ) ;
155- }
179+ // Process value
180+ value = processValueForOutput ( value , targetPath , transformFn ) ;
156181 row [ i ] = value ;
157182 }
158183 }
@@ -164,6 +189,45 @@ export function createHeaderMapFns<To extends Record<string, any>, RowArr extend
164189 } ;
165190}
166191
192+ /**
193+ * Helper function to process values for output, handling special cases
194+ * @param value - The value to process
195+ * @param key - The key or path associated with the value
196+ * @param transformFn - Optional function to transform the value
197+ * @returns Processed value
198+ */
199+ function processValueForOutput (
200+ value : any ,
201+ key : string ,
202+ transformFn ?: ( value : any , key : string ) => any
203+ ) : any {
204+ // Apply custom transformation if provided
205+ if ( transformFn ) {
206+ return transformFn ( value , key ) ;
207+ }
208+
209+ // Handle special types
210+ if ( value !== null && typeof value === 'object' && ! Array . isArray ( value ) ) {
211+ return JSON . stringify ( value ) ;
212+ }
213+
214+ return value ;
215+ }
216+
217+ /**
218+ * Helper function to convert array row to object row using headers
219+ * @param row - Array of values
220+ * @param headerRow - Array of header names
221+ * @returns Object with header names as keys
222+ */
223+ function arrayRowToObjectRow ( row : any [ ] , headerRow : string [ ] ) : Record < string , any > {
224+ const objRow : Record < string , any > = { } ;
225+ for ( let i = 0 ; i < row . length && i < headerRow . length ; i ++ ) {
226+ objRow [ headerRow [ i ] ] = row [ i ] ;
227+ }
228+ return objRow ;
229+ }
230+
167231/**
168232 * Transforms an array of arrays or objects into an array of structured objects
169233 * @template T - The type of the target object
@@ -190,7 +254,7 @@ export function createHeaderMapFns<To extends Record<string, any>, RowArr extend
190254 * { 0: 'id', 1: 'details.name', 2: 'details.price' }
191255 * );
192256 *
193- * // With custom merge function to convert price to number
257+ * // With custom merge function to convert price to number
194258 * const productsWithPriceAsNumber = arrayToObjArray<Product>(
195259 * csvData.slice(1),
196260 * { 0: 'id', 1: 'details.name', 2: 'details.price' },
@@ -208,7 +272,7 @@ export function arrayToObjArray<T extends Record<string, any>>(
208272 data : any [ ] ,
209273 headerMap : HeaderMap < T > ,
210274 headerRow ?: string [ ] ,
211- mergeFn ?: ( obj : Partial < T > , key : string , value : any ) => any
275+ mergeFn ?: MergeFn < T >
212276) : T [ ] {
213277 if ( ! Array . isArray ( data ) ) {
214278 throw new CSVError ( 'Data must be an array' ) ;
@@ -218,68 +282,53 @@ export function arrayToObjArray<T extends Record<string, any>>(
218282 return [ ] ;
219283 }
220284
221- const { fromRowArr } = createHeaderMapFns < T > ( headerMap ) ;
285+ const { fromRowArr } = createHeaderMapFns < T > ( headerMap , mergeFn ) ;
222286
223- // If first item is an array but keys are strings, we need header row
224- const firstItem = data [ 0 ] ;
225- const isArrayData = Array . isArray ( firstItem ) ;
226- const hasStringKeys = Object . keys ( headerMap ) . some ( k => isNaN ( Number ( k ) ) ) ;
287+ // Check if we need to validate header row
288+ validateHeadersIfNeeded ( data , headerMap , headerRow ) ;
227289
228- if ( isArrayData && hasStringKeys && ! headerRow ) {
229- throw new CSVError ( 'Header row is required for string-keyed header map with array data' ) ;
230- }
231-
232- if ( mergeFn ) {
233- return data . map ( row => {
234- // Convert row to an object if working with arrays and string header maps
235- let objRow : Record < string , any > = { } ;
236- if ( isArrayData && hasStringKeys && headerRow ) {
237- for ( let i = 0 ; i < row . length && i < headerRow . length ; i ++ ) {
238- objRow [ headerRow [ i ] ] = row [ i ] ;
239- }
240- } else if ( isArrayData ) {
241- // For array data with numeric indices
242- objRow = [ ...row ] ;
243- } else {
244- // For object data
245- objRow = { ...row } ;
246- }
247-
248- // Apply mappings with custom merge function
249- const result = { } as T ;
250- for ( const [ sourceKey , targetPath ] of Object . entries ( headerMap ) ) {
251- const key = isArrayData && ! hasStringKeys ? parseInt ( sourceKey ) : sourceKey ;
252- const value = isArrayData ? row [ key as number ] : objRow [ key as string ] ;
253- if ( value !== undefined ) {
254- const processedValue = mergeFn ( result , targetPath as string , value ) ;
255- setPath ( result , targetPath as string , processedValue ) ;
256- }
257- }
258- return result ;
259- } ) ;
260- }
261-
262290 return data . map ( row => {
263291 // If working with arrays and string header maps, convert to object first
292+ const isArrayData = Array . isArray ( row ) ;
293+ const hasStringKeys = Object . keys ( headerMap ) . some ( k => isNaN ( Number ( k ) ) ) ;
294+
264295 if ( isArrayData && hasStringKeys && headerRow ) {
265- const objRow : Record < string , any > = { } ;
266- for ( let i = 0 ; i < row . length && i < headerRow . length ; i ++ ) {
267- objRow [ headerRow [ i ] ] = row [ i ] ;
268- }
296+ const objRow = arrayRowToObjectRow ( row , headerRow ) ;
269297 return fromRowArr ( objRow ) ;
270298 }
271299
272300 return fromRowArr ( row ) ;
273301 } ) ;
274302}
275303
304+ /**
305+ * Validates that header row is provided when needed
306+ * @param data - The data array
307+ * @param headerMap - The header mapping configuration
308+ * @param headerRow - The header row (optional)
309+ */
310+ function validateHeadersIfNeeded < T > (
311+ data : any [ ] ,
312+ headerMap : HeaderMap < T > ,
313+ headerRow ?: string [ ]
314+ ) : void {
315+ const firstItem = data [ 0 ] ;
316+ const isArrayData = Array . isArray ( firstItem ) ;
317+ const hasStringKeys = Object . keys ( headerMap ) . some ( k => isNaN ( Number ( k ) ) ) ;
318+
319+ if ( isArrayData && hasStringKeys && ! headerRow ) {
320+ throw new CSVError ( 'Header row is required for string-keyed header map with array data' ) ;
321+ }
322+ }
323+
276324/**
277325 * Transforms an array of structured objects into an array of arrays
278326 * @template T - The type of the source object
279327 * @param data - Array of structured objects to transform
280328 * @param headerMap - Mapping between object properties and array indices or header names
281329 * @param headers - Optional array of headers (required for header-based mapping)
282330 * @param includeHeaders - Whether to include headers as the first row
331+ * @param transformFn - Optional function to transform values when converting to rows
283332 * @returns Array of arrays
284333 * @example
285334 * ```typescript
@@ -305,7 +354,8 @@ export function objArrayToArray<T extends Record<string, any>>(
305354 data : T [ ] ,
306355 headerMap : HeaderMap ,
307356 headers : string [ ] = [ ] ,
308- includeHeaders : boolean = false
357+ includeHeaders : boolean = false ,
358+ transformFn ?: ( value : any , key : string ) => any
309359) : any [ ] [ ] {
310360 if ( ! Array . isArray ( data ) ) {
311361 throw new CSVError ( 'Data must be an array' ) ;
@@ -318,14 +368,14 @@ export function objArrayToArray<T extends Record<string, any>>(
318368 // Create an inverse header map
319369 const inverseMap : HeaderMap < T > = { } ;
320370 for ( const [ key , value ] of Object . entries ( headerMap ) ) {
321- if ( typeof value === 'string' ) {
371+ if ( typeof value === 'string' || typeof value === 'number' ) {
322372 inverseMap [ value ] = key ;
323373 }
324374 }
325375
326376 const { toRowArr } = createHeaderMapFns < T > ( inverseMap ) ;
327377
328- const rows = data . map ( obj => toRowArr ( obj , headers ) ) ;
378+ const rows = data . map ( obj => toRowArr ( obj , headers , transformFn ) ) ;
329379
330380 if ( includeHeaders && headers . length > 0 ) {
331381 return [ headers , ...rows ] ;
@@ -366,7 +416,7 @@ export function groupByField<T extends Record<string, any>>(
366416 field : string
367417) : Record < string , T [ ] > {
368418 return data . reduce ( ( groups , item ) => {
369- const key = String ( getPath ( item , field ) || 'undefined' ) ;
419+ const key = String ( getPath ( item , field ) ?? 'undefined' ) ;
370420 if ( ! groups [ key ] ) {
371421 groups [ key ] = [ ] ;
372422 }
0 commit comments