From 1e8d30ee1d1740efd85b5e78ecd40e515434fc0f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:01:59 +0200 Subject: [PATCH 1/8] Fix spelling mistake --- lib/utilities/stringUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utilities/stringUtils.js b/lib/utilities/stringUtils.js index cc302cbed4..9f22effee2 100644 --- a/lib/utilities/stringUtils.js +++ b/lib/utilities/stringUtils.js @@ -66,9 +66,9 @@ const snakeToPascal = (snake) => ucFirst(snakeToCamel(snake)); * Split the received string to an array of trimmed strings. * Boolean trick: https://michaeluloth.com/javascript-filter-boolean/ * @param {string} stringCollection String containing other strings withing split by seperator. - * @param {string} stringSeperator Used to seperate the stringCollection. + * @param {string} stringSeparator Used to seperate the stringCollection. */ -const splitStringToStringsTrimmed = (stringCollection, stringSeperator = ',') => stringCollection.split(stringSeperator) +const splitStringToStringsTrimmed = (stringCollection, stringSeparator = ',') => stringCollection.split(stringSeparator) .map((string) => string.trim()) .filter(Boolean); From ded1c122aeb3c57f8e1caffab2b1972e06a9c4d0 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:04:37 +0200 Subject: [PATCH 2/8] Update beam type dto to split and return array of values --- lib/domain/dtos/common/BeamTypeDto.js | 20 +++++++++++-------- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 3 +-- .../lhcFill/GetAllLhcFillsUseCase.test.js | 6 +++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index dc2d401113..0f60d8df0a 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -12,12 +12,16 @@ */ const Joi = require('joi'); -const { validateBeamTypes, BEAM_TYPE_INVALID } = require('../../../utilities/beamTypeUtils'); +const { CustomJoi } = require('../CustomJoi.js'); +const { BEAM_TYPE_INVALID } = require('../../../utilities/beamTypeUtils'); -exports.BeamTypesDto = Joi.string() - .trim() - .custom(validateBeamTypes) - .messages({ - [BEAM_TYPE_INVALID]: '{{#message}}', - 'string.base': 'Beam type must be a string', - }); +exports.BeamTypesDto = CustomJoi.stringArray() + .items(Joi.string() + .trim() + .min(2) + .max(15) + .pattern(/^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/) + .messages({ + [BEAM_TYPE_INVALID]: '{{#message}}', + 'string.base': 'Beam type must be a string', + })); diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index f69ed2de34..8d9146dbd4 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -95,8 +95,7 @@ class GetAllLhcFillsUseCase { } if (beamTypes) { - const beamTypesArray = beamTypes.split(','); - queryBuilder.where('beamType').oneOf(beamTypesArray); + queryBuilder.where('beamType').oneOf(beamTypes); } if (schemeName) { diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 8fbb5f2781..8060e95e71 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -278,7 +278,7 @@ module.exports = () => { }) it('should only contain specified beam type, {p-p}', async () => { - getAllLhcFillsDto.query = { filter: { beamTypes: 'p-p' } }; + getAllLhcFillsDto.query = { filter: { beamTypes: ['p-p'] } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(2) @@ -290,7 +290,7 @@ module.exports = () => { it('should only contain specified beam types, {p-p, PROTON-PROTON, Pb-Pb}', async () => { const beamTypes = ['p-p', 'PROTON-PROTON', 'Pb-Pb'] - getAllLhcFillsDto.query = { filter: { beamTypes: beamTypes.join(',') } }; + getAllLhcFillsDto.query = { filter: { beamTypes: beamTypes } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(4) @@ -302,7 +302,7 @@ module.exports = () => { it('should ignore unknown beam types, {p-p, Hello-world, Pb-Pb}', async () => { const beamTypes = ['p-p', 'Hello-world', 'Pb-Pb'] - getAllLhcFillsDto.query = { filter: { beamTypes: beamTypes.join(',') } }; + getAllLhcFillsDto.query = { filter: { beamTypes: beamTypes } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(3) From 15fdc9c432344f27a1fed4e7eba1999c78a269b2 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:10:52 +0200 Subject: [PATCH 3/8] Remove unused utility and update tests --- lib/domain/dtos/common/BeamTypeDto.js | 3 +- lib/utilities/beamTypeUtils.js | 47 --------------------------- test/api/runs.test.js | 10 +++--- 3 files changed, 7 insertions(+), 53 deletions(-) delete mode 100644 lib/utilities/beamTypeUtils.js diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index 0f60d8df0a..8fd9e8cf57 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -13,7 +13,8 @@ const Joi = require('joi'); const { CustomJoi } = require('../CustomJoi.js'); -const { BEAM_TYPE_INVALID } = require('../../../utilities/beamTypeUtils'); + +const BEAM_TYPE_INVALID = 'beamType.invalid'; exports.BeamTypesDto = CustomJoi.stringArray() .items(Joi.string() diff --git a/lib/utilities/beamTypeUtils.js b/lib/utilities/beamTypeUtils.js deleted file mode 100644 index 55d8382325..0000000000 --- a/lib/utilities/beamTypeUtils.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -export const BEAM_TYPE_INVALID = 'beamType.invalid'; - -/** - * Validates beam types to have correct format - * Expects a string containing comma separated values. - * - * @param {string} value Beam types string to validate - * @param {*} helpers The helpers object - * @returns {string} The value if validation passes - */ -export const validateBeamTypes = (value, helpers) => { - const beamTypes = value.split(','); - - for (const type of beamTypes) { - // Max length accepted is 15 characters including spaces (e.g. "PROTON - PROTON") - if (type.length > 15) { - return helpers.error(BEAM_TYPE_INVALID, { - message: `Beam type exceeds max length of 15 characters: ${type}`, - }); - } - - /* - * Accepts combinations of letters and numbers separated by a hyphen, with optional spaces - * around the hyphen (e.g. "PROTON-PROTON", "PROTON - PROTON", "P1-P2") - */ - if (!/^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/.test(type)) { - return helpers.error(BEAM_TYPE_INVALID, { - message: `Invalid beam type format: ${type}`, - }); - } - } - - return value; -}; diff --git a/test/api/runs.test.js b/test/api/runs.test.js index c45a898634..a532128cff 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -161,7 +161,7 @@ module.exports = () => { }); it('should successfully filter with single beamType', async () => { - const beamType = 'p-p'; + const beamType = ['p-p']; const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamType}`); expect(response.status).to.equal(200); @@ -174,7 +174,7 @@ module.exports = () => { it('should successfully filter with multiple beamTypes', async () => { const beamTypes = ['p-p', 'Pb-Pb']; - const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes.join(',')}`); + const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes}`); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -185,15 +185,15 @@ module.exports = () => { }); it('should return 400 if beamTypes filter has the incorrect format', async () => { - const beamTypeString = 'DOES NOT EXIST'; - const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypeString}`); + const beamTypes = ['DOES NOT EXIST']; + const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes}`); expect(response.status).to.equal(400); const { errors: [error] } = response.body; expect(error.title).to.equal('Invalid Attribute'); - expect(error.detail).to.equal(`Invalid beam type format: ${beamTypeString}`); + expect(error.detail).to.equal(`Invalid beam type format: ${beamTypes}`); }); it('should return 400 if beamModes filter has the incorrect format', async () => { From b9334a34b46ef7979386e4a3e596febb4f530e5d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:19:07 +0200 Subject: [PATCH 4/8] Fix API tests --- test/api/lhcFills.test.js | 2 +- test/api/runs.test.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index dd84946b07..ff3fd3427f 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -771,7 +771,7 @@ module.exports = () => { const { errors: [error] } = res.body; expect(error.title).to.equal('Invalid Attribute'); - expect(error.detail).to.equal('"query.filter.beamTypes" is not allowed to be empty'); + expect(error.detail).to.equal('"query.filter.beamTypes[0]" is not allowed to be empty'); done(); }); }); diff --git a/test/api/runs.test.js b/test/api/runs.test.js index a532128cff..14a9d3c4e6 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -161,7 +161,7 @@ module.exports = () => { }); it('should successfully filter with single beamType', async () => { - const beamType = ['p-p']; + const beamType = 'p-p'; const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamType}`); expect(response.status).to.equal(200); @@ -173,7 +173,7 @@ module.exports = () => { }); it('should successfully filter with multiple beamTypes', async () => { - const beamTypes = ['p-p', 'Pb-Pb']; + const beamTypes = 'p-p,Pb-Pb'; const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes}`); expect(response.status).to.equal(200); @@ -185,7 +185,7 @@ module.exports = () => { }); it('should return 400 if beamTypes filter has the incorrect format', async () => { - const beamTypes = ['DOES NOT EXIST']; + const beamTypes = 'DOES NOT EXIST'; const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes}`); expect(response.status).to.equal(400); @@ -193,7 +193,7 @@ module.exports = () => { const { errors: [error] } = response.body; expect(error.title).to.equal('Invalid Attribute'); - expect(error.detail).to.equal(`Invalid beam type format: ${beamTypes}`); + expect(error.detail).to.equal(`"query.filter.beamTypes[0]" with value "${beamTypes}" fails to match the required pattern: /^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/`); }); it('should return 400 if beamModes filter has the incorrect format', async () => { From 67dd8a310dee1988bbb7c8c96bcfba08859e6361 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:24:02 +0200 Subject: [PATCH 5/8] Push also changes related to Runs as are using the BeamTypesDto --- lib/usecases/run/GetAllRunsUseCase.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index 813b5b66fe..24d7131023 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -121,10 +121,9 @@ class GetAllRunsUseCase { } if (beamTypes) { - const beamTypesList = splitStringToStringsTrimmed(beamTypes, SEARCH_ITEMS_SEPARATOR); filteringQueryBuilder.include({ association: 'lhcFill', - where: { beamType: { [Op.in]: beamTypesList } }, + where: { beamType: { [Op.in]: beamTypes } }, required: true, }); } From 33cf2876c760eda7daed27e79710c37494941856 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:32:38 +0200 Subject: [PATCH 6/8] FIx GetAllRuns tests --- test/lib/usecases/run/GetAllRunsUseCase.test.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index 7d4db513e5..bbaea9edaf 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -210,23 +210,22 @@ module.exports = () => { }); it('should successfully filter on beamTypes', async () => { - const singleBeamType = 'p-p'; - const multipleBeamTypes = 'p-p,Pb-Pb'; - const nonExistentBeamType = 'DOES-NOT-EXIST'; + const singleBeamType = ['p-p']; + const multipleBeamTypes = ['p-p', 'Pb-Pb']; + const nonExistentBeamType = ['DOES-NOT-EXIST']; getAllRunsDto.query = { filter: { beamTypes: singleBeamType }, page: { limit: 200 } }; { const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); expect(runs).to.have.lengthOf.greaterThan(0); - expect(runs.every(({ lhcFill }) => lhcFill?.beamType === singleBeamType)).to.be.true; + expect(runs.every(({ lhcFill }) => singleBeamType.includes(lhcFill?.beamType))).to.be.true; } getAllRunsDto.query = { filter: { beamTypes: multipleBeamTypes }, page: { limit: 200 } }; { - const acceptedBeamTypes = multipleBeamTypes.split(','); const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); expect(runs).to.have.lengthOf.greaterThan(0); - expect(runs.every(({ lhcFill }) => acceptedBeamTypes.includes(lhcFill?.beamType))).to.be.true; + expect(runs.every(({ lhcFill }) => multipleBeamTypes.includes(lhcFill?.beamType))).to.be.true; } getAllRunsDto.query = { filter: { beamTypes: nonExistentBeamType } }; From a35a5e8e5c24a0deb99a48cc0742e469016bf09b Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 1 Aug 2026 12:50:51 +0200 Subject: [PATCH 7/8] Provide more details to beam type format --- lib/domain/dtos/common/BeamTypeDto.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index 8fd9e8cf57..b2994bd501 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -14,8 +14,23 @@ const Joi = require('joi'); const { CustomJoi } = require('../CustomJoi.js'); -const BEAM_TYPE_INVALID = 'beamType.invalid'; - +/** + * @typedef {string[]} BeamTypesDto + * @description An array of beam types, each represented as a string. + * Each beam type must be a string with a minimum length of 2 characters and a maximum length of 15 characters. + * The string must match patterns such as "PROTON - PROTON", "NE10 - NE10", where the two parts are separated by a hyphen and optional spaces. + * + * RUN3 has the following beam types: + * "PROTON - PROTON" + * "NE10 - NE10" + * "O8 - O8" + * "PB82 - PB82" + * "PROTON - O8" + * "PROTON - PROTON" + * + * @example + * const beamTypes = ["PROTON - PROTON", "NE10 - NE10"]; + */ exports.BeamTypesDto = CustomJoi.stringArray() .items(Joi.string() .trim() @@ -23,6 +38,8 @@ exports.BeamTypesDto = CustomJoi.stringArray() .max(15) .pattern(/^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/) .messages({ - [BEAM_TYPE_INVALID]: '{{#message}}', 'string.base': 'Beam type must be a string', + 'string.min': 'Beam type must be at least 2 characters long', + 'string.max': 'Beam type must be at most 15 characters long', + 'string.pattern.base': 'Beam type must look like "PROTON - PROTON", "NE10 - NE10", etc.', })); From 11a0a33bb2dc8c384ad23b642ee01dfe74d6bd84 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 1 Aug 2026 12:55:05 +0200 Subject: [PATCH 8/8] Fix test for error message --- test/api/runs.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 14a9d3c4e6..85945ebb06 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -193,7 +193,7 @@ module.exports = () => { const { errors: [error] } = response.body; expect(error.title).to.equal('Invalid Attribute'); - expect(error.detail).to.equal(`"query.filter.beamTypes[0]" with value "${beamTypes}" fails to match the required pattern: /^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/`); + expect(error.detail).to.equal(`Beam type must look like "PROTON - PROTON", "NE10 - NE10", etc.`); }); it('should return 400 if beamModes filter has the incorrect format', async () => {