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