diff --git a/.github/workflows/cd-backend.yml b/.github/workflows/cd-backend.yml index bbf40aaf39..59c0666ef7 100644 --- a/.github/workflows/cd-backend.yml +++ b/.github/workflows/cd-backend.yml @@ -69,3 +69,4 @@ jobs: +backend-deploy --ENV_NAME=${{ github.event.inputs.target }} --KOYEB_API_KEY=${{ secrets.KOYEB_API_KEY }} + --TRAJECTOIRE_SNBC_SHEET_ID=${{ vars.TRAJECTOIRE_SNBC_SHEET_ID }} diff --git a/Earthfile b/Earthfile index 76ae544964..94124dee09 100644 --- a/Earthfile +++ b/Earthfile @@ -324,6 +324,7 @@ backend-build: backend-deploy: ## Déploie le backend dans une app Koyeb existante ARG --required KOYEB_API_KEY + ARG --required TRAJECTOIRE_SNBC_SHEET_ID FROM +koyeb RUN ./koyeb services update $ENV_NAME-backend/backend \ --docker $BACKEND_IMG_NAME \ @@ -331,7 +332,8 @@ backend-deploy: ## Déploie le backend dans une app Koyeb existante --env GCLOUD_SERVICE_ACCOUNT_KEY=@GCLOUD_SERVICE_ACCOUNT_KEY \ --env DATABASE_URL=@SUPABASE_DATABASE_URL_$ENV_NAME \ --env SUPABASE_URL=@SUPABASE_URL_$ENV_NAME \ - --env SUPABASE_SERVICE_ROLE_KEY=@SUPABASE_SERVICE_ROLE_KEY_$ENV_NAME + --env SUPABASE_SERVICE_ROLE_KEY=@SUPABASE_SERVICE_ROLE_KEY_$ENV_NAME \ + --env TRAJECTOIRE_SNBC_SHEET_ID=$TRAJECTOIRE_SNBC_SHEET_ID app-build: ## construit l'image de l'app ARG PLATFORM diff --git a/backend/package.json b/backend/package.json index 087aa6d389..e74222008a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,12 +21,16 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.4.0", "@sentry/nestjs": "^8.19.0", "@sentry/profiling-node": "^8.19.0", "@supabase/supabase-js": "^2.40.0", "async-retry": "^1.3.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "drizzle-orm": "^0.32.0", "gaxios": "^6.7.0", "google-auth-library": "^9.11.0", diff --git a/backend/src/app.controller.spec.ts b/backend/src/app.controller.spec.ts deleted file mode 100644 index d22f3890a3..0000000000 --- a/backend/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts deleted file mode 100644 index 1c4b43c905..0000000000 --- a/backend/src/app.controller.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Controller, Get, NotFoundException } from '@nestjs/common'; -import { AppService } from './app.service'; -import SheetService from './spreadsheets/services/sheet.service'; -import CollectivitesService from './collectivites/services/collectivites.service'; - -@Controller() -export class AppController { - constructor( - private readonly appService: AppService, - private readonly sheetService: SheetService, - private readonly collectiviteService: CollectivitesService, - ) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } - - @Get('exception') - getException(): string { - throw new NotFoundException('User not found'); - } - - @Get('test') - getTestResult() { - return this.sheetService.testDownload(); - } - - @Get('collectivite') - getCollectivite() { - return this.collectiviteService.getEpciBySiren('200043495'); - } -} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 097171d421..290db0452c 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,13 +1,21 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { SheetModule } from './spreadsheets/sheet.module'; -import { CommonModule } from './common/common.module'; +import { ConfigModule } from '@nestjs/config'; import { CollectivitesModule } from './collectivites/collectivites.module'; +import { CommonModule } from './common/common.module'; +import { IndicateursModule } from './indicateurs/indicateurs.module'; +import { SheetModule } from './spreadsheets/sheet.module'; @Module({ - imports: [CommonModule, SheetModule, CollectivitesModule], - controllers: [AppController], - providers: [AppService], + imports: [ + ConfigModule.forRoot({ + ignoreEnvFile: process.env.NODE_ENV === 'production', // In production, environment variables are set by the deployment + }), + CommonModule, + SheetModule, + CollectivitesModule, + IndicateursModule, + ], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts deleted file mode 100644 index 927d7cca0b..0000000000 --- a/backend/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/backend/src/collectivites/services/collectivites.service.ts b/backend/src/collectivites/services/collectivites.service.ts index a16b40c74d..2830aaf6b5 100644 --- a/backend/src/collectivites/services/collectivites.service.ts +++ b/backend/src/collectivites/services/collectivites.service.ts @@ -1,16 +1,18 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { eq } from 'drizzle-orm'; import { boolean, integer, pgTable, serial, text } from 'drizzle-orm/pg-core'; import DatabaseService from '../../common/services/database.service'; @Injectable() export default class CollectivitesService { - private readonly collectiviteTable = pgTable('collectivite', { + private readonly logger = new Logger(CollectivitesService.name); + + public readonly collectiviteTable = pgTable('collectivite', { id: serial('id').primaryKey(), access_restreint: boolean('access_restreint'), }); - private readonly epciTable = pgTable('epci', { + public readonly epciTable = pgTable('epci', { id: serial('id').primaryKey(), collectivite_id: integer('collectivite_id') .notNull() @@ -21,17 +23,34 @@ export default class CollectivitesService { constructor(private readonly databaseService: DatabaseService) {} - async getEpciById(id: number) { - return this.databaseService.db + async getEpciByCollectiviteId(collectiviteId: number) { + this.logger.log( + `Récupération de l'epci avec l'identifiant ${collectiviteId}`, + ); + const epciByIdResult = await this.databaseService.db .select() .from(this.epciTable) - .where(eq(this.epciTable.id, id)); + .where(eq(this.epciTable.collectivite_id, collectiviteId)); + if (!epciByIdResult?.length) { + throw new NotFoundException( + `EPCI avec l'identifiant de collectivite ${collectiviteId} introuvable`, + ); + } + + this.logger.log(`Epci trouvé avec l'id ${epciByIdResult[0].id}`); + return epciByIdResult[0]; } async getEpciBySiren(siren: string) { - return this.databaseService.db + this.logger.log(`Récupération de l'epci à partir du siren ${siren}`); + const epciBySirenResult = await this.databaseService.db .select() .from(this.epciTable) .where(eq(this.epciTable.siren, siren)); + if (!epciBySirenResult?.length) { + throw new NotFoundException(`EPCI avec le siren ${siren} introuvable`); + } + this.logger.log(`Epci trouvé avec l'id ${epciBySirenResult[0].id}`); + return epciBySirenResult[0]; } } diff --git a/backend/src/common/services/optionalBooleanMapper.ts b/backend/src/common/services/optionalBooleanMapper.ts new file mode 100644 index 0000000000..c70023fb91 --- /dev/null +++ b/backend/src/common/services/optionalBooleanMapper.ts @@ -0,0 +1,7 @@ +const optionalBooleanMapper = new Map([ + ['undefined', undefined], + ['true', true], + ['false', false], +]); + +export default optionalBooleanMapper; diff --git a/backend/src/indicateurs/controllers/trajectoires.controller.ts b/backend/src/indicateurs/controllers/trajectoires.controller.ts new file mode 100644 index 0000000000..68832ba0cc --- /dev/null +++ b/backend/src/indicateurs/controllers/trajectoires.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get, Logger, Query } from '@nestjs/common'; +import CalculTrajectoireRequest from '../models/calcultrajectoire.request'; +import TrajectoiresService from '../service/trajectoires.service'; + +@Controller('trajectoires') +export class TrajectoiresController { + private readonly logger = new Logger(TrajectoiresController.name); + + constructor(private readonly trajectoiresService: TrajectoiresService) {} + + @Get('snbc') // TODO: plutôt un post + calculeTrajectoireSnbc(@Query() request: CalculTrajectoireRequest) { + this.logger.log( + `Calcul de la trajectoire SNBC pour la collectivité ${request.collectivite_id}`, + ); + return this.trajectoiresService.calculeTrajectoireSnbc(request); + } +} diff --git a/backend/src/indicateurs/indicateurs.module.ts b/backend/src/indicateurs/indicateurs.module.ts new file mode 100644 index 0000000000..2dfd339e5f --- /dev/null +++ b/backend/src/indicateurs/indicateurs.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { CollectivitesModule } from '../collectivites/collectivites.module'; +import { CommonModule } from '../common/common.module'; +import { SheetModule } from '../spreadsheets/sheet.module'; +import { TrajectoiresController } from './controllers/trajectoires.controller'; +import IndicateursService from './service/indicateurs.service'; +import TrajectoiresService from './service/trajectoires.service'; + +@Module({ + imports: [CommonModule, CollectivitesModule, SheetModule], + providers: [IndicateursService, TrajectoiresService], + exports: [IndicateursService, TrajectoiresService], + controllers: [TrajectoiresController], +}) +export class IndicateursModule {} diff --git a/backend/src/indicateurs/models/calcultrajectoire.request.ts b/backend/src/indicateurs/models/calcultrajectoire.request.ts new file mode 100644 index 0000000000..29ed5c8c20 --- /dev/null +++ b/backend/src/indicateurs/models/calcultrajectoire.request.ts @@ -0,0 +1,14 @@ +import { Transform, Type } from 'class-transformer'; +import { IsBoolean, IsInt, IsOptional } from 'class-validator'; +import optionalBooleanMapper from '../../common/services/optionalBooleanMapper'; + +export default class CalculTrajectoireRequest { + @IsInt() + @Type(() => Number) + collectivite_id: number; + + @IsBoolean() + @Transform(({ value }) => optionalBooleanMapper.get(value)) // Useful for query param + @IsOptional() + conserve_fichier_temporaire?: boolean; +} diff --git a/backend/src/indicateurs/service/indicateurs.service.ts b/backend/src/indicateurs/service/indicateurs.service.ts new file mode 100644 index 0000000000..3a1306fc84 --- /dev/null +++ b/backend/src/indicateurs/service/indicateurs.service.ts @@ -0,0 +1,125 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { and, eq, gte, inArray, lte, sql, SQLWrapper } from 'drizzle-orm'; +import { + boolean, + date, + doublePrecision, + integer, + pgTable, + serial, + text, + timestamp, + uuid, +} from 'drizzle-orm/pg-core'; +import CollectivitesService from '../../collectivites/services/collectivites.service'; +import DatabaseService from '../../common/services/database.service'; + +@Injectable() +export default class IndicateursService { + private readonly logger = new Logger(IndicateursService.name); + public readonly indicateurDefinitionTable; + public readonly indicateurValeurTable; + + constructor( + private readonly databaseService: DatabaseService, + private readonly collectivitesService: CollectivitesService, + ) { + this.indicateurDefinitionTable = pgTable('indicateur_definition', { + id: serial('id').primaryKey(), + groupement_id: integer('groupement_id'), // TODO: references + collectivite_id: integer('collectivite_id') + .notNull() + .references(() => this.collectivitesService.collectiviteTable.id, { + onDelete: 'cascade', + }), + identifiant_referentiel: text('identifiant_referentiel').unique(), + titre: text('titre').notNull(), + titre_long: text('titre_long'), + description: text('description'), + unite: text('unite').notNull(), + borne_min: doublePrecision('borne_min'), + borne_max: doublePrecision('borne_max'), + participation_score: boolean('participation_score') + .default(false) + .notNull(), + sans_valeur_utilisateur: boolean('sans_valeur_utilisateur') + .default(false) + .notNull(), + valeur_calcule: text('valeur_calcule'), + modified_at: timestamp('modified_at', { withTimezone: true }) + .default(sql.raw(`CURRENT_TIMESTAMP`)) + .notNull(), // with time zone default CURRENT_TIMESTAMP + created_at: timestamp('created_at', { withTimezone: true }) + .default(sql.raw(`CURRENT_TIMESTAMP`)) + .notNull(), // with time zone default CURRENT_TIMESTAMP + modified_by: uuid('modified_by'), // TODO: default auth.uid() references auth.users + created_by: uuid('created_by'), // TODO: default auth.uid() references auth.users + }); + + this.indicateurValeurTable = pgTable('indicateur_valeur', { + id: serial('id').primaryKey(), + collectivite_id: integer('collectivite_id') + .notNull() + .references(() => this.collectivitesService.collectiviteTable.id, { + onDelete: 'cascade', + }), + indicateur_id: integer('indicateur_id') + .notNull() + .references(() => this.indicateurDefinitionTable.id, { + onDelete: 'cascade', + }), + date_valeur: date('date_valeur').notNull(), + metadonnee_id: integer('metadonnee_id'), // TODO: references + resultat: doublePrecision('resultat'), + resultat_commentaire: text('resultat_commentaire'), + objectif: doublePrecision('objectif'), + objectif_commentaire: text('objectif_commentaire'), + estimation: doublePrecision('estimation'), + modified_at: timestamp('modified_at', { withTimezone: true }) + .default(sql.raw(`CURRENT_TIMESTAMP`)) + .notNull(), // with time zone default CURRENT_TIMESTAMP + created_at: timestamp('created_at', { withTimezone: true }) + .default(sql.raw(`CURRENT_TIMESTAMP`)) + .notNull(), // with time zone default CURRENT_TIMESTAMP + modified_by: uuid('modified_by'), // TODO: default auth.uid() references auth.users + created_by: uuid('created_by'), // TODO: default auth.uid() references auth.users + }); + } + + async getReferentielIndicateursValeurs( + collectiviteId: number, + identifiantsReferentiel: string[], + dateDebut?: string, + dateFin?: string, + ) { + this.logger.log( + `Récupération des valeurs des indicateurs ${identifiantsReferentiel.join(',')} pour la collectivite ${collectiviteId} et la plage de date ${dateDebut} - ${dateFin}`, + ); + const conditions: SQLWrapper[] = [ + eq(this.indicateurValeurTable.collectivite_id, collectiviteId), + inArray( + this.indicateurDefinitionTable.identifiant_referentiel, + identifiantsReferentiel, + ), + ]; + + if (dateDebut) { + conditions.push(gte(this.indicateurValeurTable.date_valeur, dateDebut)); + } + if (dateFin) { + conditions.push(lte(this.indicateurValeurTable.date_valeur, dateFin)); + } + + return this.databaseService.db + .select() + .from(this.indicateurValeurTable) + .leftJoin( + this.indicateurDefinitionTable, + eq( + this.indicateurValeurTable.indicateur_id, + this.indicateurDefinitionTable.id, + ), + ) + .where(and(...conditions)); + } +} diff --git a/backend/src/indicateurs/service/trajectoires.service.ts b/backend/src/indicateurs/service/trajectoires.service.ts new file mode 100644 index 0000000000..f6339f2c53 --- /dev/null +++ b/backend/src/indicateurs/service/trajectoires.service.ts @@ -0,0 +1,159 @@ +import { + Injectable, + InternalServerErrorException, + Logger, + UnprocessableEntityException, +} from '@nestjs/common'; +import CollectivitesService from '../../collectivites/services/collectivites.service'; +import SheetService from '../../spreadsheets/services/sheet.service'; +import CalculTrajectoireRequest from '../models/calcultrajectoire.request'; +import IndicateursService from './indicateurs.service'; + +@Injectable() +export default class TrajectoiresService { + private readonly logger = new Logger(TrajectoiresService.name); + + private readonly SNBC_DATE_REFENCE = '2015-01-01'; + private readonly SNBC_SIREN_CELLULE = 'Caract_territoire!F6'; + private readonly SNBC_EMISSIONS_GES_IDENTIFIANTS_REFERENTIEL = [ + 'cae_1.c', // B6 + 'cae_1.d', // B7 + 'cae_1.i', // B8 + 'cae_1.g', // B9 + 'cae_1.e', // B10 + 'cae_1.f', // B11 + 'cae_1.h', // B12 + 'cae_1.j', // B13 + ]; + private readonly SNBC_EMISSIONS_GES_CELLULES = 'Carto_en-GES!B6:B13'; + private readonly SNBC_TRAJECTOIRE_RESULTAT_CELLULES = + 'TOUS SECTEURS!G253:AP262'; + private readonly SNBC_TRAJECTOIRE_RESULTAT_IDENTIFIANRS_REFERENTIEL = [ + 'cae_1.c', // 253 + 'cae_1.d', // 254 + 'cae_1.i', // 255 + 'cae_1.g', // 256 + 'cae_1.e', // 257 + 'cae_1.h', // 258 + 'cae_1.j', // 259 + '', // 260 + '', // 261 + 'cae_1.a', // 262 + ]; + + constructor( + private readonly collectivitesService: CollectivitesService, + private readonly indicateursService: IndicateursService, + private readonly sheetService: SheetService, + ) {} + + async calculeTrajectoireSnbc(request: CalculTrajectoireRequest) { + // Récupère l'EPCI associé pour obtenir son SIREN + const epci = await this.collectivitesService.getEpciByCollectiviteId( + request.collectivite_id, + ); + + // Récupère les valeurs des indicateurs pour l'année 2015 + const indicateurValeurs = + await this.indicateursService.getReferentielIndicateursValeurs( + request.collectivite_id, + this.SNBC_EMISSIONS_GES_IDENTIFIANTS_REFERENTIEL, + this.SNBC_DATE_REFENCE, + this.SNBC_DATE_REFENCE, + ); + + // Vérifie que toutes les données sont dispo et construit le tableau de valeurs à insérer dans le fichier Spreadsheet + const valeursARemplir: number[] = []; + const identifiantsReferentielManquants: string[] = []; + this.SNBC_EMISSIONS_GES_IDENTIFIANTS_REFERENTIEL.forEach((identifiant) => { + const indicateurValeur = indicateurValeurs.find( + (indicateurValeur) => + indicateurValeur.indicateur_definition?.identifiant_referentiel === + identifiant, + ); + if ( + indicateurValeur && + indicateurValeur.indicateur_valeur.resultat !== null && + indicateurValeur.indicateur_valeur.resultat !== undefined // 0 est une valeur valide + ) { + valeursARemplir.push( + indicateurValeur.indicateur_valeur.resultat / 1000, + ); + } else { + identifiantsReferentielManquants.push(identifiant); + valeursARemplir.push(0); + } + }); + + if (identifiantsReferentielManquants.length > 0) { + throw new UnprocessableEntityException( + `Les indicateurs suivants n'ont pas de valeur pour l'année 2015 : ${identifiantsReferentielManquants.join(', ')}, impossible de calculer la trajectoire SNBC.`, + ); + } + + if (!process.env.TRAJECTOIRE_SNBC_SHEET_ID) { + throw new InternalServerErrorException( + "L'identifiant de la feuille de calcul pour les trajectoires SNBC est manquante", + ); + } + + const nomFichier = `Trajectoire SNBC - ${epci.siren} - ${epci.nom}`; + // TODO: récupérer si le fichier existe déjà et le réutiliser sauf si on force ? + const trajectoireCalculSheetId = await this.sheetService.copyFile( + process.env.TRAJECTOIRE_SNBC_SHEET_ID, + nomFichier, + ); + this.logger.log( + `Fichier de trajectoire SNBC créé à partir du master ${process.env.TRAJECTOIRE_SNBC_SHEET_ID} avec l'identifiant ${trajectoireCalculSheetId}`, + ); + + // Ecriture du SIREN + const sirenNulber = parseInt(epci.siren || ''); + if (isNaN(sirenNulber)) { + throw new InternalServerErrorException( + `Le SIREN de l'EPCI ${epci.nom} (${epci.siren}) n'est pas un nombre`, + ); + } + + await this.sheetService.overwriteRawDataToSheet( + trajectoireCalculSheetId, + this.SNBC_SIREN_CELLULE, + [[sirenNulber]], + ); + + // Ecriture des informations d'émission + await this.sheetService.overwriteRawDataToSheet( + trajectoireCalculSheetId, + this.SNBC_EMISSIONS_GES_CELLULES, + valeursARemplir.map((valeur) => [valeur]), + ); + + const trajectoireCalculResultat = + await this.sheetService.getRawDataFromSheet( + trajectoireCalculSheetId, + this.SNBC_TRAJECTOIRE_RESULTAT_CELLULES, + ); + + if (!request.conserve_fichier_temporaire) { + await this.sheetService.deleteFile(trajectoireCalculSheetId); + } + + // TODO: type it + const indicateurValeursResultat: any[] = []; + trajectoireCalculResultat.data?.forEach((ligne, index) => { + const identifiantReferentiel = + this.SNBC_TRAJECTOIRE_RESULTAT_IDENTIFIANRS_REFERENTIEL[index]; + if (identifiantReferentiel) { + ligne.forEach((valeur, index) => { + indicateurValeursResultat.push({ + identifiant_referentiel: identifiantReferentiel, + date_valeur: `${2015 + index}-01-01`, + resultat: valeur, + }); + }); + } + }); + + return indicateurValeursResultat; + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts index 9e5ce078b7..dc835ccf6d 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,5 +1,5 @@ // WARNING: Do this import first -import { Logger } from '@nestjs/common'; +import { Logger, ValidationPipe } from '@nestjs/common'; import { BaseExceptionFilter, HttpAdapterHost, @@ -29,6 +29,13 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const { httpAdapter } = app.get(HttpAdapterHost); + // TODO: configure validation + app.useGlobalPipes( + new ValidationPipe({ + forbidUnknownValues: true, + }), + ); + // Seulement une v1 pour l'instant app.setGlobalPrefix('api/v1', { exclude: ['version'], diff --git a/backend/src/spreadsheets/services/sheet.service.ts b/backend/src/spreadsheets/services/sheet.service.ts index 4c9ab6a3a7..6addf4d6e6 100644 --- a/backend/src/spreadsheets/services/sheet.service.ts +++ b/backend/src/spreadsheets/services/sheet.service.ts @@ -1,10 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; -import * as auth from 'google-auth-library'; -import { google, sheets_v4, drive_v3 } from 'googleapis'; import * as retry from 'async-retry'; import * as gaxios from 'gaxios'; -import SheetValueRenderOption from '../models/SheetValueRenderOption'; +import * as auth from 'google-auth-library'; +import { drive_v3, google, sheets_v4 } from 'googleapis'; import SheetValueInputOption from '../models/SheetValueInputOption'; +import SheetValueRenderOption from '../models/SheetValueRenderOption'; const sheets = google.sheets({ version: 'v4' }); const drive = google.drive({ version: 'v3' }); @@ -45,6 +45,10 @@ export default class SheetService { async copyFile(fileId: string, copyTitle: string): Promise { const authClient = await this.getAuthClient(); + this.logger.log( + `Copie du Spreadsheet ${fileId} vers un nouveau fichier avec le nom ${copyTitle}`, + ); + const copyOptions: drive_v3.Params$Resource$Files$Copy = { auth: authClient, fileId: fileId, @@ -56,6 +60,16 @@ export default class SheetService { return copyResponse.data.id!; } + async deleteFile(fileId: string) { + const authClient = await this.getAuthClient(); + const deleteOptions: drive_v3.Params$Resource$Files$Delete = { + auth: authClient, + fileId: fileId, + }; + await drive.files.delete(deleteOptions); + this.logger.log(`Spreadsheet ${fileId} correctement supprimé.`); + } + async getRawDataFromSheet( spreadsheetId: string, range: string, @@ -110,7 +124,7 @@ export default class SheetService { await retry(async (bail, num): Promise => { try { this.logger.log( - `Overwrite data to sheet ${spreadsheetId} (attempt ${num})`, + `Overwrite data to sheet ${spreadsheetId} in range ${range} (attempt ${num})`, ); await sheets.spreadsheets.values.update({ auth: authClient, @@ -138,29 +152,4 @@ export default class SheetService { } }, this.RETRY_STRATEGY); } - - async testDownload() { - console.log('Copying file...'); - const fileCopyId = await this.copyFile( - '1_l9NkgNIefC4BZLMhZ20GzrG3xlH2kxuH2vFzCgDFw8', - 'Trajectoire SNBC Territorialisée - Compute', - ); - - console.log('File copied with id:', fileCopyId); - - const epciCode = 200043495; - // Write the epci code - await this.overwriteRawDataToSheet(fileCopyId, 'Caract_territoire!F6', [ - [epciCode], - ]); - - // Getting computed data from spreadsheet - const data = await this.getRawDataFromSheet( - fileCopyId, - 'Caract_territoire!F1239', - SheetValueRenderOption.UNFORMATTED_VALUE, - ); - - return data; - } } diff --git a/package-lock.json b/package-lock.json index c80f6c959f..f79a00f639 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1908,12 +1908,16 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.4.0", "@sentry/nestjs": "^8.19.0", "@sentry/profiling-node": "^8.19.0", "@supabase/supabase-js": "^2.40.0", "async-retry": "^1.3.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "drizzle-orm": "^0.32.0", "gaxios": "^6.7.0", "google-auth-library": "^9.11.0", @@ -6765,6 +6769,11 @@ "react": ">=16" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==" + }, "node_modules/@mui/base": { "version": "5.0.0-beta.17", "license": "MIT", @@ -7353,6 +7362,20 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", + "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", @@ -7395,6 +7418,25 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", @@ -7431,6 +7473,59 @@ "typescript": ">=4.8.2" } }, + "node_modules/@nestjs/swagger": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", + "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.17.14" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@nestjs/swagger/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@nestjs/swagger/node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + }, "node_modules/@nestjs/testing": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.10.tgz", @@ -14709,6 +14804,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==" + }, "node_modules/@types/wait-on": { "version": "5.3.4", "dev": true, @@ -17457,6 +17557,21 @@ "version": "1.2.3", "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/classnames": { "version": "2.3.2", "license": "MIT" @@ -19630,18 +19745,18 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "license": "BSD-2-Clause", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { "version": "10.0.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -29476,6 +29591,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz", + "integrity": "sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q==" + }, "node_modules/lie": { "version": "3.3.0", "license": "MIT", @@ -43925,6 +44045,11 @@ "boolbase": "~1.0.0" } }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" + }, "node_modules/swc-loader": { "version": "0.2.3", "dev": true, @@ -45733,6 +45858,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-equal": { "version": "1.0.1", "license": "MIT" @@ -47970,18 +48103,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "packages/api/node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "packages/api/node_modules/eslint": { "version": "8.31.0", "dev": true,