From 18d7653516596d338d0e6ffd2177a5f88e710f12 Mon Sep 17 00:00:00 2001 From: Marc Rutkowski <marc@attractive-media.fr> Date: Tue, 7 Jan 2025 15:13:08 +0100 Subject: [PATCH 1/6] =?UTF-8?q?Ajoute=20l'entr=C3=A9e=20tRPC=20de=20lectur?= =?UTF-8?q?e/m=C3=A0j=20des=20valeurs=20d'indicateur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/indicateurs/indicateurs.module.ts | 3 + .../valeurs/crud-valeurs.router.e2e-spec.ts | 192 ++++++++++++++++++ .../valeurs/crud-valeurs.router.ts | 26 +++ backend/src/utils/trpc/trpc.router.ts | 3 + 4 files changed, 224 insertions(+) create mode 100644 backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts create mode 100644 backend/src/indicateurs/valeurs/crud-valeurs.router.ts diff --git a/backend/src/indicateurs/indicateurs.module.ts b/backend/src/indicateurs/indicateurs.module.ts index f4b0413253..64408c1bb7 100644 --- a/backend/src/indicateurs/indicateurs.module.ts +++ b/backend/src/indicateurs/indicateurs.module.ts @@ -14,6 +14,7 @@ import TrajectoiresXlsxService from './trajectoires/trajectoires-xlsx.service'; import { TrajectoiresController } from './trajectoires/trajectoires.controller'; import { TrajectoiresRouter } from './trajectoires/trajectoires.router'; import { IndicateursController } from './valeurs/crud-valeurs.controller'; +import { IndicateurValeursRouter } from './valeurs/crud-valeurs.router'; import CrudValeursService from './valeurs/crud-valeurs.service'; @Module({ @@ -25,6 +26,7 @@ import CrudValeursService from './valeurs/crud-valeurs.service'; CrudValeursService, IndicateurFiltreService, IndicateurFiltreRouter, + IndicateurValeursRouter, TrajectoiresDataService, TrajectoiresSpreadsheetService, TrajectoiresXlsxService, @@ -37,6 +39,7 @@ import CrudValeursService from './valeurs/crud-valeurs.service'; TrajectoiresRouter, IndicateurFiltreService, IndicateurFiltreRouter, + IndicateurValeursRouter, ], controllers: [ IndicateursController, diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts b/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts new file mode 100644 index 0000000000..43ff3e1695 --- /dev/null +++ b/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts @@ -0,0 +1,192 @@ +import { INestApplication } from '@nestjs/common'; +import { inferProcedureInput } from '@trpc/server'; +import { and, eq } from 'drizzle-orm'; +import { getTestApp } from '../../../test/app-utils'; +import { getAuthUser } from '../../../test/auth-utils'; +import { AuthenticatedUser } from '../../auth/models/auth.models'; +import { DatabaseService } from '../../utils'; +import { AppRouter, TrpcRouter } from '../../utils/trpc/trpc.router'; +import { indicateurValeurTable } from '../index-domain'; +import { getIndicateursValeursResponseSchema } from '../shared/models/get-indicateurs.response'; + +type InputList = inferProcedureInput< + AppRouter['indicateurs']['valeurs']['list'] +>; + +type InputUpsert = inferProcedureInput< + AppRouter['indicateurs']['valeurs']['upsert'] +>; + +const collectiviteId = 1; +const indicateurId = 1; + +describe("Route de lecture/écriture des valeurs d'indicateurs", () => { + let app: INestApplication; + let router: TrpcRouter; + let yoloDodoUser: AuthenticatedUser; + let databaseService: DatabaseService; + + beforeAll(async () => { + app = await getTestApp(); + router = app.get(TrpcRouter); + yoloDodoUser = await getAuthUser(); + + // reset les données avant de commencer les tests + databaseService = app.get<DatabaseService>(DatabaseService); + await databaseService.db + .delete(indicateurValeurTable) + .where( + and( + eq(indicateurValeurTable.collectiviteId, collectiviteId), + eq(indicateurValeurTable.indicateurId, indicateurId) + ) + ); + }); + + test(`Renvoi des valeurs`, async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + const input: InputList = { + collectiviteId, + identifiantsReferentiel: 'cae_8', + }; + const result = await caller.indicateurs.valeurs.list(input); + expect(result.indicateurs.length).not.toBe(0); + expect(result.indicateurs[0].sources.collectivite.valeurs.length).not.toBe( + 0 + ); + const toCheck = getIndicateursValeursResponseSchema.safeParse(result); + expect(toCheck.success).toBeTruthy; + }); + + test("Permet d'insérer une valeur", async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + // vérifie le nombre de valeurs avant insertion + const inputBefore: InputList = { + collectiviteId, + indicateurIds: [indicateurId], + }; + const resultBefore = await caller.indicateurs.valeurs.list(inputBefore); + expect(resultBefore.indicateurs.length).toBe(0); + + // insère une valeur + const input: InputUpsert = { + valeurs: [ + { + collectiviteId, + indicateurId, + dateValeur: '2021-01-01', + resultat: 42, + }, + ], + }; + const result = await caller.indicateurs.valeurs.upsert(input); + expect(result).not.toBe(null); + + // vérifie le nombre de valeurs après insertion + const resultAfter = await caller.indicateurs.valeurs.list(inputBefore); + expect(resultAfter.indicateurs[0].sources.collectivite.valeurs.length).toBe( + 1 + ); + }); + + test('Permet de mettre à jour une valeur', async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + // insère une valeur + const inputInsert: InputUpsert = { + valeurs: [ + { + collectiviteId, + indicateurId, + dateValeur: '2021-01-01', + resultat: 42, + }, + ], + }; + await caller.indicateurs.valeurs.upsert(inputInsert); + + // vérifie le nombre de valeurs avant mise à jour + const inputBefore: InputList = { + collectiviteId, + indicateurIds: [indicateurId], + }; + const resultBefore = await caller.indicateurs.valeurs.list(inputBefore); + expect( + resultBefore.indicateurs[0].sources.collectivite.valeurs.length + ).toBe(1); + + // met à jour la valeur + const inputUpdate: InputUpsert = { + valeurs: [ + { + collectiviteId, + indicateurId, + dateValeur: '2021-01-01', + resultat: 43, + }, + ], + }; + const result = await caller.indicateurs.valeurs.upsert(inputUpdate); + expect(result).not.toBe(null); + + // vérifie le nombre de valeurs après mise à jour + const resultAfter = await caller.indicateurs.valeurs.list(inputBefore); + expect(resultAfter.indicateurs[0].sources.collectivite.valeurs.length).toBe( + 1 + ); + expect( + resultAfter.indicateurs[0].sources.collectivite.valeurs[0].resultat + ).toBe(43); + + // met à jour la valeur objectif pour la même date + const inputUpdateObjectif: InputUpsert = { + valeurs: [ + { + collectiviteId, + indicateurId, + dateValeur: '2021-01-01', + resultat: 43, // il faut renvoyer aussi le résultat sinon il est effacé + objectif: 44, + }, + ], + }; + const resultObjectif = await caller.indicateurs.valeurs.upsert( + inputUpdateObjectif + ); + expect(resultObjectif).not.toBe(null); + + // vérifie le nombre de valeurs après mise à jour + const resultAfterObjectif = await caller.indicateurs.valeurs.list( + inputBefore + ); + expect( + resultAfterObjectif.indicateurs[0].sources.collectivite.valeurs.length + ).toBe(1); + expect( + resultAfterObjectif.indicateurs[0].sources.collectivite.valeurs[0] + .resultat + ).toBe(43); + expect( + resultAfterObjectif.indicateurs[0].sources.collectivite.valeurs[0] + .objectif + ).toBe(44); + }); + + test("Ne permet pas d'insérer une valeur si on n'a pas le droit requis", async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + const input: InputUpsert = { + valeurs: [ + { + collectiviteId: 100, + indicateurId: 1, + dateValeur: '2021-01-01', + resultat: 42, + }, + ], + }; + await expect(caller.indicateurs.valeurs.upsert(input)).rejects.toThrow(); + }); +}); diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.router.ts b/backend/src/indicateurs/valeurs/crud-valeurs.router.ts new file mode 100644 index 0000000000..b15263dab5 --- /dev/null +++ b/backend/src/indicateurs/valeurs/crud-valeurs.router.ts @@ -0,0 +1,26 @@ +import { TrpcService } from '@/backend/utils/trpc/trpc.service'; +import { Injectable } from '@nestjs/common'; +import { getIndicateursValeursRequestSchema } from '../shared/models/get-indicateurs.request'; +import { upsertIndicateursValeursRequestSchema } from '../shared/models/upsert-indicateurs-valeurs.request'; +import IndicateurValeursService from './crud-valeurs.service'; + +@Injectable() +export class IndicateurValeursRouter { + constructor( + private readonly trpc: TrpcService, + private readonly service: IndicateurValeursService + ) {} + + router = this.trpc.router({ + list: this.trpc.authedProcedure + .input(getIndicateursValeursRequestSchema) + .query(({ ctx, input }) => { + return this.service.getIndicateurValeursGroupees(input, ctx.user); + }), + upsert: this.trpc.authedProcedure + .input(upsertIndicateursValeursRequestSchema) + .mutation(({ input, ctx }) => { + return this.service.upsertIndicateurValeurs(input.valeurs, ctx.user); + }), + }); +} diff --git a/backend/src/utils/trpc/trpc.router.ts b/backend/src/utils/trpc/trpc.router.ts index 2a8e372aa9..6859f96783 100644 --- a/backend/src/utils/trpc/trpc.router.ts +++ b/backend/src/utils/trpc/trpc.router.ts @@ -8,6 +8,7 @@ import { CollectiviteMembresRouter } from '../../collectivites/membres/membres.r import { PersonnesRouter } from '../../collectivites/personnes.router'; import { IndicateurFiltreRouter } from '../../indicateurs/definitions/indicateur-filtre.router'; import { TrajectoiresRouter } from '../../indicateurs/trajectoires/trajectoires.router'; +import { IndicateurValeursRouter } from '../../indicateurs/valeurs/crud-valeurs.router'; import { ComputeScoreRouter } from '../../referentiels/compute-score/compute-score.router'; import { ScoreSnapshotsRouter } from '../../referentiels/snapshots/score-snaphots.router'; import { UpdateActionStatutRouter } from '../../referentiels/update-action-statut/update-action-statut.router'; @@ -27,6 +28,7 @@ export class TrpcRouter { private readonly personnes: PersonnesRouter, private readonly ficheActionEtapeRouter: FicheActionEtapeRouter, private readonly indicateurFiltreRouter: IndicateurFiltreRouter, + private readonly indicateurValeursRouter: IndicateurValeursRouter, private readonly bulkEditRouter: BulkEditRouter, private readonly membresRouter: CollectiviteMembresRouter, private readonly updateActionStatutRouter: UpdateActionStatutRouter, @@ -38,6 +40,7 @@ export class TrpcRouter { indicateurs: { trajectoires: this.trajectoiresRouter.router, list: this.indicateurFiltreRouter.router.list, + valeurs: this.indicateurValeursRouter.router, }, plans: { fiches: this.trpc.mergeRouters( From e56d51ec1eb0bf26f841b9d3414279f6c38a2d4d Mon Sep 17 00:00:00 2001 From: Marc Rutkowski <marc@attractive-media.fr> Date: Wed, 15 Jan 2025 10:38:26 +0100 Subject: [PATCH 2/6] Ajoute une nouvelle fonction d'upsert d'une valeur d'indicateur --- .../shared/models/indicateur-valeur.table.ts | 1 + .../upsert-valeur-indicateur.request.ts | 17 +++++ .../valeurs/crud-valeurs.router.e2e-spec.ts | 73 ++++++++----------- .../valeurs/crud-valeurs.router.ts | 7 +- .../valeurs/crud-valeurs.service.ts | 63 ++++++++++++++++ 5 files changed, 115 insertions(+), 46 deletions(-) create mode 100644 backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts diff --git a/backend/src/indicateurs/shared/models/indicateur-valeur.table.ts b/backend/src/indicateurs/shared/models/indicateur-valeur.table.ts index 79e1c50e51..0cad910e5c 100644 --- a/backend/src/indicateurs/shared/models/indicateur-valeur.table.ts +++ b/backend/src/indicateurs/shared/models/indicateur-valeur.table.ts @@ -112,3 +112,4 @@ export interface IndicateurValeurAvecMetadonnesDefinition { indicateur_source_metadonnee: SourceMetadonnee | null; } + diff --git a/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts b/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts new file mode 100644 index 0000000000..d905979910 --- /dev/null +++ b/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +/** Upsert d'une valeur d'indicateur pour une collectivité */ +export const upsertValeurIndicateurSchema = z.object({ + collectiviteId: z.number(), + indicateurId: z.number(), + id: z.number().optional(), // pour un update + dateValeur: z.string().optional(), // pour un insert + resultat: z.number().nullish(), + resultatCommentaire: z.string().nullish(), + objectif: z.number().nullish(), + objectifCommentaire: z.string().nullish(), +}); + +export type UpsertValeurIndicateur = z.infer< + typeof upsertValeurIndicateurSchema +>; diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts b/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts index 43ff3e1695..164355bcef 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts @@ -30,9 +30,11 @@ describe("Route de lecture/écriture des valeurs d'indicateurs", () => { app = await getTestApp(); router = app.get(TrpcRouter); yoloDodoUser = await getAuthUser(); + databaseService = app.get<DatabaseService>(DatabaseService); + }); + beforeEach(async () => { // reset les données avant de commencer les tests - databaseService = app.get<DatabaseService>(DatabaseService); await databaseService.db .delete(indicateurValeurTable) .where( @@ -72,14 +74,11 @@ describe("Route de lecture/écriture des valeurs d'indicateurs", () => { // insère une valeur const input: InputUpsert = { - valeurs: [ - { - collectiviteId, - indicateurId, - dateValeur: '2021-01-01', - resultat: 42, - }, - ], + collectiviteId, + indicateurId, + dateValeur: '2021-01-01', + resultat: 42, + resultatCommentaire: 'commentaire', }; const result = await caller.indicateurs.valeurs.upsert(input); expect(result).not.toBe(null); @@ -96,14 +95,11 @@ describe("Route de lecture/écriture des valeurs d'indicateurs", () => { // insère une valeur const inputInsert: InputUpsert = { - valeurs: [ - { - collectiviteId, - indicateurId, - dateValeur: '2021-01-01', - resultat: 42, - }, - ], + collectiviteId, + indicateurId, + dateValeur: '2021-01-01', + resultat: 42, + resultatCommentaire: 'commentaire', }; await caller.indicateurs.valeurs.upsert(inputInsert); @@ -117,16 +113,12 @@ describe("Route de lecture/écriture des valeurs d'indicateurs", () => { resultBefore.indicateurs[0].sources.collectivite.valeurs.length ).toBe(1); - // met à jour la valeur + // met à jour que la valeur const inputUpdate: InputUpsert = { - valeurs: [ - { - collectiviteId, - indicateurId, - dateValeur: '2021-01-01', - resultat: 43, - }, - ], + collectiviteId, + indicateurId, + id: resultBefore.indicateurs[0].sources.collectivite.valeurs[0].id, + resultat: 43, }; const result = await caller.indicateurs.valeurs.upsert(inputUpdate); expect(result).not.toBe(null); @@ -139,18 +131,17 @@ describe("Route de lecture/écriture des valeurs d'indicateurs", () => { expect( resultAfter.indicateurs[0].sources.collectivite.valeurs[0].resultat ).toBe(43); + expect( + resultAfter.indicateurs[0].sources.collectivite.valeurs[0] + .resultatCommentaire + ).toBe('commentaire'); // met à jour la valeur objectif pour la même date const inputUpdateObjectif: InputUpsert = { - valeurs: [ - { - collectiviteId, - indicateurId, - dateValeur: '2021-01-01', - resultat: 43, // il faut renvoyer aussi le résultat sinon il est effacé - objectif: 44, - }, - ], + collectiviteId, + indicateurId, + id: resultBefore.indicateurs[0].sources.collectivite.valeurs[0].id, + objectif: 44, }; const resultObjectif = await caller.indicateurs.valeurs.upsert( inputUpdateObjectif @@ -178,14 +169,10 @@ describe("Route de lecture/écriture des valeurs d'indicateurs", () => { const caller = router.createCaller({ user: yoloDodoUser }); const input: InputUpsert = { - valeurs: [ - { - collectiviteId: 100, - indicateurId: 1, - dateValeur: '2021-01-01', - resultat: 42, - }, - ], + collectiviteId: 100, + indicateurId: 1, + dateValeur: '2021-01-01', + resultat: 42, }; await expect(caller.indicateurs.valeurs.upsert(input)).rejects.toThrow(); }); diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.router.ts b/backend/src/indicateurs/valeurs/crud-valeurs.router.ts index b15263dab5..05603d987c 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.router.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.router.ts @@ -1,7 +1,8 @@ import { TrpcService } from '@/backend/utils/trpc/trpc.service'; import { Injectable } from '@nestjs/common'; +import { upsertValeurIndicateurSchema } from '../index-domain'; import { getIndicateursValeursRequestSchema } from '../shared/models/get-indicateurs.request'; -import { upsertIndicateursValeursRequestSchema } from '../shared/models/upsert-indicateurs-valeurs.request'; +import { upsertValeurIndicateurSchema } from '../shared/models/upsert-valeur-indicateur.request'; import IndicateurValeursService from './crud-valeurs.service'; @Injectable() @@ -18,9 +19,9 @@ export class IndicateurValeursRouter { return this.service.getIndicateurValeursGroupees(input, ctx.user); }), upsert: this.trpc.authedProcedure - .input(upsertIndicateursValeursRequestSchema) + .input(upsertValeurIndicateurSchema) .mutation(({ input, ctx }) => { - return this.service.upsertIndicateurValeurs(input.valeurs, ctx.user); + return this.service.upsertValeur(input, ctx.user); }), }); } diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts index df75c0a60a..59fa6ec8ab 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts @@ -43,6 +43,7 @@ import { indicateurValeursGroupeeParSourceSchema, indicateurValeurTable, } from '../shared/models/indicateur-valeur.table'; +import { UpsertValeurIndicateur } from '../shared/models/upsert-valeur-indicateur.request'; export class IndicateurValeurGroupee extends createZodDto( extendApi(indicateurValeurGroupeeSchema) @@ -305,6 +306,68 @@ export default class CrudValeursService { return definitions; } + async upsertValeur( + data: UpsertValeurIndicateur, + tokenInfo: AuthenticatedUser + ) { + const { collectiviteId } = data; + await this.permissionService.isAllowed( + tokenInfo, + PermissionOperation.INDICATEURS_EDITION, + ResourceType.COLLECTIVITE, + collectiviteId + ); + + if (tokenInfo.role === AuthRole.AUTHENTICATED && tokenInfo.id) { + const now = new Date().toISOString(); + if (data.id !== undefined) { + this.logger.log( + `Mise à jour de la valeur id ${data.id} pour la collectivité ${data.collectiviteId}` + ); + const updated = await this.databaseService.db + .update(indicateurValeurTable) + .set({ + resultat: data.resultat, + resultatCommentaire: data.resultatCommentaire, + objectif: data.objectif, + objectifCommentaire: data.objectifCommentaire, + modifiedBy: tokenInfo.id, + modifiedAt: now, + }) + .where( + and( + eq(indicateurValeurTable.collectiviteId, collectiviteId), + eq(indicateurValeurTable.id, data.id) + ) + ) + .returning(); + return updated[0]; + } + + if (data.dateValeur !== undefined) { + this.logger.log( + `Insertion de la valeur de l'indicateur ${data.indicateurId} pour la collectivité ${data.collectiviteId}` + ); + const inserted = await this.databaseService.db + .insert(indicateurValeurTable) + .values({ + collectiviteId, + indicateurId: data.indicateurId, + dateValeur: data.dateValeur, + resultat: data.resultat, + resultatCommentaire: data.resultatCommentaire, + objectif: data.objectif, + objectifCommentaire: data.objectifCommentaire, + createdBy: tokenInfo.id, + createdAt: now, + modifiedBy: tokenInfo.id, + }) + .returning(); + return inserted[0]; + } + } + } + async upsertIndicateurValeurs( indicateurValeurs: IndicateurValeurInsert[], tokenInfo: AuthenticatedUser | undefined From 0a62215cd146619bdd2550e0e21a9b32e35a8aba Mon Sep 17 00:00:00 2001 From: Marc Rutkowski <marc@attractive-media.fr> Date: Thu, 16 Jan 2025 09:22:09 +0100 Subject: [PATCH 3/6] Ajoute une fonction de suppression d'une valeur d'indicateur --- .../delete-valeur-indicateur.request.ts | 12 ++++++ .../valeurs/crud-valeurs.router.e2e-spec.ts | 37 +++++++++++++++++++ .../valeurs/crud-valeurs.router.ts | 7 +++- .../valeurs/crud-valeurs.service.ts | 26 +++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 backend/src/indicateurs/shared/models/delete-valeur-indicateur.request.ts diff --git a/backend/src/indicateurs/shared/models/delete-valeur-indicateur.request.ts b/backend/src/indicateurs/shared/models/delete-valeur-indicateur.request.ts new file mode 100644 index 0000000000..d622a25008 --- /dev/null +++ b/backend/src/indicateurs/shared/models/delete-valeur-indicateur.request.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +/** Suppression d'une valeur d'indicateur pour une collectivité */ +export const deleteValeurIndicateurSchema = z.object({ + collectiviteId: z.number(), + indicateurId: z.number(), + id: z.number(), +}); + +export type DeleteValeurIndicateur = z.infer< + typeof deleteValeurIndicateurSchema +>; diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts b/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts index 164355bcef..a24b80884c 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.router.e2e-spec.ts @@ -176,4 +176,41 @@ describe("Route de lecture/écriture des valeurs d'indicateurs", () => { }; await expect(caller.indicateurs.valeurs.upsert(input)).rejects.toThrow(); }); + + test('Permet de supprimer une valeur', async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + // insère une valeur + const inputInsert: InputUpsert = { + collectiviteId, + indicateurId, + dateValeur: '2021-01-01', + resultat: 42, + resultatCommentaire: 'commentaire', + }; + await caller.indicateurs.valeurs.upsert(inputInsert); + + // vérifie le nombre de valeurs avant la suppression + const inputBefore: InputList = { + collectiviteId, + indicateurIds: [indicateurId], + }; + const resultBefore = await caller.indicateurs.valeurs.list(inputBefore); + expect( + resultBefore.indicateurs[0].sources.collectivite.valeurs.length + ).toBe(1); + + // supprime l'entrée + const valeurId = + resultBefore.indicateurs[0].sources.collectivite.valeurs[0].id; + await caller.indicateurs.valeurs.delete({ + collectiviteId, + indicateurId, + id: valeurId, + }); + + // vérifie le nombre de valeurs après la suppression + const resultAfter = await caller.indicateurs.valeurs.list(inputBefore); + expect(resultAfter.indicateurs.length).toBe(0); + }); }); diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.router.ts b/backend/src/indicateurs/valeurs/crud-valeurs.router.ts index 05603d987c..ce6deecd90 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.router.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.router.ts @@ -1,6 +1,6 @@ import { TrpcService } from '@/backend/utils/trpc/trpc.service'; import { Injectable } from '@nestjs/common'; -import { upsertValeurIndicateurSchema } from '../index-domain'; +import { deleteValeurIndicateurSchema } from '../shared/models/delete-valeur-indicateur.request'; import { getIndicateursValeursRequestSchema } from '../shared/models/get-indicateurs.request'; import { upsertValeurIndicateurSchema } from '../shared/models/upsert-valeur-indicateur.request'; import IndicateurValeursService from './crud-valeurs.service'; @@ -23,5 +23,10 @@ export class IndicateurValeursRouter { .mutation(({ input, ctx }) => { return this.service.upsertValeur(input, ctx.user); }), + delete: this.trpc.authedProcedure + .input(deleteValeurIndicateurSchema) + .mutation(({ input, ctx }) => { + return this.service.deleteValeurIndicateur(input, ctx.user); + }), }); } diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts index 59fa6ec8ab..8ca66bd504 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts @@ -22,6 +22,7 @@ import * as _ from 'lodash'; import { AuthenticatedUser, AuthRole } from '../../auth/models/auth.models'; import { DatabaseService } from '../../utils/database/database.service'; import { DeleteIndicateursValeursRequestType } from '../shared/models/delete-indicateurs.request'; +import { DeleteValeurIndicateur } from '../shared/models/delete-valeur-indicateur.request'; import { GetIndicateursValeursRequestType } from '../shared/models/get-indicateurs.request'; import { GetIndicateursValeursResponseType } from '../shared/models/get-indicateurs.response'; import { @@ -368,6 +369,31 @@ export default class CrudValeursService { } } + async deleteValeurIndicateur( + data: DeleteValeurIndicateur, + tokenInfo: AuthenticatedUser + ) { + const { collectiviteId, indicateurId, id } = data; + await this.permissionService.isAllowed( + tokenInfo, + PermissionOperation.INDICATEURS_EDITION, + ResourceType.COLLECTIVITE, + collectiviteId + ); + + if (tokenInfo.role === AuthRole.AUTHENTICATED && tokenInfo.id) { + await this.databaseService.db + .delete(indicateurValeurTable) + .where( + and( + eq(indicateurValeurTable.collectiviteId, collectiviteId), + eq(indicateurValeurTable.indicateurId, indicateurId), + eq(indicateurValeurTable.id, id) + ) + ); + } + } + async upsertIndicateurValeurs( indicateurValeurs: IndicateurValeurInsert[], tokenInfo: AuthenticatedUser | undefined From ba051e770b5bba93e79faee87b7d6754c3d150a2 Mon Sep 17 00:00:00 2001 From: Marc Rutkowski <marc@attractive-media.fr> Date: Tue, 21 Jan 2025 18:56:40 +0100 Subject: [PATCH 4/6] =?UTF-8?q?D=C3=A9rive=20le=20type=20plut=C3=B4t=20que?= =?UTF-8?q?=20de=20red=C3=A9finir=20le=20typage=20de=20chaque=20champ=20vo?= =?UTF-8?q?ulu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upsert-valeur-indicateur.request.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts b/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts index d905979910..671ed9c824 100644 --- a/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts +++ b/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts @@ -1,16 +1,23 @@ import { z } from 'zod'; +import { indicateurValeurSchemaInsert } from './indicateur-valeur.table'; /** Upsert d'une valeur d'indicateur pour une collectivité */ -export const upsertValeurIndicateurSchema = z.object({ - collectiviteId: z.number(), - indicateurId: z.number(), - id: z.number().optional(), // pour un update - dateValeur: z.string().optional(), // pour un insert - resultat: z.number().nullish(), - resultatCommentaire: z.string().nullish(), - objectif: z.number().nullish(), - objectifCommentaire: z.string().nullish(), -}); +const shape = indicateurValeurSchemaInsert.shape; +const upsertValeurIndicateurSchema = indicateurValeurSchemaInsert + .pick({ + collectiviteId: true, + indicateurId: true, + }) + .merge( + z.object({ + id: shape.id.optional(), // pour un update + dateValeur: shape.dateValeur.optional(), // pour un insert + resultat: shape.resultat.nullish(), + resultatCommentaire: shape.resultatCommentaire.nullish(), + objectif: shape.objectif.nullish(), + objectifCommentaire: shape.objectifCommentaire.nullish(), + }) + ); export type UpsertValeurIndicateur = z.infer< typeof upsertValeurIndicateurSchema From f0ccfecc397efe6298886bd68108c5fa5673b9ae Mon Sep 17 00:00:00 2001 From: Marc Rutkowski <marc@attractive-media.fr> Date: Wed, 22 Jan 2025 16:41:25 +0100 Subject: [PATCH 5/6] Simplifie le typage --- .../upsert-valeur-indicateur.request.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts b/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts index 671ed9c824..f1d57211e1 100644 --- a/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts +++ b/backend/src/indicateurs/shared/models/upsert-valeur-indicateur.request.ts @@ -2,22 +2,19 @@ import { z } from 'zod'; import { indicateurValeurSchemaInsert } from './indicateur-valeur.table'; /** Upsert d'une valeur d'indicateur pour une collectivité */ -const shape = indicateurValeurSchemaInsert.shape; -const upsertValeurIndicateurSchema = indicateurValeurSchemaInsert +export const upsertValeurIndicateurSchema = indicateurValeurSchemaInsert .pick({ collectiviteId: true, indicateurId: true, + id: true, + resultat: true, + resultatCommentaire: true, + objectif: true, + objectifCommentaire: true, }) - .merge( - z.object({ - id: shape.id.optional(), // pour un update - dateValeur: shape.dateValeur.optional(), // pour un insert - resultat: shape.resultat.nullish(), - resultatCommentaire: shape.resultatCommentaire.nullish(), - objectif: shape.objectif.nullish(), - objectifCommentaire: shape.objectifCommentaire.nullish(), - }) - ); + .extend({ + dateValeur: indicateurValeurSchemaInsert.shape.dateValeur.optional(), + }); export type UpsertValeurIndicateur = z.infer< typeof upsertValeurIndicateurSchema From 2ce3874b7694d536ad281e4a39bb2741a1379399 Mon Sep 17 00:00:00 2001 From: Marc Rutkowski <marc@attractive-media.fr> Date: Wed, 22 Jan 2025 16:41:35 +0100 Subject: [PATCH 6/6] Ajoute un commentaire --- backend/src/indicateurs/valeurs/crud-valeurs.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts index 8ca66bd504..7fba66452b 100644 --- a/backend/src/indicateurs/valeurs/crud-valeurs.service.ts +++ b/backend/src/indicateurs/valeurs/crud-valeurs.service.ts @@ -307,6 +307,12 @@ export default class CrudValeursService { return definitions; } + /** + * Variante de `upsertIndicateurValeurs` qui permet de ne pas être obligé de + * redonner l'objet complet sans pour autant écraser la valeur existante. Et + * donc de mettre à jour la colonne resultat indépendamment de la valeur + * objectif (et pareil pour les commentaires). + */ async upsertValeur( data: UpsertValeurIndicateur, tokenInfo: AuthenticatedUser