diff --git a/api/scripts/certification/rescore-certifications.js b/api/scripts/certification/rescore-certifications.js index 5fed135f389..3a5bb8ad6a8 100644 --- a/api/scripts/certification/rescore-certifications.js +++ b/api/scripts/certification/rescore-certifications.js @@ -1,58 +1,61 @@ import 'dotenv/config'; -import * as url from 'node:url'; +import Joi from 'joi'; -import { disconnect } from '../../db/knex-database-connection.js'; import { CertificationRescoringByScriptJob } from '../../src/certification/session-management/domain/models/CertificationRescoringByScriptJob.js'; import { certificationRescoringByScriptJobRepository } from '../../src/certification/session-management/infrastructure/repositories/jobs/certification-rescoring-by-script-job-repository.js'; -import { logger } from '../../src/shared/infrastructure/utils/logger.js'; +import { csvFileParser } from '../../src/shared/application/scripts/parsers.js'; +import { Script } from '../../src/shared/application/scripts/script.js'; +import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js'; + +const columnsSchemas = [{ name: 'certificationCourseId', schema: Joi.number() }]; + +export class RescoreCertificationScript extends Script { + constructor() { + super({ + description: 'Rescore all certification given by CSV file. This script will schedule job to rescore', + permanent: true, + options: { + file: { + type: 'string', + describe: + 'CSV File with only one column with certification-courses.id (integer) to process. Need `certificationCourseId`', + demandOption: true, + coerce: csvFileParser(columnsSchemas), + }, + }, + }); + } -const modulePath = url.fileURLToPath(import.meta.url); -const isLaunchedFromCommandLine = process.argv[1] === modulePath; + async handle({ options, logger }) { + const { file: certificationCourses } = options; + const certificationCourseIds = certificationCourses.map(({ certificationCourseId }) => certificationCourseId); -async function main(certificationCourseIds) { - logger.info(`Publishing ${certificationCourseIds.length} rescoring jobs`); - const jobs = await _scheduleRescoringJobs(certificationCourseIds); + logger.info(`Publishing ${certificationCourseIds.length} rescoring jobs`); + const jobs = await this.#scheduleRescoringJobs(certificationCourseIds); - const errors = jobs.filter((result) => result.status === 'rejected'); - if (errors.length) { - errors.forEach((result) => logger.error(result.reason, 'Some jobs could not be published')); - return 1; - } + const errors = jobs.filter((result) => result.status === 'rejected'); + if (errors.length) { + errors.forEach((result) => logger.error(result.reason, 'Some jobs could not be published')); + return 1; + } - logger.info(`${jobs.length} jobs successfully published`); - return 0; -} + logger.info(`${jobs.length} jobs successfully published`); + return 0; + } -const _scheduleRescoringJobs = async (certificationCourseIds) => { - const promisefiedJobs = certificationCourseIds.map(async (certificationCourseId) => { - try { - await certificationRescoringByScriptJobRepository.performAsync( - new CertificationRescoringByScriptJob({ certificationCourseId }), - ); - } catch (error) { - throw new Error(`Error for certificationCourseId: [${certificationCourseId}]`, { cause: error }); - } - }); - return Promise.allSettled(promisefiedJobs); -}; - -(async () => { - if (isLaunchedFromCommandLine) { - try { - const certificationCourseIds = process.argv[2] - .split(',') - .map((str) => parseInt(str, 10)) - .filter(Number.isInteger); - const exitCode = await main(certificationCourseIds); - return exitCode; - } catch (error) { - logger.error(error); - process.exitCode = 1; - } finally { - await disconnect(); - } + async #scheduleRescoringJobs(certificationCourseIds) { + const promisefiedJobs = certificationCourseIds.map(async (certificationCourseId) => { + try { + await certificationRescoringByScriptJobRepository.performAsync( + new CertificationRescoringByScriptJob({ certificationCourseId }), + ); + } catch (error) { + throw new Error(`Error for certificationCourseId: [${certificationCourseId}]`, { cause: error }); + } + }); + return Promise.allSettled(promisefiedJobs); } -})(); +} -export { main }; +await ScriptRunner.execute(import.meta.url, RescoreCertificationScript); diff --git a/api/tests/integration/scripts/certification/rescore-certifications_test.js b/api/tests/integration/scripts/certification/rescore-certifications_test.js index 6597e2e11c7..e49533d1bbb 100644 --- a/api/tests/integration/scripts/certification/rescore-certifications_test.js +++ b/api/tests/integration/scripts/certification/rescore-certifications_test.js @@ -1,13 +1,31 @@ -import { main } from '../../../../scripts/certification/rescore-certifications.js'; -import { expect, knex } from '../../../test-helper.js'; +import { RescoreCertificationScript } from '../../../../scripts/certification/rescore-certifications.js'; +import { createTempFile, expect, knex, sinon } from '../../../test-helper.js'; describe('Integration | Scripts | Certification | rescore-certfication', function () { + it('should parse input file', async function () { + const script = new RescoreCertificationScript(); + const { options } = script.metaInfo; + const file = 'certification-courses-ids-to-rescore.csv'; + const data = 'certificationCourseId\n1\n2\n3\n'; + const csvFilePath = await createTempFile(file, data); + + const parsedData = await options.file.coerce(csvFilePath); + + expect(parsedData).to.deep.equals([ + { certificationCourseId: 1 }, + { certificationCourseId: 2 }, + { certificationCourseId: 3 }, + ]); + }); + it('should save pg boss jobs for each certification course ids', async function () { // given - const certificationsCourseIdList = [1, 2]; + const file = [{ certificationCourseId: 1 }, { certificationCourseId: 2 }]; + const logger = { info: sinon.spy(), error: sinon.spy() }; + const script = new RescoreCertificationScript(); // when - await main(certificationsCourseIdList); + await script.handle({ logger, options: { file } }); // then const [job1, job2] = await knex('pgboss.job')