UNPKG

fast-csv

Version:
151 lines (131 loc) 5.23 kB
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); } } }