diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index dc2d401113..b2994bd501 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -12,12 +12,34 @@ */ const Joi = require('joi'); -const { validateBeamTypes, BEAM_TYPE_INVALID } = require('../../../utilities/beamTypeUtils'); +const { CustomJoi } = require('../CustomJoi.js'); -exports.BeamTypesDto = Joi.string() - .trim() - .custom(validateBeamTypes) - .messages({ - [BEAM_TYPE_INVALID]: '{{#message}}', - 'string.base': 'Beam type must be a string', - }); +/** + * @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() + .min(2) + .max(15) + .pattern(/^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/) + .messages({ + '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.', + })); 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/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, }); } 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/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); 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 c45a898634..85945ebb06 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -173,8 +173,8 @@ 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 beamTypes = 'p-p,Pb-Pb'; + 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(`Beam type must look like "PROTON - PROTON", "NE10 - NE10", etc.`); }); it('should return 400 if beamModes filter has the incorrect format', async () => { 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) 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 } };