fast-csv
Version:
CSV parser and writer
151 lines (131 loc) • 5.23 kB
text/typescript
import { NodeStringDecoder, StringDecoder } from 'string_decoder';
import { Transform, TransformCallback } from 'stream';
import { ParserOptions } from './ParserOptions';
import { HeaderTransformer, RowTransformerValidator } from './transforms';
import { Parser } from './parser';
import {
RowArray,
RowTransformFunction, RowValidate, RowValidatorCallback,
} from './types';
export default class CsvParserStream extends Transform {
private readonly parserOptions: ParserOptions;
private readonly decoder: NodeStringDecoder;
private readonly parser: Parser;
private readonly headerTransformer: HeaderTransformer;
private readonly rowTransformerValidator: RowTransformerValidator;
private lines: string = '';
private rowCount: number = 0;
private endEmitted: boolean = false;
public constructor(parserOptions: ParserOptions) {
super({ objectMode: parserOptions.objectMode });
this.parserOptions = parserOptions;
this.parser = new Parser(parserOptions);
this.headerTransformer = new HeaderTransformer(parserOptions);
this.decoder = new StringDecoder(parserOptions.encoding);
this.rowTransformerValidator = new RowTransformerValidator();
}
public transform(transformFunction: RowTransformFunction): CsvParserStream {
this.rowTransformerValidator.rowTransform = transformFunction;
return this;
}
public validate(validateFunction: RowValidate): CsvParserStream {
this.rowTransformerValidator.rowValidator = validateFunction;
return this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public emit(event: string | symbol, ...rest: any[]): boolean {
if (event === 'end') {
if (!this.endEmitted) {
this.endEmitted = true;
super.emit('end', this.rowCount);
}
return false;
}
return super.emit(event, ...rest);
}
public _transform(data: Buffer, encoding: string, done: TransformCallback): void {
try {
const { lines } = this;
const newLine = (lines + this.decoder.write(data));
const rows = this.parse(newLine, true);
this.processRows(rows, done);
} catch (e) {
done(e);
}
}
public _flush(done: TransformCallback): void {
try {
const newLine = (this.lines + this.decoder.end());
const rows = this.parse(newLine, false);
this.processRows(rows, done);
} catch (e) {
done(e);
}
}
private parse(data: string, hasMoreData: boolean): string[][] {
if (!data) {
return [];
}
const { line, rows } = this.parser.parse(data, hasMoreData);
this.lines = line;
return rows;
}
private processRows(rows: string[][], cb: TransformCallback): void {
const rowsLength = rows.length;
const iterate = (i: number): void => {
if (i >= rowsLength) {
return cb();
}
const row = rows[i];
this.rowCount += 1;
const nextRowCount = this.rowCount;
return this.transformRow(row, (err, transformResult): void => {
if (err) {
this.rowCount -= 1;
return cb(err);
}
if (!transformResult) {
return cb(new Error('expected transform result'));
}
if (!transformResult.isValid) {
this.rowCount -= 1;
this.emit('data-invalid', transformResult.row, nextRowCount, transformResult.reason);
} else if (!transformResult.row) {
this.rowCount -= 1;
} else if (!this.parserOptions.objectMode) {
this.push(JSON.stringify(transformResult.row));
} else {
this.push(transformResult.row);
}
if ((i % 100) === 0) {
// incase the transform are sync insert a next tick to prevent stack overflow
setImmediate((): void => iterate(i + 1));
return undefined;
}
return iterate(i + 1);
});
};
iterate(0);
}
private transformRow(parsedRow: RowArray, cb: RowValidatorCallback): void {
try {
this.headerTransformer.transform(parsedRow, (err, withHeaders): void => {
if (err) {
return cb(err);
}
if (!withHeaders) {
return cb(new Error('Expected result from header transform'));
}
if (!withHeaders.isValid) {
return cb(null, { isValid: false, row: parsedRow });
}
if (withHeaders.row) {
return this.rowTransformerValidator.transformAndValidate(withHeaders.row, cb);
}
return cb(null, { row: null, isValid: true });
});
} catch (e) {
cb(e);
}
}
}