diff --git a/app/api/csv/csv.ts b/app/api/csv/csv.ts index 64effd2dd7..a6e154ef41 100644 --- a/app/api/csv/csv.ts +++ b/app/api/csv/csv.ts @@ -1,5 +1,3 @@ -import readline from 'readline'; - import csvtojson from 'csvtojson'; import { Readable } from 'stream'; @@ -8,18 +6,17 @@ import importFile, { ImportFile } from './importFile'; type CSVRow = { [k: string]: string }; const DELIMITERS = [',', ';']; -const DELIMITER_REGEX = new RegExp(`[${DELIMITERS.join('')}]`); const peekHeaders = async (readSource: Readable | string): Promise => { const readStream = typeof readSource === 'string' ? await importFile(readSource).readStream() : readSource; let headers: string[] = []; - const rl = readline.createInterface({ input: readStream }); - const line = (await rl[Symbol.asyncIterator]().next()).value; - headers = line.split(DELIMITER_REGEX); - rl.close(); - readStream.unpipe(); - readStream.destroy(); + const stream = csvtojson().fromStream(readStream); + await stream.on('header', async h => { + headers = h; + await stream.end(); + }); + return headers; }; diff --git a/app/api/csv/importFile.ts b/app/api/csv/importFile.ts index 5eeb407e01..43e09134f5 100644 --- a/app/api/csv/importFile.ts +++ b/app/api/csv/importFile.ts @@ -5,6 +5,8 @@ import { createError } from 'api/utils'; import zipFile from 'api/utils/zipFile'; // eslint-disable-next-line node/no-restricted-import import { createReadStream } from 'fs'; +// eslint-disable-next-line node/no-restricted-import +import { readFile } from 'fs/promises'; const extractFromZip = async (zipPath: string, fileName: string) => { const readStream = await zipFile(zipPath).findReadStream(entry => entry === fileName); @@ -16,14 +18,19 @@ const extractFromZip = async (zipPath: string, fileName: string) => { return readStream; }; -export class ImportFile { +class ImportFile { filePath: string; constructor(filePath: string) { this.filePath = filePath; } + private async checkFileExists() { + await readFile(this.filePath); + } + async readStream(fileName = 'import.csv') { + await this.checkFileExists(); if (path.extname(this.filePath) === '.zip') { return extractFromZip(this.filePath, fileName); } @@ -46,4 +53,5 @@ export class ImportFile { const importFile = (filePath: string) => new ImportFile(filePath); +export { ImportFile }; export default importFile; diff --git a/app/api/csv/specs/csv.spec.ts b/app/api/csv/specs/csv.spec.ts index c36803adda..7a77f4582e 100644 --- a/app/api/csv/specs/csv.spec.ts +++ b/app/api/csv/specs/csv.spec.ts @@ -16,7 +16,7 @@ describe('peekHeaders()', () => { title1, text1, text1_es, text1_en`; const { mockedFile, mockedFileStream } = mockFileStream(content); const headers = await peekHeaders(mockedFileStream); - expect(headers).toEqual(['title', ' textprop', ' textprop__es', ' textprop__en']); + expect(headers).toEqual(['title', 'textprop', 'textprop__es', 'textprop__en']); mockedFile.mockRestore(); }); @@ -27,10 +27,10 @@ describe('peekHeaders()', () => { 'title', 'unrelated_property', 'select_property__en', - ' Select Property__es', + 'Select Property__es', 'Multiselect Property__en', - ' multiselect_property__es', - ' no_new_value_select', + 'multiselect_property__es', + 'no_new_value_select', ]); }); }); @@ -60,7 +60,7 @@ describe('validateFormat()', () => { message: 'Expected 3 columns, but found 2.', }, { - content: 'title,textprop\ntitle1', + content: 'title,textprop\ntitle1,', columns: 1, message: 'Expected 1 columns, but found 2.', }, diff --git a/app/api/csv/specs/helpers.js b/app/api/csv/specs/helpers.js index 9eee40d245..1fff06c1c4 100644 --- a/app/api/csv/specs/helpers.js +++ b/app/api/csv/specs/helpers.js @@ -2,6 +2,8 @@ import path from 'path'; import yazl from 'yazl'; import { Readable } from 'stream'; // eslint-disable-next-line node/no-restricted-import +import fsPromises from 'fs/promises'; +// eslint-disable-next-line node/no-restricted-import import fs from 'fs'; const createTestingZip = (filesToZip, fileName, directory = __dirname) => @@ -37,7 +39,9 @@ class ReadableString extends Readable { const stream = string => new ReadableString(string); -const mockCsvFileReadStream = str => - jest.spyOn(fs, 'createReadStream').mockImplementation(() => stream(str)); +const mockCsvFileReadStream = str => { + jest.spyOn(fsPromises, 'readFile').mockImplementation(() => {}); + return jest.spyOn(fs, 'createReadStream').mockImplementation(() => stream(str)); +}; export { stream, createTestingZip, mockCsvFileReadStream }; diff --git a/app/api/csv/typeParsers/multiselect.ts b/app/api/csv/typeParsers/multiselect.ts index 88d16b77a1..0e9d6ad709 100644 --- a/app/api/csv/typeParsers/multiselect.ts +++ b/app/api/csv/typeParsers/multiselect.ts @@ -17,6 +17,9 @@ function labelNotNull(label: string | null): label is string { function splitMultiselectLabels(labelString: string): { labelInfos: LabelInfo[]; } { + if (!labelString) { + return { labelInfos: [] }; + } const labels = labelString .split(csvConstants.multiValueSeparator) .map(l => l.trim()) diff --git a/app/api/i18n/specs/translations.spec.ts b/app/api/i18n/specs/translations.spec.ts index a51a79d1a3..473c3a8beb 100644 --- a/app/api/i18n/specs/translations.spec.ts +++ b/app/api/i18n/specs/translations.spec.ts @@ -425,11 +425,7 @@ describe('translations', () => { }); it('should throw error when translation is not available', async () => { - const readFileMock = jest - .spyOn(fs.promises, 'readFile') - .mockRejectedValue({ code: 'ENOENT' }); - - await expect(translations.importPredefined('zh')).rejects.toThrowError( + await expect(translations.importPredefined('non-existent')).rejects.toBeInstanceOf( UITranslationNotAvailable ); @@ -441,8 +437,6 @@ describe('translations', () => { expect(ZHTranslations.Password).toBe('Password'); expect(ZHTranslations.Account).toBe('Account'); expect(ZHTranslations.Age).toBe('Age'); - - readFileMock.mockRestore(); }); }); });