diff --git a/packages/api/src/api/rest/perimetre.test.integration.ts b/packages/api/src/api/rest/perimetre.test.integration.ts index 370f44af9f018105257ea7823bbebc7904945bf9..e2addeac64bf32b29e0d64ab90a54e5c34aca9a4 100644 --- a/packages/api/src/api/rest/perimetre.test.integration.ts +++ b/packages/api/src/api/rest/perimetre.test.integration.ts @@ -165,7 +165,7 @@ C;Point éç;-52.55;4.24113309117193` mkdirSync(dir, { recursive: true }) writeFileSync( `${dir}/${fileName}`, - `nom;description;longitude;latitude + ` nom ;description;longitude;latitude A;;-52,54;4,22269896902571 B;;-52,55;4,22438936251509 C;Point éç;-52,55;4,24113309117193` @@ -178,9 +178,54 @@ C;Point éç;-52,55;4,24113309117193` } const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) - expect(tested.statusCode).toBe(HTTP_STATUS.OK) + expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK) expect(tested.body).toMatchSnapshot() }) + test('csv valide avec des virgules', async () => { + const fileName = `existing_temp_file_${idGenerate()}.csv` + mkdirSync(dir, { recursive: true }) + writeFileSync( + `${dir}/${fileName}`, + `nom,description,longitude,latitude + A,,-52.54,4.22269896902571 + B,,-52.55,4.22438936251509 + C,Point éç,-52.55,4.24113309117193` + ) + const body: GeojsonImportBody = { + titreSlug: titreSlugValidator.parse('titre-slug'), + titreTypeId: 'arm', + tempDocumentName: tempDocumentNameValidator.parse(fileName), + fileType: 'csv', + } + + const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) + expect(tested.body).toMatchInlineSnapshot(` + { + "detail": "Seuls les séparateurs ';' sont acceptés", + "message": "Le séparateur est probablement incorrect", + "status": 400, + } + `) + }) + test('csv valide mais sans contenu', async () => { + const fileName = `existing_temp_file_${idGenerate()}.csv` + mkdirSync(dir, { recursive: true }) + writeFileSync(`${dir}/${fileName}`, `nom,description,longitude,latitude`) + const body: GeojsonImportBody = { + titreSlug: titreSlugValidator.parse('titre-slug'), + titreTypeId: 'arm', + tempDocumentName: tempDocumentNameValidator.parse(fileName), + fileType: 'csv', + } + + const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) + expect(tested.body).toMatchInlineSnapshot(` + { + "message": "Le CSV ne contient aucun élément", + "status": 400, + } + `) + }) test('fichier valide geojson polygon', async () => { const feature: FeatureCollectionPolygon = { type: 'FeatureCollection', @@ -1474,7 +1519,7 @@ B;Point B;1063526.397924559889361;6867885.978687250986695;12,12;captage` } const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body) - expect(tested.statusCode).toBe(HTTP_STATUS.OK) + expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK) expect(tested.body).toMatchSnapshot() const testedWithError = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) diff --git a/packages/api/src/api/rest/perimetre.ts b/packages/api/src/api/rest/perimetre.ts index 3441b32b3fcb723ad5848f3dcc17eec5ea40a2da..714f30069a288ceba717c4e0820e46a609f89443 100644 --- a/packages/api/src/api/rest/perimetre.ts +++ b/packages/api/src/api/rest/perimetre.ts @@ -41,7 +41,7 @@ import { import { join } from 'node:path' import { readFileSync } from 'node:fs' import { parseShp } from 'shpjs' -import { isNotNullNorUndefined, isNullOrUndefined, memoize } from 'camino-common/src/typescript-tools' +import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, memoize } from 'camino-common/src/typescript-tools' import { SDOMZoneId } from 'camino-common/src/static/sdom' import { TitreSlug } from 'camino-common/src/validators/titres' import { canReadEtape } from './permissions/etapes' @@ -157,7 +157,7 @@ const csvCommonValidator = z if ('Nom du point' in value) { value.nom = value['Nom du point'] } - Object.keys(value).forEach(k => (value[k.toLowerCase()] = value[k])) + Object.keys(value).forEach(k => (value[k.toLowerCase().trim()] = value[k])) return value }) @@ -186,7 +186,6 @@ type GeojsonImportErrorMessages = | EffectDbQueryAndValidateErrors | typeof ouvertureGeoJSONError | typeof ouvertureShapeError - | typeof ouvertureCsvError | typeof importCsvRestrictionError | typeof recuperationInfoCsvError | typeof extractionGeoJSONError @@ -194,6 +193,8 @@ type GeojsonImportErrorMessages = | GetGeojsonByGeoSystemeIdErrorMessages | GetGeojsonInformationErrorMessages | ConvertPointsErrors + | FileNameToCsvErrors + export const geojsonImport: RestNewPostCall<'/rest/geojson/import/:geoSystemeId'> = (rootPipe): Effect.Effect<GeojsonInformations, CaminoApiError<GeojsonImportErrorMessages>> => { return rootPipe.pipe( Effect.let('pathFrom', ({ body }) => join(process.cwd(), `/files/tmp/${body.tempDocumentName}`)), @@ -373,6 +374,8 @@ export const geojsonImport: RestNewPostCall<'/rest/geojson/import/:geoSystemeId' 'La liste des points est vide', 'Le nombre de points est invalide', 'Problème de Système géographique (SRID)', + 'Le CSV ne contient aucun élément', + 'Le séparateur est probablement incorrect', () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST }) ), Match.exhaustive @@ -417,7 +420,10 @@ const readIsoOrUTF8FileSync = (path: string): string => { } } -const fileNameToCsv = (pathFrom: string): Effect.Effect<unknown[], CaminoError<typeof ouvertureCsvError>> => { +const csvVideError = 'Le CSV ne contient aucun élément' as const +const mauvaisSeparateurCsvError = 'Le séparateur est probablement incorrect' as const +type FileNameToCsvErrors = typeof ouvertureCsvError | typeof mauvaisSeparateurCsvError | typeof csvVideError +const fileNameToCsv = (pathFrom: string): Effect.Effect<unknown[], CaminoError<FileNameToCsvErrors>> => { return Effect.try({ try: () => { const fileContent = readIsoOrUTF8FileSync(pathFrom) @@ -428,11 +434,22 @@ const fileNameToCsv = (pathFrom: string): Effect.Effect<unknown[], CaminoError<t } const sheet1 = result.Sheets[result.SheetNames[0]] - return xlsx.utils.sheet_to_json(sheet1, { raw: true }) }, catch: e => ({ message: ouvertureCsvError, extra: e }), - }) + }).pipe( + Effect.filterOrFail( + result => isNotNullNorUndefinedNorEmpty(result), + () => ({ message: csvVideError }) + ), + Effect.filterOrFail( + result => { + const firstValue = result[0] + return typeof firstValue === 'object' && isNotNullNorUndefined(firstValue) && Object.keys(firstValue).length >= 2 + }, + () => ({ message: mauvaisSeparateurCsvError, detail: "Seuls les séparateurs ';' sont acceptés" }) + ) + ) } const accesInterditError = 'Accès interdit' as const @@ -485,7 +502,7 @@ export const geojsonImportPoints: RestNewPostCall<'/rest/geojson_points/import/: ) } -type GeosjsonImportForagesErrorMessages = ZodUnparseable | EffectDbQueryAndValidateErrors | typeof ouvertureCsvError | typeof ouvertureShapeError | typeof ouvertureGeoJSONError | ConvertPointsErrors +type GeosjsonImportForagesErrorMessages = ZodUnparseable | EffectDbQueryAndValidateErrors | typeof ouvertureShapeError | typeof ouvertureGeoJSONError | ConvertPointsErrors | FileNameToCsvErrors export const geojsonImportForages: RestNewPostCall<'/rest/geojson_forages/import/:geoSystemeId'> = ( rootPipe ): Effect.Effect<GeojsonImportForagesResponse, CaminoApiError<GeosjsonImportForagesErrorMessages>> => { @@ -560,6 +577,8 @@ export const geojsonImportForages: RestNewPostCall<'/rest/geojson_forages/import 'La liste des points est vide', 'Le nombre de points est invalide', 'Problème de Système géographique (SRID)', + 'Le CSV ne contient aucun élément', + 'Le séparateur est probablement incorrect', () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST }) ), Match.exhaustive