All files / src/csv-parser csv-parser.ts

21.21% Statements 7/33
0% Branches 0/26
0% Functions 0/12
16.67% Lines 5/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85    1x 1x 1x   1x                                 1x                                                                                                                          
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { bind, BindingScope } from '@loopback/core';
import { Readable } from "stream";
import { parse } from 'fast-csv';
import { Options } from '@loopback/repository';
import { HttpErrors } from '@loopback/rest';
import { UploadFileType } from './types';
 
export interface RecordsCreator {
  headers: string[];
  validateRowsCsv(row: any): Promise<void>,
  create(data: any, options?: Options): Promise<any>,
}
 
type ResultParser<T> = {
  isValid: boolean,
  rowNumber?: number,
  reason?: string,
  data?: T
}
 
@bind({ scope: BindingScope.TRANSIENT })
export class CSVParser<T> {
  constructor(private recordsCreator: RecordsCreator) { }
 
  public parseFromBuffer(buffer: Buffer): Promise<ResultParser<T>[]> {
    return new Promise((resolve, reject) => {
      const results: ResultParser<T>[] = [];
      const readable = Readable.from(buffer);
      readable
        .pipe(parse({ headers: this.recordsCreator.headers, renameHeaders: true }))
        .validate((row, cb): void => {
          setImmediate(async () => {
            try {
              await this.recordsCreator.validateRowsCsv(row);
              cb(null, true);
            } catch (error) {
              const msg = error?.details?.length ?
                `${error?.details[0].message}: ${error.details[0].path?.replace('/', '')}` : error.message;
              cb(null, false, msg);
            }
          });
        })
        .on('error', error => reject(error))
        .on('data', (row: T) => {
          results.push({ isValid: true, data: row });
        })
        .on('data-invalid', (row: T, rowNumber: number, reason: string) => {
          results.push({ isValid: false, reason, rowNumber })
        })
        .on('end', () => {
          resolve(results);
        });
    });
  }
 
  /**
   * Creates records from csv files
   * @param [uploadedFiles]
   * @returns
   */
  async createRecordsFromCSV(uploadedFiles: UploadFileType, options: Options) {
    if (!uploadedFiles?.length) { 
      throw new HttpErrors.BadRequest(`Missing csv file!`);
    }
 
    const csvData = await this.parseFromBuffer((uploadedFiles as Express.Multer.File[])[0].buffer);
    const pTasks = csvData.map(({ isValid, data, reason }, idx) => this.processCsvRow(isValid, data, reason, idx, options));
 
    return Promise.all(pTasks);
  }
 
  private async processCsvRow(isValid: boolean, data: any, reason: string | undefined, idx: number, options: Options) {
    try {
      if (isValid) {
        await this.recordsCreator.create(data, options);
      }
      return { is_valid: isValid, reason, row_number: idx + 1 };
    } catch (error) {
      return { is_valid: false, reason: error.message, row_number: idx + 1 };
    }
  }
}