From a3fa89b47908c18b9314903f075e3bbe0bdb9236 Mon Sep 17 00:00:00 2001
From: SAFINE LAGET Anis <anis.safine@beta.gouv.fr>
Date: Tue, 25 Mar 2025 13:24:23 +0000
Subject: [PATCH] chore(eslint): interdit les utilisations deprecated
 (pub/pnm-public/camino!1685)

---
 packages/api/eslint.config.mjs                |   1 +
 .../api/graphql/resolvers/titres-activites.ts |   7 +-
 .../api/src/api/rest/activites.queries.ts     | 136 ++++++++++++------
 packages/api/src/api/rest/activites.ts        |  24 ++--
 packages/common/eslint.config.mjs             |   1 +
 packages/ui/eslint.config.mjs                 |   1 +
 packages/ui/package.json                      |   4 +-
 7 files changed, 108 insertions(+), 66 deletions(-)

diff --git a/packages/api/eslint.config.mjs b/packages/api/eslint.config.mjs
index 9664e6775..41ba37d40 100644
--- a/packages/api/eslint.config.mjs
+++ b/packages/api/eslint.config.mjs
@@ -92,6 +92,7 @@ export default [
 
       '@typescript-eslint/strict-boolean-expressions': 'error',
       '@typescript-eslint/no-empty-object-type': 0,
+      '@typescript-eslint/no-deprecated': 'warn',
     },
   },
   {
diff --git a/packages/api/src/api/graphql/resolvers/titres-activites.ts b/packages/api/src/api/graphql/resolvers/titres-activites.ts
index 4859a2968..a0edd0fd9 100644
--- a/packages/api/src/api/graphql/resolvers/titres-activites.ts
+++ b/packages/api/src/api/graphql/resolvers/titres-activites.ts
@@ -27,6 +27,7 @@ import {
 import { ActiviteId } from 'camino-common/src/activite'
 import { getSectionsWithValue } from 'camino-common/src/static/titresTypes_demarchesTypes_etapesTypes/sections'
 import { getUtilisateursEmailsByEntrepriseIds } from '../../../database/queries/utilisateurs.queries'
+import { callAndExit } from '../../../tools/fp-tools'
 
 /**
  * Retourne les activités
@@ -161,13 +162,11 @@ export const activiteDeposer = async ({ id }: { id: ActiviteId }, { user, pool }
   try {
     if (!user) throw new Error('droits insuffisants')
 
-    const titreTypeId = memoize(() => titreTypeIdByActiviteId(id, pool))
+    const titreTypeId = memoize(() => callAndExit(titreTypeIdByActiviteId(id, pool)))
     const administrationsLocales = memoize(() => administrationsLocalesByActiviteId(id, pool))
     const entreprisesTitulairesOuAmodiataires = memoize(() => entreprisesTitulairesOuAmoditairesByActiviteId(id, pool))
 
-    const activite = await getActiviteById(id, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
-
-    if (!activite) throw new Error("l'activité n'existe pas")
+    const activite = await callAndExit(getActiviteById(id, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
 
     const activitesDocuments = await getActiviteDocumentsByActiviteId(id, pool)
     const sectionsWithValue = getSectionsWithValue(activite.sections, activite.contenu)
diff --git a/packages/api/src/api/rest/activites.queries.ts b/packages/api/src/api/rest/activites.queries.ts
index 4aa7b01fd..c3b29e96b 100644
--- a/packages/api/src/api/rest/activites.queries.ts
+++ b/packages/api/src/api/rest/activites.queries.ts
@@ -1,5 +1,5 @@
 import { sql } from '@pgtyped/runtime'
-import { Redefine, dbQueryAndValidate } from '../../pg-database'
+import { EffectDbQueryAndValidateErrors, Redefine, dbQueryAndValidate, effectDbQueryAndValidate } from '../../pg-database'
 import {
   IDeleteActiviteDocumentQueryQuery,
   IGetActiviteByIdQueryQuery,
@@ -33,20 +33,28 @@ import { ACTIVITES_STATUTS_IDS, ActivitesStatutId } from 'camino-common/src/stat
 import { CaminoDate, getCurrent } from 'camino-common/src/date'
 import { titreIdValidator } from 'camino-common/src/validators/titres'
 import { ActivitesTypesId } from 'camino-common/src/static/activitesTypes'
-import { SimplePromiseFn } from 'camino-common/src/typescript-tools'
+import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, SimplePromiseFn } from 'camino-common/src/typescript-tools'
 import { ActiviteDocumentTypeId } from 'camino-common/src/static/documentsTypes'
 import { sectionValidator } from 'camino-common/src/static/titresTypes_demarchesTypes_etapesTypes/sections'
+import { Effect } from 'effect'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { callAndExit } from '../../tools/fp-tools'
+
+const typeDeTitreIntrouvablePourActivite = `Pas de type de titre trouvé pour l'activité` as const
+type TitreTypeIdByActiviteIdErrors = EffectDbQueryAndValidateErrors | typeof typeDeTitreIntrouvablePourActivite
+export const titreTypeIdByActiviteId = (activiteId: ActiviteIdOrSlug, pool: Pool): Effect.Effect<TitreTypeId, CaminoError<TitreTypeIdByActiviteIdErrors>> =>
+  Effect.Do.pipe(
+    Effect.flatMap(() => effectDbQueryAndValidate(getTitreTypeIdByActiviteId, { activiteId }, pool, titreTypeIdObjectValidator)),
+    Effect.filterOrFail(
+      result => isNotNullNorUndefinedNorEmpty(result) && result.length === 1,
+      () => ({ message: typeDeTitreIntrouvablePourActivite, detail: `Pas de type de titre trouvé pour l'activité ${activiteId}` })
+    ),
+    Effect.map(result => result[0].titre_type_id)
+  )
 
-export const titreTypeIdByActiviteId = async (activiteId: ActiviteIdOrSlug, pool: Pool): Promise<TitreTypeId> => {
-  const typeIds = await dbQueryAndValidate(getTitreTypeIdByActiviteId, { activiteId }, pool, titreTypeIdObjectValidator)
-  if (typeIds.length === 0) {
-    throw new Error(`Pas de type de titre trouvé pour l'activité ${activiteId}`)
-  }
-
-  return typeIds[0].titre_type_id
-}
-
-export const updateActiviteQuery = async (
+const miseAJourActiviteInterdite = `Interdiction d'éditer une activité` as const
+type UpdateActiviteQueryErrors = EffectDbQueryAndValidateErrors | typeof miseAJourActiviteInterdite
+export const updateActiviteQuery = (
   pool: Pool,
   user: User,
   activiteId: ActiviteId,
@@ -54,12 +62,32 @@ export const updateActiviteQuery = async (
   titreTypeId: SimplePromiseFn<TitreTypeId>,
   titresAdministrationsLocales: SimplePromiseFn<AdministrationId[]>,
   entreprisesTitulairesOuAmodiataires: SimplePromiseFn<EntrepriseId[]>
-): Promise<void> => {
-  if (user === null || user === undefined || !(await canEditActivite(user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires, ACTIVITES_STATUTS_IDS.EN_CONSTRUCTION))) {
-    throw new Error("Interdiction d'éditer une activité")
-  }
-  await dbQueryAndValidate(updateActiviteDb, { userId: user.id, activiteId, dateSaisie: getCurrent(), activiteStatutId: ACTIVITES_STATUTS_IDS.EN_CONSTRUCTION, contenu }, pool, z.void())
-}
+): Effect.Effect<void, CaminoError<UpdateActiviteQueryErrors>> =>
+  Effect.Do.pipe(
+    Effect.bind('userNotNull', () =>
+      Effect.Do.pipe(
+        Effect.map(() => user),
+        Effect.filterOrFail(
+          (userNotNull): userNotNull is NonNullable<User> => isNotNullNorUndefined(userNotNull),
+          () => ({ message: miseAJourActiviteInterdite, detail: 'Utilisateur null ou undefined' })
+        )
+      )
+    ),
+    Effect.bind('canEditActivite', () =>
+      Effect.tryPromise({
+        try: () => canEditActivite(user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires, ACTIVITES_STATUTS_IDS.EN_CONSTRUCTION),
+        catch: error => ({ message: miseAJourActiviteInterdite, detail: 'Appel à canEditActivite échoué', extra: error }),
+      })
+    ),
+    Effect.filterOrFail(
+      ({ canEditActivite }) => canEditActivite,
+      () => ({ message: miseAJourActiviteInterdite, detail: "Utilisateur n'a pas le droit de modifier cette activité", extra: { activiteId } })
+    ),
+    Effect.tap(({ userNotNull }) =>
+      effectDbQueryAndValidate(updateActiviteDb, { userId: userNotNull.id, activiteId, dateSaisie: getCurrent(), activiteStatutId: ACTIVITES_STATUTS_IDS.EN_CONSTRUCTION, contenu }, pool, z.void())
+    ),
+    Effect.flatMap(() => Effect.void)
+  )
 
 const updateActiviteDb = sql<
   Redefine<IUpdateActiviteDbQuery, { userId: string; dateSaisie: CaminoDate; activiteId: ActiviteId; activiteStatutId: ActivitesStatutId; contenu: Contenu }, void>
@@ -89,27 +117,36 @@ const dbActiviteValidator = activiteValidator
     utilisateur_id: utilisateurIdValidator.nullable(),
   })
 
+const activiteInterdite = `Lecture de l'activité impossible` as const
+const activiteIntrouvable = `Pas d'activité trouvée` as const
+type GetActiviteByIdErrors = EffectDbQueryAndValidateErrors | typeof activiteInterdite | typeof activiteIntrouvable
 export type DbActivite = z.infer<typeof dbActiviteValidator>
-export const getActiviteById = async (
+export const getActiviteById = (
   activiteId: ActiviteIdOrSlug,
   pool: Pool,
   user: User,
   titreTypeId: SimplePromiseFn<TitreTypeId>,
   titresAdministrationsLocales: SimplePromiseFn<AdministrationId[]>,
   entreprisesTitulairesOuAmodiataires: SimplePromiseFn<EntrepriseId[]>
-): Promise<(DbActivite & { suppression: boolean }) | null> => {
-  const canRead = await canReadTitreActivites(user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires)
-
-  if (!canRead) {
-    return null
-  }
-
-  const activite = await dbQueryAndValidate(getActiviteByIdQuery, { activiteId }, pool, dbActiviteValidator)
-  if (activite.length === 0) {
-    throw new Error(`Pas d'activité trouvée pour l'id '${activiteId}'`)
-  }
-
-  return { ...activite[0], suppression: canDeleteActivite(activite[0], user) }
+): Effect.Effect<DbActivite & { suppression: boolean }, CaminoError<GetActiviteByIdErrors>> => {
+  return Effect.Do.pipe(
+    Effect.bind('canRead', () =>
+      Effect.tryPromise({
+        try: () => canReadTitreActivites(user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires),
+        catch: error => ({ message: activiteInterdite, detail: 'Appel à canReadTitreActivites échoué', extra: error }),
+      })
+    ),
+    Effect.filterOrFail(
+      ({ canRead }) => canRead,
+      () => ({ message: activiteInterdite, detail: `Utilisateur ${user?.id} n'a pas la permission nécessaire` })
+    ),
+    Effect.flatMap(() => effectDbQueryAndValidate(getActiviteByIdQuery, { activiteId }, pool, dbActiviteValidator)),
+    Effect.filterOrFail(
+      result => isNotNullNorUndefinedNorEmpty(result) && result.length === 1,
+      () => ({ message: activiteIntrouvable, detail: `Pas d'activité trouvée pour l'id '${activiteId}'` })
+    ),
+    Effect.map(result => ({ ...result[0], suppression: canDeleteActivite(result[0], user) }))
+  )
 }
 
 const getActiviteByIdQuery = sql<Redefine<IGetActiviteByIdQueryQuery, { activiteId: ActiviteIdOrSlug }, DbActivite>>`
@@ -130,25 +167,32 @@ const canDeleteActivite = (activite: DbActivite, user: User): boolean => {
   return isSuper(user) && activite.suppression
 }
 
-export const activiteDeleteQuery = async (
+const suppressionActiviteInterdite = `Suppression de l'activité interdite` as const
+type ActiviteDeleteQueryErrors = EffectDbQueryAndValidateErrors
+export const activiteDeleteQuery = (
   activiteId: ActiviteId,
   pool: Pool,
   user: User,
   titreTypeId: SimplePromiseFn<TitreTypeId>,
   titresAdministrationsLocales: SimplePromiseFn<AdministrationId[]>,
   entreprisesTitulairesOuAmodiataires: SimplePromiseFn<EntrepriseId[]>
-): Promise<boolean> => {
-  const activite = await getActiviteById(activiteId, pool, user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires)
-
-  if (activite !== null && activite.suppression) {
-    await dbQueryAndValidate(activiteDocumentDeleteDb, { activiteId }, pool, z.void())
-    await dbQueryAndValidate(activiteDeleteDb, { activiteId }, pool, z.void())
-
-    return true
-  }
-
-  return false
-}
+): Effect.Effect<boolean, CaminoError<ActiviteDeleteQueryErrors>> =>
+  Effect.Do.pipe(
+    Effect.flatMap(() => getActiviteById(activiteId, pool, user, titreTypeId, titresAdministrationsLocales, entreprisesTitulairesOuAmodiataires)),
+    Effect.filterOrFail(
+      activite => activite.suppression,
+      () => ({ message: suppressionActiviteInterdite })
+    ),
+    Effect.flatMap(() =>
+      Effect.Do.pipe(
+        Effect.tap(() => effectDbQueryAndValidate(activiteDocumentDeleteDb, { activiteId }, pool, z.void())),
+        Effect.tap(() => effectDbQueryAndValidate(activiteDeleteDb, { activiteId }, pool, z.void())),
+        Effect.map(() => true)
+      )
+    ),
+    // @TODO 2025-03-25: retirer cette ligne et mieux gérer les cas d'erreurs au niveau des appelants (ne plus retourner de booléen)
+    Effect.catchAll(() => Effect.succeed(false))
+  )
 
 const activiteDeleteDb = sql<Redefine<IActiviteDeleteDbQuery, { activiteId: ActiviteId }, void>>`
 delete from titres_activites ta
@@ -309,7 +353,7 @@ export const getLargeobjectIdByActiviteDocumentId = async (activiteDocumentId: A
   if (result.length === 1) {
     const activiteDocument = result[0]
 
-    const titreTypeId = () => titreTypeIdByActiviteId(activiteDocument.activite_id, pool)
+    const titreTypeId = () => callAndExit(titreTypeIdByActiviteId(activiteDocument.activite_id, pool))
     const administrationsLocales = () => administrationsLocalesByActiviteId(activiteDocument.activite_id, pool)
     const entreprisesTitulairesOuAmodiataires = () => entreprisesTitulairesOuAmoditairesByActiviteId(activiteDocument.activite_id, pool)
     if (await canReadTitreActivites(user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)) {
diff --git a/packages/api/src/api/rest/activites.ts b/packages/api/src/api/rest/activites.ts
index e5be63cfe..7010e6dbc 100644
--- a/packages/api/src/api/rest/activites.ts
+++ b/packages/api/src/api/rest/activites.ts
@@ -70,13 +70,13 @@ export const updateActivite =
       res.sendStatus(HTTP_STATUS.BAD_REQUEST)
     } else {
       try {
-        const titreTypeId = memoize(() => titreTypeIdByActiviteId(activiteIdParsed.data, pool))
+        const titreTypeId = memoize(() => callAndExit(titreTypeIdByActiviteId(activiteIdParsed.data, pool)))
         const administrationsLocales = memoize(() => administrationsLocalesByActiviteId(activiteIdParsed.data, pool))
         const entreprisesTitulairesOuAmodiataires = memoize(() => entreprisesTitulairesOuAmoditairesByActiviteId(activiteIdParsed.data, pool))
 
-        const result = await getActiviteById(activiteIdParsed.data, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
+        const result = await callAndExit(getActiviteById(activiteIdParsed.data, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
 
-        if (result === null || !(await canEditActivite(user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, result.activite_statut_id))) {
+        if (!(await canEditActivite(user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, result.activite_statut_id))) {
           res.sendStatus(HTTP_STATUS.FORBIDDEN)
         } else {
           const parsed = activiteEditionValidator.safeParse(req.body)
@@ -85,7 +85,7 @@ export const updateActivite =
             res.sendStatus(HTTP_STATUS.BAD_REQUEST)
           } else {
             const contenu = extractContenuFromSectionWithValue(result.sections, parsed.data.sectionsWithValue)
-            await updateActiviteQuery(pool, user, result.id, contenu, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
+            await callAndExit(updateActiviteQuery(pool, user, result.id, contenu, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
 
             const activiteDocumentsToCreate = parsed.data.newTempDocuments
             const alreadyExistingDocumentIds = parsed.data.activiteDocumentIds
@@ -178,18 +178,14 @@ export const getActivite =
       res.sendStatus(HTTP_STATUS.BAD_REQUEST)
     } else {
       try {
-        const titreTypeId = memoize(() => titreTypeIdByActiviteId(activiteIdParsed.data, pool))
+        const titreTypeId = memoize(() => callAndExit(titreTypeIdByActiviteId(activiteIdParsed.data, pool)))
         const administrationsLocales = memoize(() => administrationsLocalesByActiviteId(activiteIdParsed.data, pool))
         const entreprisesTitulairesOuAmodiataires = memoize(() => entreprisesTitulairesOuAmoditairesByActiviteId(activiteIdParsed.data, pool))
 
-        const result = await getActiviteById(activiteIdParsed.data, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
+        const result = await callAndExit(getActiviteById(activiteIdParsed.data, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
 
-        if (result !== null) {
-          const activite = await formatActivite(result, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
-          res.json(activite)
-        } else {
-          res.sendStatus(HTTP_STATUS.NOT_FOUND)
-        }
+        const activite = await formatActivite(result, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
+        res.json(activite)
       } catch (e) {
         res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
         console.error(e)
@@ -205,11 +201,11 @@ export const deleteActivite =
       res.sendStatus(HTTP_STATUS.BAD_REQUEST)
     } else {
       const id = activiteIdParsed.data
-      const titreTypeId = memoize(() => titreTypeIdByActiviteId(id, pool))
+      const titreTypeId = memoize(() => callAndExit(titreTypeIdByActiviteId(id, pool)))
       const administrationsLocales = memoize(() => administrationsLocalesByActiviteId(id, pool))
       const entreprisesTitulairesOuAmodiataires = memoize(() => entreprisesTitulairesOuAmoditairesByActiviteId(id, pool))
 
-      const isOk = await activiteDeleteQuery(id, pool, req.auth, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
+      const isOk = await callAndExit(activiteDeleteQuery(id, pool, req.auth, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
       if (isOk) {
         res.sendStatus(HTTP_STATUS.NO_CONTENT)
       } else {
diff --git a/packages/common/eslint.config.mjs b/packages/common/eslint.config.mjs
index a2c552185..6d3dcff01 100644
--- a/packages/common/eslint.config.mjs
+++ b/packages/common/eslint.config.mjs
@@ -63,6 +63,7 @@ export default [{
         '@typescript-eslint/no-misused-promises': 'error',
         '@typescript-eslint/await-thenable': 'error',
         '@typescript-eslint/no-unnecessary-condition': 'error',
+        '@typescript-eslint/no-deprecated': 'warn',
 
     },
 }];
diff --git a/packages/ui/eslint.config.mjs b/packages/ui/eslint.config.mjs
index 657eee319..9dc8984b7 100644
--- a/packages/ui/eslint.config.mjs
+++ b/packages/ui/eslint.config.mjs
@@ -83,6 +83,7 @@ export default [
 
       '@typescript-eslint/no-empty-function': 0,
       '@typescript-eslint/strict-boolean-expressions': 'error',
+      '@typescript-eslint/no-deprecated': 'warn',
       //  TODO 2024-09-19 activer ça sur le front ?
       // '@typescript-eslint/no-floating-promises': 'error',
       // '@typescript-eslint/no-misused-promises': 'error',
diff --git a/packages/ui/package.json b/packages/ui/package.json
index eace0e984..b9f1138a9 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -12,8 +12,8 @@
     "dev:update": "npm-check-updates && npm install && npm audit fix",
     "start": "node ./index.js",
     "test": "vitest",
-    "lint": "prettier --write src && eslint --fix src --max-warnings=0",
-    "lint:check": "prettier --check src && eslint src --max-warnings=0",
+    "lint": "prettier --write src && eslint --fix src",
+    "lint:check": "prettier --check src && eslint src",
     "storybook": "storybook dev -p 6006",
     "storybook:build": "storybook build",
     "storybook:test": "test-storybook --browsers chromium"
-- 
GitLab