Skip to content

Commit bab3379

Browse files
authored
Merge pull request #249 from mrodrig/feat/add-key-expansion-functionality
Add key expansion capability
2 parents ca7ada3 + fb57a7b commit bab3379

File tree

8 files changed

+110
-7
lines changed

8 files changed

+110
-7
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,19 @@ Returns the CSV `string` or rejects with an `Error` if there was an issue.
128128
* Note: This may result in CSV output that does not map back exactly to the original JSON. See #102 for more information.
129129
* `keys` - Array - Specify the keys that should be converted.
130130
* Default: These will be auto-detected from your data by default.
131-
* Keys can either be specified as a String representing the key path that should be converted, or as an Object with the `field` property specifying the path. When specifying keys as an Object, you can also optionally specify a `title` which will be used for that column in the header. The list specified can contain a combination of Objects and Strings.
132-
* `[ 'key1', 'key2', ... ]`
133-
* `[ { field: 'key1', title: 'Key 1' }, { field: 'key2' }, 'key3', ... ]`
131+
* Keys can either be specified as a String representing the key path that should be converted, or as an Object of the following format:
132+
```javascript
133+
{
134+
"field": "string", // required
135+
"title": "string", // optional
136+
"wildcardMatch": false, // optional - default: false
137+
}
138+
```
139+
* When specifying keys as an Object, the `field` property specifies the key path, while `title` specifies a more human readable field heading. Additionally, the `wildcardMatch` option allows you to optionally specify that all auto-detected fields with the specified field prefix should be included in the CSV. The list specified can contain a combination of Objects and Strings.
140+
* Examples:
141+
* `[ 'key1', 'key2', ... ]`
142+
* `[ 'key1', { field: 'key2', wildcardMatch: true }]`
143+
* `[ { field: 'key1', title: 'Key 1' }, { field: 'key2' }, 'key3', ... ]`
134144
* Key Paths - If you are converting a nested object (ie. {info : {name: 'Mike'}}), then set this to ['info.name']
135145
* `parseValue` - Function - Specify how values should be converted into CSV format. This function is provided a single field value at a time and must return a `String`. The built-in parsing method is provided as the second argument for cases where default parsing is preferred.
136146
* Default: A built-in method is used to parse out a variety of different value types to well-known formats.

src/json2csv.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,26 +172,60 @@ export const Json2Csv = function(options: FullJson2CsvOptions) {
172172
});
173173
}
174174

175+
function extractWildcardMatchKeys() {
176+
if (!options.keys) return [];
177+
178+
return options.keys.flatMap(item => {
179+
if (typeof item === 'string') {
180+
// Exclude plain strings that were passed in options.keys
181+
return [];
182+
} else if (item?.wildcardMatch) {
183+
// Return "field" value for objects with wildcardMatch: true
184+
return item.field;
185+
}
186+
// Exclude other objects
187+
return [];
188+
});
189+
}
190+
175191
/**
176192
* Retrieve the headings for all documents and return it.
177193
* This checks that all documents have the same schema.
178194
*/
179195
function retrieveHeaderFields(data: object[]) {
196+
const wildcardMatchKeys = extractWildcardMatchKeys();
180197
const keyStrings = convertKeysToHeaderFields();
198+
const fieldNames = getFieldNameList(data);
199+
const processed = processSchemas(fieldNames);
181200

182201
if (options.keys) {
183202
options.keys = keyStrings;
203+
204+
const matchedKeys = keyStrings.flatMap((userProvidedKey) => {
205+
// If this is not a wildcard matched key, then just return and include it in the resulting key list
206+
if (!wildcardMatchKeys.includes(userProvidedKey)) {
207+
return userProvidedKey;
208+
}
184209

185-
if (!options.unwindArrays) {
186-
const filtered = filterExcludedKeys(keyStrings);
210+
// Otherwise, identify all detected keys that match with the provided wildcard key:
211+
const matches = [];
212+
const regex = new RegExp(`^${userProvidedKey}`);
213+
214+
for (const detectedKey of processed) {
215+
if (userProvidedKey === detectedKey || detectedKey.match(regex)) {
216+
matches.push(detectedKey);
217+
}
218+
}
219+
return matches;
220+
});
187221

222+
if (!options.unwindArrays) {
223+
const filtered = filterExcludedKeys(matchedKeys);
188224

189225
return sortHeaderFields(filtered);
190226
}
191227
}
192228

193-
const fieldNames = getFieldNameList(data);
194-
const processed = processSchemas(fieldNames);
195229
const filtered = filterExcludedKeys(processed);
196230

197231
return sortHeaderFields(filtered);

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface DelimiterOptions {
1212
export type KeysList = (string | {
1313
field: string;
1414
title?: string;
15+
wildcardMatch?: boolean;
1516
})[];
1617

1718
interface SharedConverterOptions {

test/config/testCsvFilesList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const csvFileConfig = [
5151
{key: 'nestedNotUnwoundObjects', file: '../data/csv/nestedNotUnwoundObjects.csv'},
5252
{key: 'newlineWithWrapDelimiters', file: '../data/csv/newlineWithWrapDelimiters.csv'},
5353
{key: 'excludeKeyPattern', file: '../data/csv/excludeKeyPattern.csv'},
54+
{key: 'wildcardMatch', file: '../data/csv/wildcardMatch.csv'},
5455
];
5556

5657
function readCsvFile(filePath: string) {

test/config/testJsonFilesList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ export default {
4444
falsyValues: require('../data/json/falsyValues.json'),
4545
newlineWithWrapDelimiters: require('../data/json/newlineWithWrapDelimiters'),
4646
excludeKeyPattern: require('../data/json/excludeKeyPattern'),
47+
wildcardMatch: require('../data/json/wildcardMatch.json'),
4748
};

test/data/csv/wildcardMatch.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
foo,bar,baz.a,baz.array
2+
foo,bar,a,c

test/data/json/wildcardMatch.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[
2+
{
3+
"foo": "foo",
4+
"bar": "bar",
5+
"baz": {
6+
"a": "a",
7+
"b": "b",
8+
"array": "c"
9+
}
10+
}
11+
]

test/json2csv.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,13 +394,15 @@ export function runTests() {
394394
});
395395

396396
it('should allow titles to be specified for certain keys, but not others when not unwinding arrays', () => {
397+
console.log('START EXEC----');
397398
const csv = json2csv(jsonTestData.unwind, {
398399
unwindArrays: false,
399400
keys: [
400401
{field: 'data.category', title: 'Category'},
401402
'data.options.name'
402403
]
403404
});
405+
console.log('END EXEC----');
404406
assert.equal(csv, csvTestData.withSpecifiedKeys.replace('data.category,data.options.name', 'Category,data.options.name'));
405407
});
406408

@@ -416,13 +418,16 @@ export function runTests() {
416418
});
417419

418420
it('should allow titles to be specified', () => {
421+
console.log('START EXEC----');
422+
419423
const csv = json2csv(jsonTestData.unwind, {
420424
unwindArrays: false,
421425
keys: [
422426
{field: 'data.category', title: 'Category'},
423427
{field: 'data.options.name', title: 'Option Name'}
424428
]
425429
});
430+
console.log('END EXEC----');
426431
assert.equal(csv, csvTestData.withSpecifiedKeys.replace('data.category,data.options.name', 'Category,Option Name'));
427432
});
428433

@@ -535,6 +540,44 @@ export function runTests() {
535540
assert.equal(csv, csvTestData.nestedDotKeys.replace(/\\\./g, '.'));
536541
});
537542

543+
// Test case for #247
544+
it('should not escape nested dots in keys with nested dots in them if turned off via the option', () => {
545+
const csv = json2csv(jsonTestData.wildcardMatch, {
546+
keys: ['foo', 'bar', 'baz.a', 'baz.array'],
547+
});
548+
assert.equal(csv, csvTestData.wildcardMatch);
549+
});
550+
551+
// Test case for #247
552+
it('should not escape nested dots in keys with nested dots in them if turned off via the option', () => {
553+
const csv = json2csv(jsonTestData.wildcardMatch, {
554+
keys: ['foo', 'bar', { field: 'baz.a', wildcardMatch: true }],
555+
});
556+
assert.equal(csv, csvTestData.wildcardMatch);
557+
});
558+
559+
// Test case for #247
560+
it('should not escape nested dots in keys with nested dots in them if turned off via the option', () => {
561+
const updatedCsv = csvTestData.wildcardMatch.replace('baz.a,baz.array', 'baz.a,baz.b,baz.array')
562+
.replace('a,c', 'a,b,c');
563+
564+
const csv = json2csv(jsonTestData.wildcardMatch, {
565+
keys: ['foo', 'bar', { field: 'baz', wildcardMatch: true }],
566+
});
567+
assert.equal(csv, updatedCsv);
568+
});
569+
570+
// Test case for #247
571+
it('should not escape nested dots in keys with nested dots in them if turned off via the option', () => {
572+
const updatedCsv = csvTestData.wildcardMatch.replace('foo,bar,baz.a,baz.array', 'foo,baz.a,baz.array,bar')
573+
.replace('foo,bar,a,c', 'foo,a,c,bar');
574+
575+
const csv = json2csv(jsonTestData.wildcardMatch, {
576+
keys: ['foo', { field: 'baz.a', wildcardMatch: true }, 'bar'],
577+
});
578+
assert.equal(csv, updatedCsv);
579+
});
580+
538581
it('should use a custom value parser function when provided', () => {
539582
const updatedCsv = csvTestData.trimmedFields.split('\n');
540583
const textRow = 'Parsed Value,Parsed Value,Parsed Value,Parsed Value,Parsed Value';

0 commit comments

Comments
 (0)