diff --git a/.env.sample b/.env.sample index 3323664..d32076f 100644 --- a/.env.sample +++ b/.env.sample @@ -36,3 +36,9 @@ PROCONNECT_ENDPOINT= # Api INSEE INSEE_API_URL= INSEE_API_KEY_INTEGRATION= + +# API DataGouv +DATAGOUV_API_URL= +DATAGOUV_API_KEY= +DATAGOUV_DATASET_ID= +DATAGOUV_RESOURCE_ID= diff --git a/README.md b/README.md index e8cd397..c8a8d1b 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ Elles peuvent être définies classiquement ou en créant un fichier `.env` sur | `PROCONNECT_ENDPOINT` | Proconnect Endpoint | | `INSEE_API_URL` | Url de l'API INSEE (Sirene) | | `INSEE_API_KEY_INTEGRATION` | Clef de l'API INSEE (Sirene) | +| `DATAGOUV_API_URL` | URL de l'API DataGouv | +| `DATAGOUV_API_KEY` | Clef de l'API DataGouv | +| `DATAGOUV_DATASET_ID` | Id du dataset sur DataGouv | +| `DATAGOUV_RESOURCE_ID` | Id de la ressource sur DataGouv | ## Licence diff --git a/package.json b/package.json index 0eb501d..182b422 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@testcontainers/postgresql": "^10.10.0", "@types/express": "^4.17.17", "@types/geojson-vt": "^3.2.0", - "@types/jest": "^29.5.2", + "@types/jest": "^30.0.0", "@types/node": "^20.3.1", "@types/nodemailer": "^6.4.15", "@types/supertest": "^6.0.0", diff --git a/src/modules/datagouv/datagouv.module.ts b/src/modules/datagouv/datagouv.module.ts new file mode 100644 index 0000000..88c4f26 --- /dev/null +++ b/src/modules/datagouv/datagouv.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DataGouvService } from './datagouv.service'; + +@Module({ + imports: [ConfigModule], + providers: [DataGouvService], + exports: [DataGouvService], +}) +export class DataGouvModule {} diff --git a/src/modules/datagouv/datagouv.service.ts b/src/modules/datagouv/datagouv.service.ts new file mode 100644 index 0000000..c647923 --- /dev/null +++ b/src/modules/datagouv/datagouv.service.ts @@ -0,0 +1,44 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; +import FormData = require('form-data'); + +@Injectable() +export class DataGouvService { + private readonly logger = new Logger(DataGouvService.name); + + constructor(private readonly configService: ConfigService) {} + + async uploadCSVResource( + datasetId: string, + resourceId: string, + csvContent: string, + fileName: string, + ): Promise { + const apiKey = this.configService.get('DATAGOUV_API_KEY'); + const apiUrl = + this.configService.get('DATAGOUV_API_URL') || + 'https://www.data.gouv.fr/api/1'; + + const form = new FormData(); + form.append('file', Buffer.from(csvContent, 'utf-8'), { + filename: fileName, + contentType: 'text/csv', + }); + + await axios.post( + `${apiUrl}/datasets/${datasetId}/resources/${resourceId}/upload/`, + form, + { + headers: { + ...form.getHeaders(), + 'X-API-KEY': apiKey, + }, + }, + ); + + this.logger.log( + `CSV uploaded to data.gouv.fr (dataset: ${datasetId}, resource: ${resourceId})`, + ); + } +} diff --git a/src/modules/signalement/signalement.service.ts b/src/modules/signalement/signalement.service.ts index 304d6d6..6cc0609 100644 --- a/src/modules/signalement/signalement.service.ts +++ b/src/modules/signalement/signalement.service.ts @@ -17,6 +17,8 @@ import { CreateReportDTO, } from '../../common/base-report.service'; import { StatsDTO } from '../stats/stats.dto'; +import { ReportStatusEnum } from '../../common/report-status.enum'; +import { getCommune } from '../../utils/cog.utils'; @Injectable() export class SignalementService extends BaseReportService { @@ -144,4 +146,79 @@ export class SignalementService extends BaseReportService { count: Number(count), })) as { codeCommune: string; count: number }[]; } + + async getSignalementCountsByCommune(): Promise< + { + codeCommune: string; + nomCommune: string; + pending: number; + processed: number; + ignored: number; + expired: number; + total: number; + }[] + > { + const qb = this.repository.createQueryBuilder('signalement'); + + const rawResults: { + codeCommune: string; + status: string; + count: string; + }[] = await qb + .select('signalement.code_commune', 'codeCommune') + .addSelect('signalement.status', 'status') + .addSelect('COUNT(signalement.id)', 'count') + .groupBy('signalement.code_commune') + .addGroupBy('signalement.status') + .getRawMany(); + + const communeMap = new Map< + string, + { + pending: number; + processed: number; + ignored: number; + expired: number; + total: number; + } + >(); + + for (const row of rawResults) { + if (!communeMap.has(row.codeCommune)) { + communeMap.set(row.codeCommune, { + pending: 0, + processed: 0, + ignored: 0, + expired: 0, + total: 0, + }); + } + const entry = communeMap.get(row.codeCommune); + const count = Number(row.count); + + switch (row.status) { + case ReportStatusEnum.PENDING: + entry.pending = count; + break; + case ReportStatusEnum.PROCESSED: + entry.processed = count; + break; + case ReportStatusEnum.IGNORED: + entry.ignored = count; + break; + case ReportStatusEnum.EXPIRED: + entry.expired = count; + break; + } + entry.total += count; + } + + return Array.from(communeMap.entries()) + .map(([codeCommune, counts]) => ({ + codeCommune, + nomCommune: getCommune(codeCommune)?.nom || codeCommune, + ...counts, + })) + .sort((a, b) => b.total - a.total); + } } diff --git a/src/modules/task/task.module.ts b/src/modules/task/task.module.ts index 7ce6cc9..b8cd6b9 100644 --- a/src/modules/task/task.module.ts +++ b/src/modules/task/task.module.ts @@ -2,12 +2,14 @@ import { Module, forwardRef } from '@nestjs/common'; import { SignalementModule } from '../signalement/signalement.module'; import { TaskService } from './task.service'; import { MesAdressesAPIModule } from '../mes-adresses-api/mes-adresses-api.module'; +import { DataGouvModule } from '../datagouv/datagouv.module'; import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ forwardRef(() => SignalementModule), MesAdressesAPIModule, + DataGouvModule, ConfigModule, ], providers: [TaskService], diff --git a/src/modules/task/task.service.ts b/src/modules/task/task.service.ts index d34db0c..d7e0a04 100644 --- a/src/modules/task/task.service.ts +++ b/src/modules/task/task.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { SignalementService } from '../signalement/signalement.service'; import { MesAdressesAPIService } from '../mes-adresses-api/mes-adresses-api.service'; +import { DataGouvService } from '../datagouv/datagouv.service'; import { MailerService } from '@nestjs-modules/mailer'; import { ConfigService } from '@nestjs/config'; import { getCommune } from '../../utils/cog.utils'; @@ -13,6 +14,7 @@ export class TaskService { constructor( private readonly signalementService: SignalementService, private readonly mesAdressesAPIService: MesAdressesAPIService, + private readonly dataGouvService: DataGouvService, private readonly mailerService: MailerService, private configService: ConfigService, ) {} @@ -98,4 +100,40 @@ export class TaskService { ' signalements', ); } + + // Cron job that runs every Tuesday at 17:00 PM + @Cron('0 17 * * 2') + async weeklyDataGouvCSVExport() { + const datasetId = this.configService.get('DATAGOUV_DATASET_ID'); + const resourceId = this.configService.get('DATAGOUV_RESOURCE_ID'); + + if (!datasetId || !resourceId) { + this.logger.warn( + 'Skipping weeklyDataGouvCSVExport: DATAGOUV_DATASET_ID or DATAGOUV_RESOURCE_ID not configured', + ); + return; + } + + this.logger.log('Start task : weeklyDataGouvCSVExport'); + + const countsByCommune = + await this.signalementService.getSignalementCountsByCommune(); + + const header = + 'code_insee,nom_commune,nb_signalements_en_attente,nb_signalements_traites,nb_signalements_ignores,nb_signalements_expires,nb_signalements_total'; + const rows = countsByCommune.map( + (row) => + `${row.codeCommune},${row.nomCommune.includes(',') ? `"${row.nomCommune}"` : row.nomCommune},${row.pending},${row.processed},${row.ignored},${row.expired},${row.total}`, + ); + const csvContent = [header, ...rows].join('\n'); + + await this.dataGouvService.uploadCSVResource( + datasetId, + resourceId, + csvContent, + 'signalements-par-commune.csv', + ); + + this.logger.log('End task : weeklyDataGouvCSVExport'); + } } diff --git a/src/tests/proconnect.spec.ts b/src/tests/proconnect.spec.ts index fcd2881..93e44b2 100644 --- a/src/tests/proconnect.spec.ts +++ b/src/tests/proconnect.spec.ts @@ -31,6 +31,8 @@ const MES_ADRESSES_URL = 'http://localhost:8888'; jest.setTimeout(60000); +jest.spyOn(console, 'error').mockImplementation(() => undefined); + describe('ProConnect module', () => { let app: INestApplication; let postgresContainer: StartedPostgreSqlContainer; diff --git a/src/tests/settings.spec.ts b/src/tests/settings.spec.ts index fc1862a..ca5d3c1 100644 --- a/src/tests/settings.spec.ts +++ b/src/tests/settings.spec.ts @@ -20,6 +20,9 @@ import { EnabledListKeys, SignalementSubmissionMode, } from '../modules/setting/setting.type'; +import { Logger } from '@nestjs/common'; + +Logger.overrideLogger(false); const currentRevisionMock = jest.fn(); @@ -34,6 +37,7 @@ const testSource = new Source({ provide: ApiDepotService, useValue: { getCurrentRevision: currentRevisionMock, + getAllCurrentRevisions: jest.fn().mockResolvedValue([]), }, }, ], diff --git a/src/tests/task.spec.ts b/src/tests/task.spec.ts index bfae64c..03acecb 100644 --- a/src/tests/task.spec.ts +++ b/src/tests/task.spec.ts @@ -5,6 +5,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { SignalementService } from '../modules/signalement/signalement.service'; import { MesAdressesAPIService } from '../modules/mes-adresses-api/mes-adresses-api.service'; import { MailerService } from '@nestjs-modules/mailer'; +import { DataGouvService } from '../modules/datagouv/datagouv.service'; import { createRecording } from '../utils/test.utils'; import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm'; import { Signalement } from '../modules/signalement/signalement.entity'; @@ -22,6 +23,7 @@ import { import { Client as PGClient } from 'pg'; import { entities } from '../app.entities'; import { SignalementModule } from '../modules/signalement/signalement.module'; +import { ReportStatusEnum } from '../common/report-status.enum'; const getPendingSignalementsReportMock = jest.fn(() => Promise.resolve([ @@ -49,6 +51,8 @@ const searchBaseLocaleMock = jest.fn(() => const sendMailMock = jest.fn(); +const uploadCSVResourceMock = jest.fn(() => Promise.resolve()); + @Global() @Module({ providers: [ @@ -64,8 +68,14 @@ const sendMailMock = jest.fn(); sendMail: sendMailMock, }, }, + { + provide: DataGouvService, + useValue: { + uploadCSVResource: uploadCSVResourceMock, + }, + }, ], - exports: [MesAdressesAPIService, MailerService], + exports: [MesAdressesAPIService, MailerService, DataGouvService], }) class TestModule {} @@ -265,4 +275,129 @@ describe('Task module', () => { expect(signalements).toHaveLength(0); }); }); + + describe('Task weeklyDataGouvCSVExport', () => { + beforeEach(() => { + uploadCSVResourceMock.mockClear(); + }); + + it('should skip if DATAGOUV_DATASET_ID is not configured', async () => { + jest.spyOn(configService, 'get').mockReturnValue(undefined); + + await app.get(TaskService).weeklyDataGouvCSVExport(); + + expect(uploadCSVResourceMock).not.toHaveBeenCalled(); + }); + + it('should generate CSV and upload to data.gouv.fr', async () => { + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + const config = { + DATAGOUV_DATASET_ID: 'test-dataset-id', + DATAGOUV_RESOURCE_ID: 'test-resource-id', + }; + return config[key]; + }); + + const source = await createRecording( + sourceRepository, + new Source({ + nom: 'Mes signalements', + type: SourceTypeEnum.PUBLIC, + }), + ); + + const createSignalement = ( + codeCommune: string, + status: ReportStatusEnum, + ) => { + const signalement = new Signalement({ + codeCommune, + author: { email: 'test@test.com' }, + type: SignalementTypeEnum.LOCATION_TO_UPDATE, + existingLocation: { + type: ExistingLocationTypeEnum.NUMERO, + numero: 2, + suffixe: 'bis', + position: { + type: PositionTypeEnum.BATIMENT, + point: { type: 'Point', coordinates: [0.982904, 47.410998] }, + }, + toponyme: { + type: ExistingLocationTypeEnum.VOIE, + nom: 'Rue de la Paix', + }, + }, + changesRequested: { + numero: 3, + suffixe: 'ter', + positions: [ + { + type: PositionTypeEnum.BATIMENT, + point: { type: 'Point', coordinates: [0.982904, 47.410998] }, + }, + ], + parcelles: ['37003000BA0744'], + } as NumeroChangesRequestedDTO, + }); + signalement.source = source; + signalement.status = status; + return signalement; + }; + + // Create signalements with different statuses for two communes + await createRecording( + signalementRepository, + createSignalement('37003', ReportStatusEnum.PENDING), + ); + await createRecording( + signalementRepository, + createSignalement('37003', ReportStatusEnum.PENDING), + ); + await createRecording( + signalementRepository, + createSignalement('37003', ReportStatusEnum.PROCESSED), + ); + await createRecording( + signalementRepository, + createSignalement('37185', ReportStatusEnum.IGNORED), + ); + await createRecording( + signalementRepository, + createSignalement('37185', ReportStatusEnum.EXPIRED), + ); + + await app.get(TaskService).weeklyDataGouvCSVExport(); + + expect(uploadCSVResourceMock).toHaveBeenCalledTimes(1); + expect(uploadCSVResourceMock).toHaveBeenCalledWith( + 'test-dataset-id', + 'test-resource-id', + expect.any(String), + 'signalements-par-commune.csv', + ); + + const csvContent = ( + uploadCSVResourceMock.mock.calls as any + )[0][2] as string; + const lines = csvContent.split('\n'); + + expect(lines[0]).toBe( + 'code_insee,nom_commune,nb_signalements_en_attente,nb_signalements_traites,nb_signalements_ignores,nb_signalements_expires,nb_signalements_total', + ); + + // Should have header + 2 communes + expect(lines).toHaveLength(3); + + const commune37003 = lines.find((l) => l.startsWith('37003')); + const commune37185 = lines.find((l) => l.startsWith('37185')); + + expect(commune37003).toBeDefined(); + expect(commune37185).toBeDefined(); + + // 37003: 2 pending, 1 processed, 0 ignored, 0 expired, 3 total + expect(commune37003).toContain(',2,1,0,0,3'); + // 37185: 0 pending, 0 processed, 1 ignored, 1 expired, 2 total + expect(commune37185).toContain(',0,0,1,1,2'); + }); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 189e8af..39c5d3d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,6 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, "resolveJsonModule": true, + "types": ["node", "jest"], }, } diff --git a/yarn.lock b/yarn.lock index 7d691b2..34513c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -67,6 +67,15 @@ "@babel/highlight" "^7.23.4" chalk "^2.4.2" +"@babel/code-frame@^7.27.1": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz" @@ -191,6 +200,11 @@ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz" @@ -640,6 +654,11 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/diff-sequences@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz#25b0818d3d83f00b9c7b04e069b8810f9014b143" + integrity sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA== + "@jest/environment@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz" @@ -650,6 +669,13 @@ "@types/node" "*" jest-mock "^29.7.0" +"@jest/expect-utils@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.3.0.tgz#c45b2da9802ffed33bf43b3e019ddb95e5ad95e8" + integrity sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA== + dependencies: + "@jest/get-type" "30.1.0" + "@jest/expect-utils@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz" @@ -677,6 +703,11 @@ jest-mock "^29.7.0" jest-util "^29.7.0" +"@jest/get-type@30.1.0": + version "30.1.0" + resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" + integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== + "@jest/globals@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz" @@ -687,6 +718,14 @@ "@jest/types" "^29.6.3" jest-mock "^29.7.0" +"@jest/pattern@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" + integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA== + dependencies: + "@types/node" "*" + jest-regex-util "30.0.1" + "@jest/reporters@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz" @@ -717,6 +756,13 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" +"@jest/schemas@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" + integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA== + dependencies: + "@sinclair/typebox" "^0.34.0" + "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" @@ -774,6 +820,19 @@ slash "^3.0.0" write-file-atomic "^4.0.2" +"@jest/types@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.3.0.tgz#cada800d323cb74945c24ac74615fdb312a6c85f" + integrity sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw== + dependencies: + "@jest/pattern" "30.0.1" + "@jest/schemas" "30.0.5" + "@types/istanbul-lib-coverage" "^2.0.6" + "@types/istanbul-reports" "^3.0.4" + "@types/node" "*" + "@types/yargs" "^17.0.33" + chalk "^4.1.2" + "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" @@ -1074,6 +1133,11 @@ resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sinclair/typebox@^0.34.0": + version "0.34.49" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.49.tgz#4f1369234f2ecf693866476c3b2e1b54d2a9d68e" + integrity sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A== + "@sinonjs/commons@^3.0.0": version "3.0.1" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" @@ -2740,7 +2804,7 @@ resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== @@ -2752,20 +2816,20 @@ dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^3.0.0": +"@types/istanbul-reports@^3.0.0", "@types/istanbul-reports@^3.0.4": version "3.0.4" resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.2": - version "29.5.12" - resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz" - integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== +"@types/jest@^30.0.0": + version "30.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" + integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== dependencies: - expect "^29.0.0" - pretty-format "^29.0.0" + expect "^30.0.0" + pretty-format "^30.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8": version "7.0.15" @@ -2884,7 +2948,7 @@ "@types/node" "*" "@types/ssh2-streams" "*" -"@types/stack-utils@^2.0.0": +"@types/stack-utils@^2.0.0", "@types/stack-utils@^2.0.3": version "2.0.3" resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== @@ -2916,6 +2980,13 @@ resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== +"@types/yargs@^17.0.33": + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^17.0.8": version "17.0.32" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz" @@ -3259,7 +3330,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: +ansi-styles@^5.0.0, ansi-styles@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== @@ -3861,6 +3932,11 @@ ci-info@^3.2.0, ci-info@^3.8.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +ci-info@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" + integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== + cjs-module-lexer@^1.0.0: version "1.2.3" resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz" @@ -4829,7 +4905,7 @@ exit@^0.1.2: resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^29.0.0, expect@^29.7.0: +expect@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz" integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== @@ -4840,6 +4916,18 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +expect@^30.0.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.3.0.tgz#1b82111517d1ab030f3db0cf1b4061c8aa644f61" + integrity sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q== + dependencies: + "@jest/expect-utils" "30.3.0" + "@jest/get-type" "30.1.0" + jest-matcher-utils "30.3.0" + jest-message-util "30.3.0" + jest-mock "30.3.0" + jest-util "30.3.0" + express@4.18.2: version "4.18.2" resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" @@ -5321,7 +5409,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -5891,6 +5979,16 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-diff@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.3.0.tgz#e0a4c84ef350ffd790ffd5b0016acabeecf5f759" + integrity sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ== + dependencies: + "@jest/diff-sequences" "30.3.0" + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + pretty-format "30.3.0" + jest-diff@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz" @@ -5963,6 +6061,16 @@ jest-leak-detector@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" +jest-matcher-utils@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz#d6c739fec1ecd33809f2d2b1348f6ab01d2f2493" + integrity sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA== + dependencies: + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + jest-diff "30.3.0" + pretty-format "30.3.0" + jest-matcher-utils@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz" @@ -5973,6 +6081,21 @@ jest-matcher-utils@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" +jest-message-util@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.3.0.tgz#4d723544d36890ba862ac3961db52db5b0d1ba39" + integrity sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@jest/types" "30.3.0" + "@types/stack-utils" "^2.0.3" + chalk "^4.1.2" + graceful-fs "^4.2.11" + picomatch "^4.0.3" + pretty-format "30.3.0" + slash "^3.0.0" + stack-utils "^2.0.6" + jest-message-util@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz" @@ -5988,6 +6111,15 @@ jest-message-util@^29.7.0: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.3.0.tgz#e0fa4184a596a6c4fdec53d4f412158418923747" + integrity sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog== + dependencies: + "@jest/types" "30.3.0" + "@types/node" "*" + jest-util "30.3.0" + jest-mock@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz" @@ -6002,6 +6134,11 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== +jest-regex-util@30.0.1: + version "30.0.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" + integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== + jest-regex-util@^29.6.3: version "29.6.3" resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" @@ -6111,6 +6248,18 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" +jest-util@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.3.0.tgz#95a4fbacf2dac20e768e2f1744b70519f2ba7980" + integrity sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg== + dependencies: + "@jest/types" "30.3.0" + "@types/node" "*" + chalk "^4.1.2" + ci-info "^4.2.0" + graceful-fs "^4.2.11" + picomatch "^4.0.3" + jest-util@^29.0.0, jest-util@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" @@ -7606,6 +7755,11 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz" @@ -7616,6 +7770,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + pirates@^4.0.4: version "4.0.6" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" @@ -7692,7 +7851,16 @@ prettier@^3.0.0: resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz" integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ== -pretty-format@^29.0.0, pretty-format@^29.7.0: +pretty-format@30.3.0, pretty-format@^30.0.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.3.0.tgz#e977eed4bcd1b6195faed418af8eac68b9ea1f29" + integrity sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ== + dependencies: + "@jest/schemas" "30.0.5" + ansi-styles "^5.2.0" + react-is "^18.3.1" + +pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== @@ -8006,6 +8174,11 @@ react-is@^18.0.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-is@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" @@ -8452,7 +8625,7 @@ ssh2@^1.11.0, ssh2@^1.4.0: cpu-features "~0.0.9" nan "^2.18.0" -stack-utils@^2.0.3: +stack-utils@^2.0.3, stack-utils@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==