From 4abc631ce871603fe8932144d5cff914c30b6bf7 Mon Sep 17 00:00:00 2001 From: Anis Safine Laget <anis.safine@beta.gouv.fr> Date: Mon, 24 Mar 2025 18:02:39 +0100 Subject: [PATCH 1/6] wip --- packages/api/src/api/rest/etapes.queries.ts | 44 ++++-- packages/api/src/api/rest/etapes.ts | 149 +++++++++++------- packages/api/src/api/rest/fichiers.ts | 2 +- .../database/queries/titres-etapes.queries.ts | 25 ++- packages/api/src/server/rest.ts | 4 +- packages/api/src/tools/fp-tools.ts | 7 + packages/common/src/rest.ts | 4 +- 7 files changed, 152 insertions(+), 83 deletions(-) diff --git a/packages/api/src/api/rest/etapes.queries.ts b/packages/api/src/api/rest/etapes.queries.ts index 4fd77d265..baa9514d4 100644 --- a/packages/api/src/api/rest/etapes.queries.ts +++ b/packages/api/src/api/rest/etapes.queries.ts @@ -24,13 +24,14 @@ import { EntrepriseId, entrepriseIdValidator } from 'camino-common/src/entrepris import { User } from 'camino-common/src/roles' import { LargeObjectId, largeObjectIdValidator } from '../../database/largeobjects' import { canReadDocument } from './permissions/documents' -import { memoize, Memoized } from 'camino-common/src/typescript-tools' +import { isNotNullNorUndefinedNorEmpty, memoize, Memoized } from 'camino-common/src/typescript-tools' import { etapeStatutIdValidator } from 'camino-common/src/static/etapesStatuts' import { CaminoError } from 'camino-common/src/zod-tools' import { Effect, pipe } from 'effect' import { DATE_DEBUT_PROCEDURE_SPECIFIQUE } from 'camino-common/src/machines' import { TitreId } from 'camino-common/src/validators/titres' import { communeIdValidator } from 'camino-common/src/static/communes' +import { callAndExit } from '../../tools/fp-tools' const getEtapeByIdValidator = z.object({ etape_id: etapeIdValidator, @@ -84,7 +85,7 @@ export const getLargeobjectIdByEtapeDocumentId = async (pool: Pool, user: User, if (result.length === 1) { const etapeDocument = result[0] - const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await getEtapeDataForEdition(pool, etapeDocument.etape_id) + const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await callAndExit(getEtapeDataForEdition(pool, etapeDocument.etape_id)) if ( await canReadDocument(etapeDocument, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, etapeData.etape_type_id, { @@ -113,22 +114,33 @@ where LIMIT 1 ` -export const getEtapeDataForEdition = async ( +const etapeNotFound = "l'étape n'a pas été trouvée" as const +export type GetEtapeDataForEditionErrors = EffectDbQueryAndValidateErrors | typeof etapeNotFound +export const getEtapeDataForEdition = ( pool: Pool, etapeId: EtapeId -): Promise<{ - etapeData: GetEtapeDataForEdition - titreTypeId: Memoized<TitreTypeId> - administrationsLocales: Memoized<AdministrationId[]> - entreprisesTitulairesOuAmodiataires: Memoized<EntrepriseId[]> -}> => { - const etapeData = (await dbQueryAndValidate(getEtapeDataForEditionDb, { etapeId }, pool, getEtapeDataForEditionValidator))[0] - - const titreTypeId = memoize(() => Promise.resolve(etapeData.titre_type_id)) - const administrationsLocales = memoize(() => administrationsLocalesByEtapeId(etapeId, pool)) - const entreprisesTitulairesOuAmodiataires = memoize(() => entreprisesTitulairesOuAmoditairesByEtapeId(etapeId, pool)) - - return { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } +): Effect.Effect< + { + etapeData: GetEtapeDataForEdition + titreTypeId: Memoized<TitreTypeId> + administrationsLocales: Memoized<AdministrationId[]> + entreprisesTitulairesOuAmodiataires: Memoized<EntrepriseId[]> + }, + CaminoError<GetEtapeDataForEditionErrors> +> => { + return Effect.Do.pipe( + Effect.flatMap(() => effectDbQueryAndValidate(getEtapeDataForEditionDb, { etapeId }, pool, getEtapeDataForEditionValidator)), + Effect.filterOrFail( + result => isNotNullNorUndefinedNorEmpty(result) && result.length === 1, + () => ({ message: etapeNotFound }) + ), + Effect.map(result => ({ + etapeData: result[0], + titreTypeId: memoize(() => Promise.resolve(result[0].titre_type_id)), + administrationsLocales: memoize(() => administrationsLocalesByEtapeId(etapeId, pool)), + entreprisesTitulairesOuAmodiataires: memoize(() => entreprisesTitulairesOuAmoditairesByEtapeId(etapeId, pool)), + })) + ) } const getEtapeDataForEditionValidator = z.object({ diff --git a/packages/api/src/api/rest/etapes.ts b/packages/api/src/api/rest/etapes.ts index 408f32dcf..350e62de5 100644 --- a/packages/api/src/api/rest/etapes.ts +++ b/packages/api/src/api/rest/etapes.ts @@ -7,7 +7,6 @@ import { ETAPE_IS_NOT_BROUILLON, etapeIdOrSlugValidator, GetEtapeAvisByEtapeId, - getEtapeAvisByEtapeIdValidator, EtapeBrouillon, etapeSlugValidator, EtapeSlug, @@ -30,6 +29,7 @@ import { EntrepriseDocument, EntrepriseDocumentId, EtapeEntrepriseDocument } fro import { deleteTitreEtapeEntrepriseDocument, getDocumentsByEtapeId, + GetDocumentsByEtapeIdErrors, getEntrepriseDocumentIdsByEtapeId, getEtapeAvisLargeObjectIdsByEtapeId, GetEtapeAvisLargeObjectIdsByEtapeIdErrors, @@ -44,7 +44,7 @@ import { updateEtapeDocuments, UpdateEtapeDocumentsErrors, } from '../../database/queries/titres-etapes.queries' -import { getEtapeDataForEdition, hasTitreFrom } from './etapes.queries' +import { getEtapeDataForEdition, GetEtapeDataForEditionErrors, hasTitreFrom } from './etapes.queries' import { SDOMZoneId } from 'camino-common/src/static/sdom' import { titreEtapeAdministrationsEmailsSend, titreEtapeUtilisateursEmailsSend } from '../graphql/resolvers/_titre-etape-email' import { ConvertPointsErrors, GetGeojsonInformation, GetGeojsonInformationErrorMessages, convertPoints, getGeojsonInformation } from './perimetre.queries' @@ -89,64 +89,97 @@ export const getEtapeEntrepriseDocuments: RestNewGetCall<'/rest/etapes/:etapeId/ ) ) -export const getEtapeDocuments = - (pool: Pool) => - async (req: CaminoRequest, res: CustomResponse<GetEtapeDocumentsByEtapeId>): Promise<void> => { - const etapeIdParsed = etapeIdValidator.safeParse(req.params.etapeId) - const user = req.auth - - if (!etapeIdParsed.success) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else { - try { - const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await getEtapeDataForEdition(pool, etapeIdParsed.data) - - const result = await callAndExit( - getDocumentsByEtapeId(etapeIdParsed.data, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, etapeData.etape_type_id, { - demarche_type_id: etapeData.demarche_type_id, - entreprises_lecture: etapeData.demarche_entreprises_lecture, - public_lecture: etapeData.demarche_public_lecture, - titre_public_lecture: etapeData.titre_public_lecture, +// FIXME : traiter la partie front + tests +type GetEtapeDocumentsErrors = EffectDbQueryAndValidateErrors | GetEtapeDataForEditionErrors | GetDocumentsByEtapeIdErrors +export const getEtapeDocuments: RestNewGetCall<'/rest/etapes/:etapeId/etapeDocuments'> = (rootPipe): Effect.Effect<GetEtapeDocumentsByEtapeId, CaminoApiError<GetEtapeDocumentsErrors>> => + rootPipe.pipe( + Effect.bind('etapeDataForEdition', ({ pool, params }) => getEtapeDataForEdition(pool, params.etapeId)), + Effect.flatMap(({ pool, user, params, etapeDataForEdition }) => + getDocumentsByEtapeId( + params.etapeId, + pool, + user, + etapeDataForEdition.titreTypeId, + etapeDataForEdition.administrationsLocales, + etapeDataForEdition.entreprisesTitulairesOuAmodiataires, + etapeDataForEdition.etapeData.etape_type_id, + { + demarche_type_id: etapeDataForEdition.etapeData.demarche_type_id, + entreprises_lecture: etapeDataForEdition.etapeData.demarche_entreprises_lecture, + public_lecture: etapeDataForEdition.etapeData.demarche_public_lecture, + titre_public_lecture: etapeDataForEdition.etapeData.titre_public_lecture, + } + ) + ), + Effect.map(result => ({ + etapeDocuments: result, + })), + Effect.mapError(caminoError => + Match.value(caminoError.message).pipe( + Match.whenOr("l'étape n'a pas été trouvée", () => ({ + ...caminoError, + status: HTTP_STATUS.NOT_FOUND, + })), + Match.whenOr( + "Impossible d'exécuter la requête dans la base de données", + 'Les données en base ne correspondent pas à ce qui est attendu', + "une erreur s'est produite lors de la vérification des droits de lecture d'un document", + "Impossible de transformer le document en base en document d'API", + () => ({ + ...caminoError, + status: HTTP_STATUS.INTERNAL_SERVER_ERROR, }) - ) - - res.json({ etapeDocuments: result }) - } catch (e) { - res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR) - console.error(e) - } - } - } - -export const getEtapeAvis = - (pool: Pool) => - async (req: CaminoRequest, res: CustomResponse<GetEtapeAvisByEtapeId>): Promise<void> => { - const etapeIdParsed = etapeIdValidator.safeParse(req.params.etapeId) - const user = req.auth - - if (!etapeIdParsed.success) { - res.sendStatus(HTTP_STATUS.BAD_REQUEST) - } else { - try { - const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await getEtapeDataForEdition(pool, etapeIdParsed.data) + ), + Match.exhaustive + ) + ) + ) - const result = await callAndExit( - getEtapeAvisLargeObjectIdsByEtapeId(etapeIdParsed.data, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, etapeData.etape_type_id, { - demarche_type_id: etapeData.demarche_type_id, - entreprises_lecture: etapeData.demarche_entreprises_lecture, - public_lecture: etapeData.demarche_public_lecture, - titre_public_lecture: etapeData.titre_public_lecture, +// FIXME : traiter la partie front + tests +type GetEtapeAvisErrors = EffectDbQueryAndValidateErrors | GetEtapeDataForEditionErrors | GetEtapeAvisLargeObjectIdsByEtapeIdErrors +export const getEtapeAvis: RestNewGetCall<'/rest/etapes/:etapeId/etapeAvis'> = (rootPipe): Effect.Effect<GetEtapeAvisByEtapeId, CaminoApiError<GetEtapeAvisErrors>> => + rootPipe.pipe( + Effect.bind('etapeDataForEdition', ({ pool, params }) => getEtapeDataForEdition(pool, params.etapeId)), + Effect.flatMap(({ pool, params, user, etapeDataForEdition }) => + getEtapeAvisLargeObjectIdsByEtapeId( + params.etapeId, + pool, + user, + etapeDataForEdition.titreTypeId, + etapeDataForEdition.administrationsLocales, + etapeDataForEdition.entreprisesTitulairesOuAmodiataires, + etapeDataForEdition.etapeData.etape_type_id, + { + demarche_type_id: etapeDataForEdition.etapeData.demarche_type_id, + entreprises_lecture: etapeDataForEdition.etapeData.demarche_entreprises_lecture, + public_lecture: etapeDataForEdition.etapeData.demarche_public_lecture, + titre_public_lecture: etapeDataForEdition.etapeData.titre_public_lecture, + } + ) + ), + Effect.map(result => { + const avis: GetEtapeAvisByEtapeId = result.map(a => ({ ...a, has_file: isNotNullNorUndefined(a.largeobject_id) })) + return avis + }), + Effect.mapError(caminoError => + Match.value(caminoError.message).pipe( + Match.whenOr("l'étape n'a pas été trouvée", () => ({ + ...caminoError, + status: HTTP_STATUS.NOT_FOUND, + })), + Match.whenOr( + "Impossible d'exécuter la requête dans la base de données", + 'Les données en base ne correspondent pas à ce qui est attendu', + "une erreur s'est produite lors de la vérification des droits de lecture d'un avis", + () => ({ + ...caminoError, + status: HTTP_STATUS.INTERNAL_SERVER_ERROR, }) - ) - - const avis: GetEtapeAvisByEtapeId = result.map(a => ({ ...a, has_file: isNotNullNorUndefined(a.largeobject_id) })) - res.json(getEtapeAvisByEtapeIdValidator.parse(avis)) - } catch (e) { - res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR) - console.error(e) - } - } - } + ), + Match.exhaustive + ) + ) + ) export const deleteEtape = (pool: Pool) => @@ -899,6 +932,7 @@ type DeposeEtapeErrors = | TitreEtapeToFlattenEtapeErrors | GetEtapeDocumentLargeObjectIdsByEtapeIdErrors | GetEtapeAvisLargeObjectIdsByEtapeIdErrors + | GetDocumentsByEtapeIdErrors export const deposeEtape: RestNewPutCall<'/rest/etapes/:etapeId/depot'> = (rootPipe): Effect.Effect<{ id: EtapeId }, CaminoApiError<DeposeEtapeErrors>> => { return rootPipe.pipe( Effect.bind('titreEtape', ({ params, user }) => @@ -1092,6 +1126,7 @@ export const deposeEtape: RestNewPutCall<'/rest/etapes/:etapeId/depot'> = (rootP 'une erreur est survenue lors des tâches annexes', "une erreur s'est produite lors de la vérification des droits de lecture d'un avis", "une erreur s'est produite lors de la vérification des droits de lecture d'un document", + "Impossible de transformer le document en base en document d'API", () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }) ), Match.exhaustive diff --git a/packages/api/src/api/rest/fichiers.ts b/packages/api/src/api/rest/fichiers.ts index bb00c9859..fe74b8f2a 100644 --- a/packages/api/src/api/rest/fichiers.ts +++ b/packages/api/src/api/rest/fichiers.ts @@ -32,7 +32,7 @@ export const etapeTelecharger = throw new Error("id d'étape absent") } - const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await getEtapeDataForEdition(pool, etapeId) + const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await callAndExit(getEtapeDataForEdition(pool, etapeId)) const documents = await callAndExit( getEtapeDocumentLargeObjectIdsByEtapeId(etapeId, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, etapeData.etape_type_id, { diff --git a/packages/api/src/database/queries/titres-etapes.queries.ts b/packages/api/src/database/queries/titres-etapes.queries.ts index e0fd86bc5..5e70fcce3 100644 --- a/packages/api/src/database/queries/titres-etapes.queries.ts +++ b/packages/api/src/database/queries/titres-etapes.queries.ts @@ -64,9 +64,9 @@ import { CommuneId } from 'camino-common/src/static/communes' import { EtapeStatutId, etapeStatutIdValidator } from 'camino-common/src/static/etapesStatuts' import { contenuValidator, FlattenedContenu, heritageContenuValidator } from 'camino-common/src/etape-form' import { DemarcheTypeId, demarcheTypeIdValidator } from 'camino-common/src/static/demarchesTypes' -import { Effect, Option, pipe } from 'effect' +import { Effect, Match, Option, pipe } from 'effect' import { CaminoError } from 'camino-common/src/zod-tools' -import { shortCircuitError, zodParseEffect, ZodUnparseable } from '../../tools/fp-tools' +import { callAndExit, shortCircuitError, zodParseEffect, zodParseEffectTyped, ZodUnparseable } from '../../tools/fp-tools' import { TempDocumentName } from 'camino-common/src/document' import { DemarcheId } from 'camino-common/src/demarche' @@ -485,7 +485,7 @@ export const getLargeobjectIdByEtapeAvisId = async (pool: Pool, user: User, etap if (result.length === 1) { const etapeAvis = result[0] - const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await getEtapeDataForEdition(pool, etapeAvis.etape_id) + const { etapeData, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires } = await callAndExit(getEtapeDataForEdition(pool, etapeAvis.etape_id)) if ( await canReadAvis(etapeAvis, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, etapeData.etape_type_id, { @@ -512,6 +512,9 @@ where d.id = $ etapeAvisId ! LIMIT 1 ` + +const errorParseGetDocumentsByEtapeId = "Impossible de transformer le document en base en document d'API" as const +export type GetDocumentsByEtapeIdErrors = GetEtapeDocumentLargeObjectIdsByEtapeIdErrors | typeof errorParseGetDocumentsByEtapeId export const getDocumentsByEtapeId = ( titre_etape_id: EtapeId, pool: Pool, @@ -521,9 +524,21 @@ export const getDocumentsByEtapeId = ( entreprisesTitulairesOuAmodiataires: SimplePromiseFn<EntrepriseId[]>, etapeTypeId: EtapeTypeId, demarche: CanReadDemarche -): Effect.Effect<EtapeDocument[], CaminoError<GetEtapeDocumentLargeObjectIdsByEtapeIdErrors | ZodUnparseable>> => +): Effect.Effect<EtapeDocument[], CaminoError<GetDocumentsByEtapeIdErrors>> => getEtapeDocumentLargeObjectIdsByEtapeId(titre_etape_id, pool, user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires, etapeTypeId, demarche).pipe( - Effect.flatMap(result => zodParseEffect(z.array(etapeDocumentValidator), result)) + Effect.flatMap(result => + zodParseEffectTyped(z.array(etapeDocumentValidator), result).pipe( + Effect.mapError(caminoError => + Match.value(caminoError.message).pipe( + Match.when('Problème de validation de données', () => ({ + ...caminoError, + message: errorParseGetDocumentsByEtapeId, + })), + Match.exhaustive + ) + ) + ) + ) ) const getEtapesWithAutomaticStatutValidator = z.object({ diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts index f0a6b4ee9..891111944 100644 --- a/packages/api/src/server/rest.ts +++ b/packages/api/src/server/rest.ts @@ -236,8 +236,8 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k '/rest/etapes': { newPostCall: createEtape, newPutCall: updateEtape, ...CaminoRestRoutes['/rest/etapes'] }, '/rest/etapes/:etapeId/depot': { newPutCall: deposeEtape, ...CaminoRestRoutes['/rest/etapes/:etapeId/depot'] }, '/rest/etapes/:etapeId/entrepriseDocuments': { newGetCall: getEtapeEntrepriseDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/entrepriseDocuments'] }, - '/rest/etapes/:etapeId/etapeDocuments': { getCall: getEtapeDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeDocuments'] }, - '/rest/etapes/:etapeId/etapeAvis': { getCall: getEtapeAvis, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeAvis'] }, + '/rest/etapes/:etapeId/etapeDocuments': { newGetCall: getEtapeDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeDocuments'] }, + '/rest/etapes/:etapeId/etapeAvis': { newGetCall: getEtapeAvis, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeAvis'] }, '/rest/activites/:activiteId': { getCall: getActivite, putCall: updateActivite, deleteCall: deleteActivite, ...CaminoRestRoutes['/rest/activites/:activiteId'] }, '/rest/communes': { newGetCall: getCommunes, ...CaminoRestRoutes['/rest/communes'] }, '/rest/geojson/import/:geoSystemeId': { newPostCall: geojsonImport, ...CaminoRestRoutes['/rest/geojson/import/:geoSystemeId'] }, diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts index 0a34a3edd..4fedc8aea 100644 --- a/packages/api/src/tools/fp-tools.ts +++ b/packages/api/src/tools/fp-tools.ts @@ -27,6 +27,13 @@ export const zodParseEffect = <T extends ZodTypeAny>(validator: T, item: unknown }) } +export const zodParseEffectTyped = <T extends ZodTypeAny>(validator: T, item: T['_output']): Effect.Effect<T['_output'], CaminoError<ZodUnparseable>> => { + return Effect.try({ + try: () => validator.parse(item), + catch: myError => ({ message: 'Problème de validation de données', detail: zodErrorToDetail(myError), zodErrorReadableMessage: zodErrorToReadableMessage(myError) }), + }) +} + export const callAndExit = async <A>(toCall: Effect.Effect<A, CaminoError<string>, never>): Promise<A> => { const pipeline = await pipe(toCall, Effect.runPromiseExit) diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts index 3d78d7505..c5e809b96 100644 --- a/packages/common/src/rest.ts +++ b/packages/common/src/rest.ts @@ -218,8 +218,8 @@ export const CaminoRestRoutes = { '/rest/demarches/:demarcheId/miseEnConcurrence': { params: z.object({ demarcheId: demarcheIdValidator }), newGet: { output: z.array(getDemarcheMiseEnConcurrenceValidator) } }, '/rest/demarches/:demarcheId/resultatMiseEnConcurrence': { params: z.object({ demarcheId: demarcheIdValidator }), newGet: { output: getResultatMiseEnConcurrenceValidator } }, '/rest/etapes/:etapeId/geojson': { params: z.object({ etapeId: etapeIdOrSlugValidator }), newGet: { output: perimetreInformationsValidator } }, - '/rest/etapes/:etapeId/etapeDocuments': { params: etapeIdParamsValidator, get: { output: getEtapeDocumentsByEtapeIdValidator } }, - '/rest/etapes/:etapeId/etapeAvis': { params: etapeIdParamsValidator, get: { output: getEtapeAvisByEtapeIdValidator } }, + '/rest/etapes/:etapeId/etapeDocuments': { params: etapeIdParamsValidator, newGet: { output: getEtapeDocumentsByEtapeIdValidator } }, + '/rest/etapes/:etapeId/etapeAvis': { params: etapeIdParamsValidator, newGet: { output: getEtapeAvisByEtapeIdValidator } }, '/rest/etapes/:etapeId/entrepriseDocuments': { params: etapeIdParamsValidator, newGet: { output: z.array(etapeEntrepriseDocumentValidator) } }, '/rest/etapes/:etapeIdOrSlug': { params: z.object({ etapeIdOrSlug: etapeIdOrSlugValidator }), delete: true, get: { output: flattenEtapeValidator } }, '/rest/etapes/:etapeId/depot': { params: etapeIdParamsValidator, newPut: { input: z.object({}), output: z.object({ id: etapeIdValidator }) } }, -- GitLab From e9c71f2da368a017f6ea2e5eec5c2581dea52937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com> Date: Tue, 25 Mar 2025 09:22:40 +0100 Subject: [PATCH 2/6] plop front ok --- .../rest/etape-modifier.test.integration.ts | 6 ++--- .../src/api/rest/etapes.test.integration.ts | 6 ++--- .../database/queries/titres-etapes.queries.ts | 2 +- packages/ui/src/api/client-rest.ts | 10 +++++++ .../src/components/etape/etape-api-client.ts | 8 +++--- .../src/components/etape/etape-avis-edit.tsx | 26 +++++++------------ .../components/etape/etape-documents-edit.tsx | 26 +++++++------------ 7 files changed, 39 insertions(+), 45 deletions(-) diff --git a/packages/api/src/api/rest/etape-modifier.test.integration.ts b/packages/api/src/api/rest/etape-modifier.test.integration.ts index 7e61235b6..e04e67fdc 100644 --- a/packages/api/src/api/rest/etape-modifier.test.integration.ts +++ b/packages/api/src/api/rest/etape-modifier.test.integration.ts @@ -1,5 +1,5 @@ import { dbManager } from '../../../tests/db-manager' -import { restNewPutCall, restCall, restNewPostCall } from '../../../tests/_utils/index' +import { restNewPutCall, restCall, restNewPostCall, restNewCall } from '../../../tests/_utils/index' import Titres from '../../database/models/titres' import { userSuper } from '../../database/user-super' import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations' @@ -343,7 +343,7 @@ describe('etapeModifier', () => { expect(res.statusCode, JSON.stringify(res.body)).toBe(HTTP_STATUS.OK) - let documents = await restCall(dbPool, '/rest/etapes/:etapeId/etapeDocuments', { etapeId: titreEtapeId }, userSuper) + let documents = await restNewCall(dbPool, '/rest/etapes/:etapeId/etapeDocuments', { etapeId: titreEtapeId }, userSuper) expect(documents.statusCode).toBe(HTTP_STATUS.OK) expect(documents.body.etapeDocuments).toHaveLength(1) expect(documents.body.etapeDocuments[0]).toMatchInlineSnapshot( @@ -362,7 +362,7 @@ describe('etapeModifier', () => { expect(res.statusCode).toBe(HTTP_STATUS.OK) - documents = await restCall(dbPool, '/rest/etapes/:etapeId/etapeDocuments', { etapeId: titreEtapeId }, userSuper) + documents = await restNewCall(dbPool, '/rest/etapes/:etapeId/etapeDocuments', { etapeId: titreEtapeId }, userSuper) expect(documents.statusCode).toBe(HTTP_STATUS.OK) expect(documents.body.etapeDocuments).toHaveLength(0) }) diff --git a/packages/api/src/api/rest/etapes.test.integration.ts b/packages/api/src/api/rest/etapes.test.integration.ts index 3f59754ef..26487e6ee 100644 --- a/packages/api/src/api/rest/etapes.test.integration.ts +++ b/packages/api/src/api/rest/etapes.test.integration.ts @@ -417,12 +417,12 @@ describe('getEtapeAvis', () => { ], }) - let getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper) + let getAvis = await restNewCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper) expect(getAvis.statusCode).toBe(HTTP_STATUS.OK) expect(getAvis.body).toStrictEqual([]) await titreEtapeUpdate(etapeId, { typeId: 'asc' }, userSuper, titreId) - getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper) + getAvis = await restNewCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper) expect(getAvis.statusCode).toBe(HTTP_STATUS.OK) expect(getAvis.body).toStrictEqual([]) @@ -445,7 +445,7 @@ describe('getEtapeAvis', () => { await callAndExit(insertEtapeAvisWithLargeObjectId(dbPool, etapeId, avis, etapeAvisIdValidator.parse('avisId'), largeObjectIdValidator.parse(42))) - getAvis = await restCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper) + getAvis = await restNewCall(dbPool, '/rest/etapes/:etapeId/etapeAvis', { etapeId: etapeId }, userSuper) expect(getAvis.statusCode).toBe(HTTP_STATUS.OK) expect(getAvis.body).toMatchInlineSnapshot(` [ diff --git a/packages/api/src/database/queries/titres-etapes.queries.ts b/packages/api/src/database/queries/titres-etapes.queries.ts index 5e70fcce3..6d2fdc8dd 100644 --- a/packages/api/src/database/queries/titres-etapes.queries.ts +++ b/packages/api/src/database/queries/titres-etapes.queries.ts @@ -66,7 +66,7 @@ import { contenuValidator, FlattenedContenu, heritageContenuValidator } from 'ca import { DemarcheTypeId, demarcheTypeIdValidator } from 'camino-common/src/static/demarchesTypes' import { Effect, Match, Option, pipe } from 'effect' import { CaminoError } from 'camino-common/src/zod-tools' -import { callAndExit, shortCircuitError, zodParseEffect, zodParseEffectTyped, ZodUnparseable } from '../../tools/fp-tools' +import { callAndExit, shortCircuitError, zodParseEffectTyped } from '../../tools/fp-tools' import { TempDocumentName } from 'camino-common/src/document' import { DemarcheId } from 'camino-common/src/demarche' diff --git a/packages/ui/src/api/client-rest.ts b/packages/ui/src/api/client-rest.ts index 5d3273963..529076e74 100644 --- a/packages/ui/src/api/client-rest.ts +++ b/packages/ui/src/api/client-rest.ts @@ -33,6 +33,16 @@ export class CaminoHttpError extends Error { export type AsyncData<T> = Loading | { status: 'LOADED'; value: T } | CaminoApiError | NewCaminoApiError +export const asyncDataAutomaticLoad = async <T extends object>(value: () => Promise<T | CaminoError<string>>, setState: (newState: AsyncData<T>) => void): Promise<void> => { + setState({ status: 'LOADING' }) + const result = await value() + if ('message' in result) { + setState({ status: 'NEW_ERROR', error: result }) + } else { + setState({ status: 'LOADED', value: result }) + } +} + type UiRestRoute = string & { __camino: 'RestRoute' } const baseRoute = '/apiUrl' diff --git a/packages/ui/src/components/etape/etape-api-client.ts b/packages/ui/src/components/etape/etape-api-client.ts index 210596939..37235b203 100644 --- a/packages/ui/src/components/etape/etape-api-client.ts +++ b/packages/ui/src/components/etape/etape-api-client.ts @@ -82,9 +82,9 @@ export interface EtapeApiClient { getEtapesTypesEtapesStatuts: (titreDemarcheId: DemarcheId, titreEtapeId: EtapeId | null, date: CaminoDate) => Promise<EtapeTypeEtapeStatutWithMainStep | CaminoError<string>> deleteEtape: (titreEtapeId: EtapeId) => Promise<void> deposeEtape: (titreEtapeId: EtapeId) => Promise<{ id: EtapeId } | CaminoError<string>> - getEtapeDocumentsByEtapeId: (etapeId: EtapeId) => Promise<GetEtapeDocumentsByEtapeId> + getEtapeDocumentsByEtapeId: (etapeId: EtapeId) => Promise<GetEtapeDocumentsByEtapeId | CaminoError<string>> getEtapeHeritagePotentiel: (etape: Pick<CoreEtapeCreationOrModification, 'id' | 'date' | 'typeId'>, titreDemarcheId: DemarcheId) => Promise<GetEtapeHeritagePotentiel> - getEtapeAvisByEtapeId: (etapeId: EtapeId) => Promise<GetEtapeAvisByEtapeId> + getEtapeAvisByEtapeId: (etapeId: EtapeId) => Promise<GetEtapeAvisByEtapeId | CaminoError<string>> getEtape: (etapeIdOrSlug: EtapeIdOrSlug) => Promise<{ etape: FlattenEtape; demarche: GetDemarcheByIdOrSlug | CaminoError<string> }> etapeCreer: (etape: RestEtapeCreation) => Promise<CaminoError<string> | { id: EtapeId }> etapeModifier: (etape: RestEtapeModification) => Promise<CaminoError<string> | { id: EtapeId }> @@ -98,8 +98,8 @@ export const etapeApiClient: EtapeApiClient = { }, deposeEtape: async etapeId => newPutWithJson('/rest/etapes/:etapeId/depot', { etapeId }, {}), - getEtapeDocumentsByEtapeId: async etapeId => getWithJson('/rest/etapes/:etapeId/etapeDocuments', { etapeId }), - getEtapeAvisByEtapeId: async etapeId => getWithJson('/rest/etapes/:etapeId/etapeAvis', { etapeId }), + getEtapeDocumentsByEtapeId: async etapeId => newGetWithJson('/rest/etapes/:etapeId/etapeDocuments', { etapeId }), + getEtapeAvisByEtapeId: async etapeId => newGetWithJson('/rest/etapes/:etapeId/etapeAvis', { etapeId }), getEtape: async etapeIdOrSlug => { const etape = await getWithJson('/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug }) diff --git a/packages/ui/src/components/etape/etape-avis-edit.tsx b/packages/ui/src/components/etape/etape-avis-edit.tsx index b7c791db9..ed976f763 100644 --- a/packages/ui/src/components/etape/etape-avis-edit.tsx +++ b/packages/ui/src/components/etape/etape-avis-edit.tsx @@ -6,7 +6,7 @@ import { ApiClient } from '../../api/api-client' import { FunctionalComponent, computed, defineComponent, onMounted, ref, watch } from 'vue' import { isNonEmptyArray, isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, NonEmptyArray } from 'camino-common/src/typescript-tools' import { LoadingElement } from '../_ui/functional-loader' -import { AsyncData } from '../../api/client-rest' +import { AsyncData, asyncDataAutomaticLoad } from '../../api/client-rest' import { DsfrButtonIcon } from '../_ui/dsfr-button' import { AddEtapeAvisPopup } from './add-etape-avis-popup' import { dateFormat, FirstEtapeDate } from 'camino-common/src/date' @@ -23,6 +23,7 @@ import { Column, TableSimple } from '../_ui/table-simple' import { TableRow } from '../_ui/table' import { getAvisTypes } from 'camino-common/src/avisTypes' import { isArmMecanise } from 'camino-common/src/static/mecanise' +import { useState } from '@/utils/vue-tsx-utils' interface Props { tde: { @@ -44,25 +45,16 @@ type WithIndex = { index: number } type EtapeAvisModificationWithIndex = EtapeAvisModification & WithIndex export const EtapeAvisEdit = defineComponent<Props>(props => { - const etapeAvis = ref<AsyncData<EtapeAvis[]>>({ status: 'LOADING' }) + const [etapeAvis, setEtapeAvis] = useState<AsyncData<EtapeAvis[]>>({ status: 'LOADING' }) onMounted(async () => { - if (isNotNullNorUndefined(props.etapeId)) { - etapeAvis.value = { status: 'LOADING' } - try { - const result = await props.apiClient.getEtapeAvisByEtapeId(props.etapeId) - - etapeAvis.value = { status: 'LOADED', value: result } - } catch (e: any) { - console.error('error', e) - etapeAvis.value = { - status: 'ERROR', - message: e.message ?? "Une erreur s'est produite", - } + await asyncDataAutomaticLoad(() => { + if (isNotNullNorUndefined(props.etapeId)) { + return props.apiClient.getEtapeAvisByEtapeId(props.etapeId) + } else { + return Promise.resolve([]) } - } else { - etapeAvis.value = { status: 'LOADED', value: [] } - } + }, setEtapeAvis) if (etapeAvis.value.status === 'LOADED') { props.onChange(etapeAvis.value.value) } diff --git a/packages/ui/src/components/etape/etape-documents-edit.tsx b/packages/ui/src/components/etape/etape-documents-edit.tsx index 5035f9a2f..9a1fb0df6 100644 --- a/packages/ui/src/components/etape/etape-documents-edit.tsx +++ b/packages/ui/src/components/etape/etape-documents-edit.tsx @@ -8,7 +8,7 @@ import { SDOMZoneId } from 'camino-common/src/static/sdom' import { isNonEmptyArray, isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, NonEmptyArray } from 'camino-common/src/typescript-tools' import { AutreDocumentType, AutreDocumentTypeId, DocumentType, DocumentTypeId, DocumentsTypes } from 'camino-common/src/static/documentsTypes' import { LoadingElement } from '../_ui/functional-loader' -import { AsyncData } from '../../api/client-rest' +import { AsyncData, asyncDataAutomaticLoad } from '../../api/client-rest' import { DsfrButtonIcon } from '../_ui/dsfr-button' import { getVisibilityLabel, sortDocumentsColumn } from './etape-documents' import { AddEtapeDocumentPopup } from './add-etape-document-popup' @@ -21,6 +21,7 @@ import { Column, TableSimple } from '../_ui/table-simple' import { TableRow } from '../_ui/table' import { getDocuments } from 'camino-common/src/static/titresTypes_demarchesTypes_etapesTypes/documents' import { isArmMecanise } from 'camino-common/src/static/mecanise' +import { useState } from '@/utils/vue-tsx-utils' interface Props { tde: { @@ -43,25 +44,16 @@ type WithIndex = { index: number } type EtapeDocumentModificationWithIndex = EtapeDocumentModification & WithIndex export const EtapeDocumentsEdit = defineComponent<Props>(props => { - const etapeDocuments = ref<AsyncData<GetEtapeDocumentsByEtapeId>>({ status: 'LOADING' }) + const [etapeDocuments, setEtapeDocuments] = useState<AsyncData<GetEtapeDocumentsByEtapeId>>({ status: 'LOADING' }) onMounted(async () => { - if (isNotNullNorUndefined(props.etapeId)) { - etapeDocuments.value = { status: 'LOADING' } - try { - const result = await props.apiClient.getEtapeDocumentsByEtapeId(props.etapeId) - - etapeDocuments.value = { status: 'LOADED', value: result } - } catch (e: any) { - console.error('error', e) - etapeDocuments.value = { - status: 'ERROR', - message: e.message ?? "Une erreur s'est produite", - } + await asyncDataAutomaticLoad(() => { + if (isNotNullNorUndefined(props.etapeId)) { + return props.apiClient.getEtapeDocumentsByEtapeId(props.etapeId) } - } else { - etapeDocuments.value = { status: 'LOADED', value: { etapeDocuments: [] } } - } + return Promise.resolve({ etapeDocuments: [] }) + }, setEtapeDocuments) + if (etapeDocuments.value.status === 'LOADED') { props.completeUpdate(etapeDocuments.value.value.etapeDocuments) } -- GitLab From 0909a358828604144e89b0e8ca5b6f2b5678e3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com> Date: Tue, 25 Mar 2025 09:56:25 +0100 Subject: [PATCH 3/6] fix daily flush --- packages/api/src/business/daily.ts | 8 +++----- packages/api/src/database/init.ts | 3 ++- packages/api/src/scripts/daily.ts | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/api/src/business/daily.ts b/packages/api/src/business/daily.ts index e63278530..935e8be6f 100644 --- a/packages/api/src/business/daily.ts +++ b/packages/api/src/business/daily.ts @@ -26,7 +26,7 @@ import { allEtapesConsentementUpdate } from './processes/titres-etapes-consentem import { etapesCompletesCheck } from '../tools/etapes/etapes-complete-check' import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, NonEmptyArray } from 'camino-common/src/typescript-tools' import { config } from '../config' -import { createReadStream, createWriteStream, readFileSync, writeFileSync } from 'node:fs' +import { createReadStream, createWriteStream, readFileSync, writeFileSync, WriteStream } from 'node:fs' import { createInterface } from 'node:readline' import { fetch } from 'undici' import { mailjetSend } from '../tools/api-mailjet/emails' @@ -187,9 +187,7 @@ const dailyResult = (daily: Daily | null): DailyResult => { return { changes: false } } -export const daily = async (pool: Pool, logFile: string): Promise<void> => { - const output = createWriteStream(logFile, { flush: true, autoClose: true }) - +export const daily = async (pool: Pool, logFile: string, stream: WriteStream): Promise<void> => { // Réinitialise les logs qui seront envoyés par email writeFileSync(logFile, '') let resume: DailyResult = { changes: true, logs: ["Le daily ne s'est pas lancé"] } @@ -201,7 +199,7 @@ export const daily = async (pool: Pool, logFile: string): Promise<void> => { if (isNotNullNorUndefined(config().CAMINO_STAGE)) { await new Promise<void>(resolve => { - output.end(() => resolve()) + stream.end(() => resolve()) }) const emailBody = `Résultats de ${config().ENV} \n${resume.changes ? resume.logs.join('\n') : 'Pas de changement\n'}\n${readFileSync(logFile).toString()}` try { diff --git a/packages/api/src/database/init.ts b/packages/api/src/database/init.ts index 7a65fd750..a0a2dc717 100644 --- a/packages/api/src/database/init.ts +++ b/packages/api/src/database/init.ts @@ -3,11 +3,12 @@ import { daily } from '../business/daily' import type { Pool } from 'pg' import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools' import { config } from '../config' +import { createWriteStream } from 'node:fs' export const databaseInit = async (pool: Pool): Promise<void> => { await knex.migrate.latest() if (isNotNullNorUndefined(config().CAMINO_STAGE)) { // pas de await pour ne pas bloquer le démarrage de l’appli - daily(pool, '/tmp/unused') // eslint-disable-line @typescript-eslint/no-floating-promises + daily(pool, '/tmp/unused', createWriteStream('/tmp/unused')) // eslint-disable-line @typescript-eslint/no-floating-promises } } diff --git a/packages/api/src/scripts/daily.ts b/packages/api/src/scripts/daily.ts index 117f54a1c..51776c97b 100644 --- a/packages/api/src/scripts/daily.ts +++ b/packages/api/src/scripts/daily.ts @@ -35,7 +35,7 @@ const tasks = async () => { // Réinitialise les logs qui seront envoyés par email writeFileSync(logFile, '') try { - await daily(pool, logFile) + await daily(pool, logFile, output) } catch (e) { console.error('Erreur durant le daily', e) } -- GitLab From 209c72e5430903f3fc0e64c282b4964e423fb6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com> Date: Tue, 25 Mar 2025 10:40:45 +0100 Subject: [PATCH 4/6] fix edition --- .../rest/etape-modifier.test.integration.ts | 2 +- packages/api/src/business/daily.ts | 2 +- .../etape/etape-documents-edit.stories.tsx | 2 +- ...ents-edit.stories_snapshots_WithError.html | 4 +- .../src/components/etape/etape-edit-form.tsx | 148 +++++++++--------- packages/ui/src/components/titre.tsx | 4 +- 6 files changed, 83 insertions(+), 79 deletions(-) diff --git a/packages/api/src/api/rest/etape-modifier.test.integration.ts b/packages/api/src/api/rest/etape-modifier.test.integration.ts index e04e67fdc..61701ed9b 100644 --- a/packages/api/src/api/rest/etape-modifier.test.integration.ts +++ b/packages/api/src/api/rest/etape-modifier.test.integration.ts @@ -1,5 +1,5 @@ import { dbManager } from '../../../tests/db-manager' -import { restNewPutCall, restCall, restNewPostCall, restNewCall } from '../../../tests/_utils/index' +import { restNewPutCall, restNewPostCall, restNewCall } from '../../../tests/_utils/index' import Titres from '../../database/models/titres' import { userSuper } from '../../database/user-super' import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations' diff --git a/packages/api/src/business/daily.ts b/packages/api/src/business/daily.ts index 935e8be6f..d659a33cb 100644 --- a/packages/api/src/business/daily.ts +++ b/packages/api/src/business/daily.ts @@ -26,7 +26,7 @@ import { allEtapesConsentementUpdate } from './processes/titres-etapes-consentem import { etapesCompletesCheck } from '../tools/etapes/etapes-complete-check' import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, NonEmptyArray } from 'camino-common/src/typescript-tools' import { config } from '../config' -import { createReadStream, createWriteStream, readFileSync, writeFileSync, WriteStream } from 'node:fs' +import { createReadStream, readFileSync, writeFileSync, WriteStream } from 'node:fs' import { createInterface } from 'node:readline' import { fetch } from 'undici' import { mailjetSend } from '../tools/api-mailjet/emails' diff --git a/packages/ui/src/components/etape/etape-documents-edit.stories.tsx b/packages/ui/src/components/etape/etape-documents-edit.stories.tsx index a3a852ad7..a9038a3fe 100644 --- a/packages/ui/src/components/etape/etape-documents-edit.stories.tsx +++ b/packages/ui/src/components/etape/etape-documents-edit.stories.tsx @@ -323,7 +323,7 @@ export const Loading: StoryFn = () => ( ) export const WithError: StoryFn = () => ( <EtapeDocumentsEdit - apiClient={{ ...apiClient, getEtapeDocumentsByEtapeId: () => Promise.reject(new Error('Une erreur est survenue')) }} + apiClient={{ ...apiClient, getEtapeDocumentsByEtapeId: () => Promise.resolve({ message: 'Une erreur est survenue' }) }} contenu={{}} etapeId={etapeIdValidator.parse('etapeId')} sdomZoneIds={[]} diff --git a/packages/ui/src/components/etape/etape-documents-edit.stories_snapshots_WithError.html b/packages/ui/src/components/etape/etape-documents-edit.stories_snapshots_WithError.html index 5cd6a4951..96ab73995 100644 --- a/packages/ui/src/components/etape/etape-documents-edit.stories_snapshots_WithError.html +++ b/packages/ui/src/components/etape/etape-documents-edit.stories_snapshots_WithError.html @@ -1,7 +1,7 @@ <div class="" style="display: flex; justify-content: center;"> - <div class="fr-alert fr-alert--error fr-alert--sm"> + <!----> + <div class="fr-alert fr-alert--error fr-alert--sm" role="alert"> <p>Une erreur est survenue</p> </div> <!----> - <!----> </div> \ No newline at end of file diff --git a/packages/ui/src/components/etape/etape-edit-form.tsx b/packages/ui/src/components/etape/etape-edit-form.tsx index 554591a0c..9ce7e8bde 100644 --- a/packages/ui/src/components/etape/etape-edit-form.tsx +++ b/packages/ui/src/components/etape/etape-edit-form.tsx @@ -82,11 +82,9 @@ export type Props = { > } -type EtapeEditFormDocuments = { - etapeDocuments: (EtapeDocument | TempEtapeDocument)[] - entrepriseDocuments: SelectedEntrepriseDocument[] - etapeAvis: (EtapeAvis | TempEtapeAvis)[] -} +type EtapeDocumentEdit = (EtapeDocument | TempEtapeDocument)[] +type EntrepriseDocumentEdit = SelectedEntrepriseDocument[] +type EtapeAvisEdit = (EtapeAvis | TempEtapeAvis)[] const mergeFlattenEtapeWithNewHeritage = ( etape: CoreEtapeCreationOrModification, @@ -218,11 +216,10 @@ export const EtapeEditForm = defineComponent<Props>(props => { const [etape, setEtape] = useState<AsyncData<CoreEtapeCreationOrModification | null>>({ status: 'LOADED', value: null }) const [perimetreInfos, setPerimetreInfos] = useState<PerimetreInformations>(props.perimetre) - const [documents, setDocuments] = useState<EtapeEditFormDocuments>({ - etapeDocuments: [], - entrepriseDocuments: [], - etapeAvis: [], - }) + const [etapeDocuments, setEtapeDocuments] = useState<EtapeDocumentEdit>([]) + const [entrepriseDocuments, setEntrepriseDocuments] = useState<EntrepriseDocumentEdit>([]) + const [etapeAvis, setEtapeAvis] = useState<EtapeAvisEdit>([]) + onMounted(async () => { if (isNotNullNorUndefined(props.etape.date) && isNotNullNorUndefined(props.etape.typeId) && isNotNullNorUndefined(props.etape.statutId)) { setEtape({ status: 'LOADED', value: { ...props.etape, date: props.etape.date, typeId: props.etape.typeId, statutId: props.etape.statutId } }) @@ -257,9 +254,8 @@ export const EtapeEditForm = defineComponent<Props>(props => { } } - const setEtapeAndDocument = (etape: CoreEtapeCreationOrModification, documents: EtapeEditFormDocuments) => { + const setEtapeInternal = (etape: CoreEtapeCreationOrModification) => { setEtape({ status: 'LOADED', value: etape }) - setDocuments(documents) } const alertes = computed<EtapeAlerte[]>(() => { @@ -299,11 +295,11 @@ export const EtapeEditForm = defineComponent<Props>(props => { props.titreTypeId, props.demarcheId, props.demarcheTypeId, - documents.value.etapeDocuments, - documents.value.entrepriseDocuments.map(e => ({ entreprise_document_type_id: e.documentTypeId, entreprise_id: e.entrepriseId })), + etapeDocuments.value, + entrepriseDocuments.value.map(e => ({ entreprise_document_type_id: e.documentTypeId, entreprise_id: e.entrepriseId })), props.perimetre.sdomZoneIds, props.perimetre.communes, - documents.value.etapeAvis, + etapeAvis.value, props.user, props.firstEtapeDate ?? firstEtapeDateValidator.parse(etape.value.value.date) ) @@ -322,11 +318,11 @@ export const EtapeEditForm = defineComponent<Props>(props => { props.demarcheId, props.demarcheTypeId, etape.value.value, - documents.value.etapeDocuments, - documents.value.entrepriseDocuments.map(({ documentTypeId, entrepriseId }) => ({ entreprise_document_type_id: documentTypeId, entreprise_id: entrepriseId })), + etapeDocuments.value, + entrepriseDocuments.value.map(({ documentTypeId, entrepriseId }) => ({ entreprise_document_type_id: documentTypeId, entreprise_id: entrepriseId })), props.perimetre.sdomZoneIds, props.perimetre.communes, - documents.value.etapeAvis, + etapeAvis.value, props.firstEtapeDate ?? firstEtapeDateValidator.parse(etape.value.value.date) ) } @@ -387,17 +383,18 @@ export const EtapeEditForm = defineComponent<Props>(props => { ...etape.value.value, id: props.etape.id, titreDemarcheId: props.demarcheId, - ...documents.value, - entrepriseDocumentIds: documents.value.entrepriseDocuments.map(({ id }) => id), + etapeAvis: etapeAvis.value, + etapeDocuments: etapeDocuments.value, + entrepriseDocumentIds: entrepriseDocuments.value.map(({ id }) => id), ...heritage, }) } else { etapeId = await props.apiClient.etapeCreer({ ...etape.value.value, titreDemarcheId: props.demarcheId, - etapeAvis: documents.value.etapeAvis, - etapeDocuments: documents.value.etapeDocuments.filter(value => 'temp_document_name' in value), - entrepriseDocumentIds: documents.value.entrepriseDocuments.map(({ id }) => id), + etapeAvis: etapeAvis.value, + etapeDocuments: etapeDocuments.value.filter(value => 'temp_document_name' in value), + entrepriseDocumentIds: entrepriseDocuments.value.map(({ id }) => id), ...heritage, }) } @@ -461,7 +458,19 @@ export const EtapeEditForm = defineComponent<Props>(props => { <> {isNotNullNorUndefined(etapeLoaded) ? ( <> - <EtapeEditFormInternal {...props} perimetre={perimetreInfos.value} etape={etapeLoaded} documents={documents.value} setEtape={setEtapeAndDocument} alertesUpdate={setPerimetreInfos} /> + <EtapeEditFormInternal + {...props} + perimetre={perimetreInfos.value} + etape={etapeLoaded} + setEtape={setEtapeInternal} + setEtapeDocuments={setEtapeDocuments} + setEntrepriseDocuments={setEntrepriseDocuments} + setEtapeAvis={setEtapeAvis} + alertesUpdate={setPerimetreInfos} + etapeDocuments={etapeDocuments.value} + entrepriseDocuments={entrepriseDocuments.value} + etapeAvis={etapeAvis.value} + /> <PureFormSaveBtn class="fr-mt-2w fr-pt-2w fr-pb-2w" style={{ position: 'sticky', bottom: 0, background: 'white', zIndex: 100000 }} @@ -485,16 +494,18 @@ export const EtapeEditForm = defineComponent<Props>(props => { const EtapeEditFormInternal = defineComponent< { etape: CoreEtapeCreationOrModification - documents: EtapeEditFormDocuments - setEtape: (etape: CoreEtapeCreationOrModification, documents: EtapeEditFormDocuments) => void + etapeDocuments: EtapeDocumentEdit + entrepriseDocuments: EntrepriseDocumentEdit + etapeAvis: EtapeAvisEdit + setEtape: (etape: CoreEtapeCreationOrModification) => void + setEtapeDocuments: (values: EtapeDocumentEdit) => void + setEntrepriseDocuments: (values: EntrepriseDocumentEdit) => void + setEtapeAvis: (values: EtapeAvisEdit) => void alertesUpdate: (alertes: PerimetreInformations) => void } & Omit<Props, 'etape'> >(props => { const documentsCompleteUpdate = (etapeDocuments: (EtapeDocument | TempEtapeDocument)[]) => { - props.setEtape(props.etape, { - ...props.documents, - etapeDocuments, - }) + props.setEtapeDocuments(etapeDocuments) } const firstEtapeDate = computed<FirstEtapeDate>(() => { @@ -502,73 +513,64 @@ const EtapeEditFormInternal = defineComponent< }) const avisCompleteUpdate = (etapeAvis: (EtapeAvis | TempEtapeAvis)[]) => { - props.setEtape(props.etape, { - ...props.documents, - etapeAvis, - }) + props.setEtapeAvis(etapeAvis) } const entrepriseDocumentsCompleteUpdate = (entrepriseDocuments: SelectedEntrepriseDocument[]) => { - props.setEtape(props.etape, { ...props.documents, entrepriseDocuments }) + props.setEntrepriseDocuments(entrepriseDocuments) } const onEtapePerimetreChange = (perimetreInfos: GeojsonInformations) => { - props.setEtape( - { - ...props.etape, - perimetre: { - ...props.etape.perimetre, - value: { - geojson4326Forages: isNotNullNorUndefined(props.etape.perimetre.value) ? props.etape.perimetre.value.geojson4326Forages : null, - geojsonOrigineForages: isNotNullNorUndefined(props.etape.perimetre.value) ? props.etape.perimetre.value.geojsonOrigineForages : null, - geojson4326Perimetre: perimetreInfos.geojson4326_perimetre, - geojson4326Points: perimetreInfos.geojson4326_points, - geojsonOriginePerimetre: perimetreInfos.geojson_origine_perimetre, - geojsonOriginePoints: perimetreInfos.geojson_origine_points, - geojsonOrigineGeoSystemeId: perimetreInfos.geojson_origine_geo_systeme_id, - surface: perimetreInfos.surface, - }, + props.setEtape({ + ...props.etape, + perimetre: { + ...props.etape.perimetre, + value: { + geojson4326Forages: isNotNullNorUndefined(props.etape.perimetre.value) ? props.etape.perimetre.value.geojson4326Forages : null, + geojsonOrigineForages: isNotNullNorUndefined(props.etape.perimetre.value) ? props.etape.perimetre.value.geojsonOrigineForages : null, + geojson4326Perimetre: perimetreInfos.geojson4326_perimetre, + geojson4326Points: perimetreInfos.geojson4326_points, + geojsonOriginePerimetre: perimetreInfos.geojson_origine_perimetre, + geojsonOriginePoints: perimetreInfos.geojson_origine_points, + geojsonOrigineGeoSystemeId: perimetreInfos.geojson_origine_geo_systeme_id, + surface: perimetreInfos.surface, }, }, - props.documents - ) + }) props.alertesUpdate({ superposition_alertes: perimetreInfos.superposition_alertes, sdomZoneIds: perimetreInfos.sdomZoneIds, communes: perimetreInfos.communes.map(({ id }) => id) }) } const onEtapePerimetreHeritageChange = (perimetre: FlattenEtape['perimetre']) => { - props.setEtape( - { - ...props.etape, - perimetre, - }, - props.documents - ) + props.setEtape({ + ...props.etape, + perimetre, + }) } const onEtapePointsChange = (geojson4326Points: FeatureCollectionPoints, geojsonOriginePoints: FeatureCollectionPoints) => { if (isNotNullNorUndefined(props.etape.perimetre.value)) { - props.setEtape({ ...props.etape, perimetre: { ...props.etape.perimetre, value: { ...props.etape.perimetre.value, geojson4326Points, geojsonOriginePoints } } }, props.documents) + props.setEtape({ ...props.etape, perimetre: { ...props.etape.perimetre, value: { ...props.etape.perimetre.value, geojson4326Points, geojsonOriginePoints } } }) } } const onEtapeForagesChange = (geojson4326Forages: FeatureCollectionForages, geojsonOrigineForages: FeatureCollectionForages) => { if (isNotNullNorUndefined(props.etape.perimetre.value)) { - props.setEtape({ ...props.etape, perimetre: { ...props.etape.perimetre, value: { ...props.etape.perimetre.value, geojson4326Forages, geojsonOrigineForages } } }, props.documents) + props.setEtape({ ...props.etape, perimetre: { ...props.etape.perimetre, value: { ...props.etape.perimetre.value, geojson4326Forages, geojsonOrigineForages } } }) } } const sectionCompleteUpdate = (sectionsEtape: SectionsEditEtape) => { - props.setEtape({ ...props.etape, contenu: sectionsEtape.contenu }, props.documents) + props.setEtape({ ...props.etape, contenu: sectionsEtape.contenu }) } const onUpdateNotes = (notes: string) => { - props.setEtape({ ...props.etape, note: { is_avertissement: props.etape.note.is_avertissement, valeur: notes } }, props.documents) + props.setEtape({ ...props.etape, note: { is_avertissement: props.etape.note.is_avertissement, valeur: notes } }) } const onUpdateNoteAvertissement = (isAvertissement: boolean) => { - props.setEtape({ ...props.etape, note: { valeur: props.etape.note.valeur, is_avertissement: isAvertissement } }, props.documents) + props.setEtape({ ...props.etape, note: { valeur: props.etape.note.valeur, is_avertissement: isAvertissement } }) } const fondamentalesCompleteUpdate = (etapeFondamentale: EtapeFondamentaleEdit) => { - props.setEtape({ ...props.etape, ...etapeFondamentale }, props.documents) + props.setEtape({ ...props.etape, ...etapeFondamentale }) } const titulairesAndAmodiataires = computed<Entreprise[]>(() => { @@ -628,10 +630,7 @@ const EtapeEditFormInternal = defineComponent< {etapeDocumentsStepIsVisible() ? ( <Bloc step={{ name: 'Liste des documents', help: null }} - complete={ - etapeDocumentsStepIsComplete(props.etape, props.demarcheTypeId, props.titreTypeId, props.demarcheId, props.documents.etapeDocuments, props.perimetre.sdomZoneIds, firstEtapeDate.value) - .valid - } + complete={etapeDocumentsStepIsComplete(props.etape, props.demarcheTypeId, props.titreTypeId, props.demarcheId, props.etapeDocuments, props.perimetre.sdomZoneIds, firstEtapeDate.value).valid} > <EtapeDocumentsEdit apiClient={props.apiClient} @@ -655,7 +654,7 @@ const EtapeEditFormInternal = defineComponent< {etapeAvisStepIsVisible(props.etape, props.titreTypeId, props.demarcheTypeId, props.demarcheId, firstEtapeDate.value, props.perimetre.communes) ? ( <Bloc step={{ name: 'Liste des avis', help: null }} - complete={etapeAvisStepIsComplete(props.etape, props.documents.etapeAvis, props.titreTypeId, props.demarcheTypeId, props.demarcheId, firstEtapeDate.value, props.perimetre.communes).valid} + complete={etapeAvisStepIsComplete(props.etape, props.etapeAvis, props.titreTypeId, props.demarcheTypeId, props.demarcheId, firstEtapeDate.value, props.perimetre.communes).valid} > <EtapeAvisEdit apiClient={props.apiClient} @@ -684,7 +683,7 @@ const EtapeEditFormInternal = defineComponent< ? "Les documents d’entreprise sont des documents propres à l'entreprise, et pourront être réutilisés pour la création d'un autre dossier et mis à jour si nécessaire. Ces documents d’entreprise sont consultables dans la fiche entreprise de votre société. Cette section permet de protéger et de centraliser les informations d'ordre privé relatives à la société et à son personnel." : null, }} - complete={entrepriseDocumentsStepIsComplete(props.etape, props.demarcheTypeId, props.titreTypeId, props.documents.entrepriseDocuments).valid} + complete={entrepriseDocumentsStepIsComplete(props.etape, props.demarcheTypeId, props.titreTypeId, props.entrepriseDocuments).valid} > <EntrepriseDocumentsEdit entreprises={titulairesAndAmodiataires.value} @@ -715,6 +714,13 @@ EtapeEditFormInternal.props = [ 'etape', 'demarcheId', 'demarcheTypeId', + 'etapeDocuments', + 'entrepriseDocuments', + 'etapeAvis', + 'setEtape', + 'setEtapeDocuments', + 'setEntrepriseDocuments', + 'setEtapeAvis', 'titreTypeId', 'titreSlug', 'user', diff --git a/packages/ui/src/components/titre.tsx b/packages/ui/src/components/titre.tsx index 05badb3cf..b625a4bc8 100644 --- a/packages/ui/src/components/titre.tsx +++ b/packages/ui/src/components/titre.tsx @@ -197,10 +197,8 @@ export const PureTitre = defineComponent<Props>(props => { watch( () => props.titreIdOrSlug, - async (newValue, oldValue) => { - console.log('PLOP', props.titreIdOrSlug, newValue, oldValue) + async (_newValue, _oldValue) => { if (titreData.value.status !== 'LOADED' || (titreData.value.value.id !== props.titreIdOrSlug && titreData.value.value.slug !== props.titreIdOrSlug)) { - console.log('Changement de titre') await updateTitre(props.titreIdOrSlug) } } -- GitLab From b8bf3f06a9abe1033a4dfd03810073078a4f3ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com> Date: Tue, 25 Mar 2025 11:10:13 +0100 Subject: [PATCH 5/6] remove fixme --- packages/api/src/api/rest/etapes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/api/src/api/rest/etapes.ts b/packages/api/src/api/rest/etapes.ts index 350e62de5..46487ac62 100644 --- a/packages/api/src/api/rest/etapes.ts +++ b/packages/api/src/api/rest/etapes.ts @@ -89,7 +89,6 @@ export const getEtapeEntrepriseDocuments: RestNewGetCall<'/rest/etapes/:etapeId/ ) ) -// FIXME : traiter la partie front + tests type GetEtapeDocumentsErrors = EffectDbQueryAndValidateErrors | GetEtapeDataForEditionErrors | GetDocumentsByEtapeIdErrors export const getEtapeDocuments: RestNewGetCall<'/rest/etapes/:etapeId/etapeDocuments'> = (rootPipe): Effect.Effect<GetEtapeDocumentsByEtapeId, CaminoApiError<GetEtapeDocumentsErrors>> => rootPipe.pipe( @@ -135,7 +134,6 @@ export const getEtapeDocuments: RestNewGetCall<'/rest/etapes/:etapeId/etapeDocum ) ) -// FIXME : traiter la partie front + tests type GetEtapeAvisErrors = EffectDbQueryAndValidateErrors | GetEtapeDataForEditionErrors | GetEtapeAvisLargeObjectIdsByEtapeIdErrors export const getEtapeAvis: RestNewGetCall<'/rest/etapes/:etapeId/etapeAvis'> = (rootPipe): Effect.Effect<GetEtapeAvisByEtapeId, CaminoApiError<GetEtapeAvisErrors>> => rootPipe.pipe( -- GitLab From 9002ddf491852b4ed1264014a45323d9637ad696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com> Date: Tue, 25 Mar 2025 11:16:18 +0100 Subject: [PATCH 6/6] better zodParseEffectTyped --- .../database/queries/titres-etapes.queries.ts | 16 ++-------------- packages/api/src/tools/fp-tools.ts | 13 +++++++++++-- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/api/src/database/queries/titres-etapes.queries.ts b/packages/api/src/database/queries/titres-etapes.queries.ts index 6d2fdc8dd..f4cc9f320 100644 --- a/packages/api/src/database/queries/titres-etapes.queries.ts +++ b/packages/api/src/database/queries/titres-etapes.queries.ts @@ -64,7 +64,7 @@ import { CommuneId } from 'camino-common/src/static/communes' import { EtapeStatutId, etapeStatutIdValidator } from 'camino-common/src/static/etapesStatuts' import { contenuValidator, FlattenedContenu, heritageContenuValidator } from 'camino-common/src/etape-form' import { DemarcheTypeId, demarcheTypeIdValidator } from 'camino-common/src/static/demarchesTypes' -import { Effect, Match, Option, pipe } from 'effect' +import { Effect, Option, pipe } from 'effect' import { CaminoError } from 'camino-common/src/zod-tools' import { callAndExit, shortCircuitError, zodParseEffectTyped } from '../../tools/fp-tools' import { TempDocumentName } from 'camino-common/src/document' @@ -526,19 +526,7 @@ export const getDocumentsByEtapeId = ( demarche: CanReadDemarche ): Effect.Effect<EtapeDocument[], CaminoError<GetDocumentsByEtapeIdErrors>> => getEtapeDocumentLargeObjectIdsByEtapeId(titre_etape_id, pool, user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires, etapeTypeId, demarche).pipe( - Effect.flatMap(result => - zodParseEffectTyped(z.array(etapeDocumentValidator), result).pipe( - Effect.mapError(caminoError => - Match.value(caminoError.message).pipe( - Match.when('Problème de validation de données', () => ({ - ...caminoError, - message: errorParseGetDocumentsByEtapeId, - })), - Match.exhaustive - ) - ) - ) - ) + Effect.flatMap(result => zodParseEffectTyped(z.array(etapeDocumentValidator), result, errorParseGetDocumentsByEtapeId)) ) const getEtapesWithAutomaticStatutValidator = z.object({ diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts index 4fedc8aea..b670998c5 100644 --- a/packages/api/src/tools/fp-tools.ts +++ b/packages/api/src/tools/fp-tools.ts @@ -3,8 +3,14 @@ import { Cause, Effect, Exit, pipe } from 'effect' import { ZodTypeAny } from 'zod' import { fromError, isZodErrorLike } from 'zod-validation-error' +/** + * @deprecated use more precise message + */ export type ZodUnparseable = 'Problème de validation de données' +/** + * @deprecated use zodParseEffectTyped + */ export const zodParseEffectCallback = <T extends ZodTypeAny>(validator: T) => (value: unknown): Effect.Effect<T['_output'], CaminoError<ZodUnparseable>> => @@ -20,6 +26,9 @@ const zodErrorToDetail = (myError: unknown): string | undefined => { return undefined } +/** + * @deprecated use zodParseEffectTyped + */ export const zodParseEffect = <T extends ZodTypeAny>(validator: T, item: unknown): Effect.Effect<T['_output'], CaminoError<ZodUnparseable>> => { return Effect.try({ try: () => validator.parse(item), @@ -27,10 +36,10 @@ export const zodParseEffect = <T extends ZodTypeAny>(validator: T, item: unknown }) } -export const zodParseEffectTyped = <T extends ZodTypeAny>(validator: T, item: T['_output']): Effect.Effect<T['_output'], CaminoError<ZodUnparseable>> => { +export const zodParseEffectTyped = <T extends ZodTypeAny, U extends string>(validator: T, item: T['_output'], errorMessage: U): Effect.Effect<T['_output'], CaminoError<U>> => { return Effect.try({ try: () => validator.parse(item), - catch: myError => ({ message: 'Problème de validation de données', detail: zodErrorToDetail(myError), zodErrorReadableMessage: zodErrorToReadableMessage(myError) }), + catch: myError => ({ message: errorMessage, detail: zodErrorToDetail(myError), zodErrorReadableMessage: zodErrorToReadableMessage(myError) }), }) } -- GitLab