/**
* Options to configure `CSVNormalizer`.
* @typedef {Object} CSVNormalizerOptions
+ * @property {boolean} [useLiteralNames=false] Set to `true` to use the literal `name` value to match with literal input CSV header
+ * names. Otherwise, both `name` and the input CSV header name will be normalized (i.e., trimmed and converted to camelCase).
+ * The default value is `false`.
* @property {boolean} [passthroughEmptyRows=false] Set to `true` to send empty rows (i.e., rows where all field values are
* `''`) downstream. Otherwise, empty rows will be removed. The default value is `false`.
* @property {boolean} [typeCastOnly=false] Set to `true` to NOT apply any error correction to 'number', `bigint`, or `date`
export class CSVNormalizer extends TransformStream {
#columns = [];
#firstChunk = true;
+ #useLiteralNames;
#passthroughEmptyRows;
#typeCastOnly;
transform: (chunk, controller) => this.#transform(chunk, controller)
});
+ this.#useLiteralNames = options.useLiteralNames ?? false;
this.#passthroughEmptyRows = options.passthroughEmptyRows ?? false;
this.#typeCastOnly = options.typeCastOnly ?? false;
normalizedType = 'string';
}
this.#columns.push({
- name: toCamelCase(name),
+ name: this.#useLiteralNames ? name : toCamelCase(name),
type: normalizedType,
displayName,
defaultValue,
// Assume first row is headers and use it to prepare the columns object
// Note: the headers row is NOT forwarded downstream
if (this.#firstChunk) {
- const normalizedRow = row.map(f => toCamelCase(f));
+ const normalizedRow = this.#useLiteralNames ? row : row.map(f => toCamelCase(f));
let i = 0;
for (const header of normalizedRow) {
const col = this.#columns.find(c => c.name === header);
['a', 'b']
]));
});
+ test('removes columns that have the same normalized name (last column prevails)', { concurrency: true }, async () => {
+ await createCSVMockStream([
+ ['columnA', 'column A', 'column-A'],
+ ['a1', 'a2', 'a3']
+ ])
+ .pipeThrough(new CSVNormalizer([
+ { name: 'columnA' }
+ ]))
+ .pipeThrough(new CSVDenormalizer())
+ .pipeTo(csvStreamEqualWritable([
+ ['columnA'],
+ ['a3']
+ ]));
+ });
+ test('can match by literal column names', { concurrency: true }, async () => {
+ await createCSVMockStream([
+ ['columnA', 'column A', 'column-A'],
+ ['a1', 'a2', 'a3']
+ ])
+ .pipeThrough(new CSVNormalizer([
+ { name: 'columnA' },
+ { name: 'column A' },
+ { name: 'column-A' }
+ ], { useLiteralNames: true }))
+ .pipeThrough(new CSVDenormalizer())
+ .pipeTo(csvStreamEqualWritable([
+ ['columnA', 'column A', 'column-A'],
+ ['a1', 'a2', 'a3']
+ ]));
+ });
+ test('removes columns that have the same literal name (last column prevails)', { concurrency: true }, async () => {
+ await createCSVMockStream([
+ ['columnA', 'columnA', 'columnA'],
+ ['a1', 'a2', 'a3']
+ ])
+ .pipeThrough(new CSVNormalizer([
+ { name: 'columnA' }
+ ], { useLiteralNames: true }))
+ .pipeThrough(new CSVDenormalizer())
+ .pipeTo(csvStreamEqualWritable([
+ ['columnA'],
+ ['a3']
+ ]));
+ });
test('can remove empty rows', { concurrency: true }, async () => {
await createCSVMockStream([
['columnA', 'columnB'],