From 1e8d30ee1d1740efd85b5e78ecd40e515434fc0f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:01:59 +0200 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 17586546252ffb1e83093167f8f8344babc7e98e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 14:10:35 +0200 Subject: [PATCH 07/21] Add endpoint for retrieving all unique pdpBeamTypes --- lib/server/controllers/runs.controller.js | 26 +++++++++++++++ lib/server/routers/runs.router.js | 5 +++ .../services/beam/getAllPdpBeamTypes.js | 32 +++++++++++++++++++ .../pdpBeamTypes/getAllPdpBeamTypes.test.js | 30 +++++++++++++++++ .../lib/server/services/pdpBeamTypes/index.js | 18 +++++++++++ 5 files changed, 111 insertions(+) create mode 100644 lib/server/services/beam/getAllPdpBeamTypes.js create mode 100644 test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js create mode 100644 test/lib/server/services/pdpBeamTypes/index.js diff --git a/lib/server/controllers/runs.controller.js b/lib/server/controllers/runs.controller.js index 916fdb7443..380899dff4 100644 --- a/lib/server/controllers/runs.controller.js +++ b/lib/server/controllers/runs.controller.js @@ -38,6 +38,7 @@ const { ApiConfig } = require('../../config/index.js'); const { DtoFactory } = require('../../domain/dtos/DtoFactory.js'); const { runService } = require('../services/run/RunService.js'); const { getAllBeamModes } = require('../services/beam/getAllBeamModes.js'); +const { getAllPdpBeamTypes } = require('../services/beam/getAllPdpBeamTypes.js'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js'); const { runToHttpView } = require('./runsToHttpView.js'); @@ -321,6 +322,30 @@ const listBeamModes = async (_request, response, _next) => { } }; +/** + * Retrieve a list of unique PDP beam types + * + * @param {Object} _request The *request* object represents the HTTP request and has properties for the request query + * string, parameters, body, HTTP headers, and so on. + * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets + * an HTTP request. + * @param {NextFunction} _next The *next* object represents the next middleware function which is used to pass control to + * the next middleware function. + * @returns {undefined} + */ +const listPdpBeamTypes = async (_request, response, _next) => { + try { + const pdpBeamTypes = await getAllPdpBeamTypes(); + if (pdpBeamTypes?.length > 0) { + response.status(200).json({ data: pdpBeamTypes }); + } else { + response.status(204).json({ data: [] }); + } + } catch { + response.status(502).json({ errors: ['Unable to retrieve list of PDP beam types'] }); + } +}; + // eslint-disable-next-line jsdoc/require-param /** * Retrieve distinct combination of levels of alice L3 and dipole current rounded to kilo amperes @@ -348,6 +373,7 @@ module.exports = { getFlpsByRunNumberHandler, listReasonTypes, listBeamModes, + listPdpBeamTypes, listRuns, startRun, updateRunByRunNumber, diff --git a/lib/server/routers/runs.router.js b/lib/server/routers/runs.router.js index 59d453d9bc..b0db9f58bd 100644 --- a/lib/server/routers/runs.router.js +++ b/lib/server/routers/runs.router.js @@ -30,6 +30,11 @@ module.exports = { path: 'beamModes', controller: RunsController.listBeamModes, }, + { + method: 'get', + path: 'pdpBeamTypes', + controller: RunsController.listPdpBeamTypes, + }, { method: 'get', controller: [infoLoggerListenerMiddleware(FilterLogger), RunsController.listRuns], diff --git a/lib/server/services/beam/getAllPdpBeamTypes.js b/lib/server/services/beam/getAllPdpBeamTypes.js new file mode 100644 index 0000000000..4258ca3a72 --- /dev/null +++ b/lib/server/services/beam/getAllPdpBeamTypes.js @@ -0,0 +1,32 @@ +/** + * @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. + */ + +const { repositories: { RunRepository }, sequelize } = require('../../../database'); +const { Op } = require('sequelize'); + +/** + * Return the a list of unique PDP beam types which is built from the runs data + * + * @returns {Promise} Promise resolving with the list of unique PDP beam types + */ +exports.getAllPdpBeamTypes = async () => { + const pdpBeamTypes = await RunRepository.findAll({ + where: { + pdp_beam_type: { + [Op.ne]: null, + }, + }, + attributes: [[sequelize.fn('DISTINCT', sequelize.col('pdp_beam_type')), 'name']], + }); + return pdpBeamTypes; +}; diff --git a/test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js b/test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js new file mode 100644 index 0000000000..55c3435a2e --- /dev/null +++ b/test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js @@ -0,0 +1,30 @@ +/** + * @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. + */ + +const { expect } = require('chai'); +const { getAllPdpBeamTypes } = require('../../../../../lib/server/services/beam/getAllPdpBeamTypes.js'); + +module.exports = () => { + it('should successfully return the full list of not null PDP beam types from runs table', async () => { + const pdpBeamTypes = await getAllPdpBeamTypes(); + expect(pdpBeamTypes.map(({ dataValues: { name } }) => ({ name }))).to.deep.eq([ + { name: 'cosmic' }, + { name: 'technical' }, + { name: 'pp' }, + { name: 'PbPb' }, + { name: 'pO' }, + { name: 'OO' }, + { name: 'NeNe' }, + ]); + }); +}; diff --git a/test/lib/server/services/pdpBeamTypes/index.js b/test/lib/server/services/pdpBeamTypes/index.js new file mode 100644 index 0000000000..c2b0111eaf --- /dev/null +++ b/test/lib/server/services/pdpBeamTypes/index.js @@ -0,0 +1,18 @@ +/** + * @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. + */ + +const getAllPdpBeamTypes = require('./getAllPdpBeamTypes.test.js'); + +module.exports = () => { + describe('getAllPdpBeamTypes', getAllPdpBeamTypes); +}; From a1273f2f503ced5cf128a958ef518a5cb1558ff9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 14:11:09 +0200 Subject: [PATCH 08/21] Add runs endpoint option to filter by pdpBeamType --- lib/domain/dtos/filters/RunFilterDto.js | 1 + lib/usecases/run/GetAllRunsUseCase.js | 5 ++ test/api/runs.test.js | 66 ++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 726ab4220f..39cc5a4418 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -41,6 +41,7 @@ exports.RunFilterDto = Joi.object({ 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', })), beamTypes: BeamTypesDto, + pdpBeamTypes: CustomJoi.stringArray().items(Joi.string().trim().min(2).max(20)), runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index 24d7131023..3014a8e548 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -85,6 +85,7 @@ class GetAllRunsUseCase { detectorsQcNotBadFraction, beamModes, beamTypes, + pdpBeamTypes, } = filter; if (runNumbers) { @@ -120,6 +121,10 @@ class GetAllRunsUseCase { filteringQueryBuilder.where('lhcBeamMode').oneOf(...beamModes); } + if (pdpBeamTypes) { + filteringQueryBuilder.where('pdpBeamType').oneOf(...pdpBeamTypes); + } + if (beamTypes) { filteringQueryBuilder.include({ association: 'lhcFill', diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 14a9d3c4e6..8e42828f5a 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -196,6 +196,52 @@ module.exports = () => { 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 successfully filter runs with pdpBeamType', async () => { + const pdpBeamType = 'pp'; + const response = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamType}`); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf(6); + expect(runs.every(({ pdpBeamType: type }) => type === pdpBeamType)).to.be.true; + }); + + it('should successfully filter runs with multiple pdpBeamTypes', async () => { + const pdpBeamTypes = 'pp,PbPb'; + const response = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamTypes}`); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf(10); + expect(runs.every(({ pdpBeamType: type }) => pdpBeamTypes.includes(type))).to.be.true; + }); + + it('should return 400 if pdpBeamTypes filter has the incorrect format', async () => { + const pdpBeamTypes = 'S'; // Too short + const response = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamTypes}`); + + expect(response.status).to.equal(400); + + let { errors: [error] } = response.body; + + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal(`"query.filter.pdpBeamTypes[0]" length must be at least 2 characters long`); + + const pdpBeamTypesLong = 'This is definitely not a pdp beam type'; // Too short + const responseLong = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamTypesLong}`); + + expect(responseLong.status).to.equal(400); + + ({ errors: [error] } = responseLong.body); + + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal(`"query.filter.pdpBeamTypes[0]" length must be less than or equal to 20 characters long`); + }); + it('should return 400 if beamModes filter has the incorrect format', async () => { const beamModeString = '*THERE\'S NON LETTERS IN HERE'; const response = await request(server).get(`/api/runs?filter[beamModes]=${beamModeString}`); @@ -727,7 +773,6 @@ module.exports = () => { }); }); - describe('GET /api/runs/beamModes', () => { it('should successfully return status 200 and list of beam modes', async () => { const { body } = await request(server) @@ -740,6 +785,25 @@ module.exports = () => { expect(body.data[0].name).to.equal('STABLE BEAMS'); }); }); + + describe('GET /api/runs/pdpBeamTypes', () => { + it('should successfully return status 200 and list of pdp beam types', async () => { + const { body } = await request(server) + .get('/api/runs/pdpBeamTypes') + .expect(200); + + expect(body.data).to.be.an('array'); + expect(body.data).to.have.lengthOf(5); + expect(body.data).to.deep.equal([ + { name: 'pp' }, + { name: 'PbPb' }, + { name: 'technical' }, + { name: 'cosmic' }, + { name: 'OO' }, + ]); + }); + }); + describe('GET /api/runs/reasonTypes', () => { it('should successfully return status 200 and list of reason types', async () => { const { body } = await request(server) From 2d90f6334fa1d1e3a7978ff5ab84408a267cd0de Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:01:59 +0200 Subject: [PATCH 09/21] 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 037e703ffddcd348364c8f1eeb7a508145efca32 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:04:37 +0200 Subject: [PATCH 10/21] 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 7fec84af5fac20ff732b07002dcd9cfa70705a75 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:10:52 +0200 Subject: [PATCH 11/21] 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 f1c60b471e7298af2fd704f6c61e6858fbdb8b66 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:19:07 +0200 Subject: [PATCH 12/21] 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 a8e5a4720683d6a3b809080b258f3074fe32fac6 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:24:02 +0200 Subject: [PATCH 13/21] 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 bde60d621eaa69f6e88cb04723f844f777c1ecad Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 13:32:38 +0200 Subject: [PATCH 14/21] 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 bab5e09d3e58fd951b704cb3ce74e587736fd613 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 1 Aug 2026 12:50:51 +0200 Subject: [PATCH 15/21] 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 f05178960aac102b64a4317070b292d1c8443d45 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 1 Aug 2026 12:55:05 +0200 Subject: [PATCH 16/21] 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 () => { From 8b135f014278a2bfc60fe5bc584bf39c04386f3f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 14:10:35 +0200 Subject: [PATCH 17/21] Add endpoint for retrieving all unique pdpBeamTypes --- lib/server/controllers/runs.controller.js | 26 +++++++++++++++ lib/server/routers/runs.router.js | 5 +++ .../services/beam/getAllPdpBeamTypes.js | 32 +++++++++++++++++++ .../pdpBeamTypes/getAllPdpBeamTypes.test.js | 30 +++++++++++++++++ .../lib/server/services/pdpBeamTypes/index.js | 18 +++++++++++ 5 files changed, 111 insertions(+) create mode 100644 lib/server/services/beam/getAllPdpBeamTypes.js create mode 100644 test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js create mode 100644 test/lib/server/services/pdpBeamTypes/index.js diff --git a/lib/server/controllers/runs.controller.js b/lib/server/controllers/runs.controller.js index 916fdb7443..380899dff4 100644 --- a/lib/server/controllers/runs.controller.js +++ b/lib/server/controllers/runs.controller.js @@ -38,6 +38,7 @@ const { ApiConfig } = require('../../config/index.js'); const { DtoFactory } = require('../../domain/dtos/DtoFactory.js'); const { runService } = require('../services/run/RunService.js'); const { getAllBeamModes } = require('../services/beam/getAllBeamModes.js'); +const { getAllPdpBeamTypes } = require('../services/beam/getAllPdpBeamTypes.js'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js'); const { runToHttpView } = require('./runsToHttpView.js'); @@ -321,6 +322,30 @@ const listBeamModes = async (_request, response, _next) => { } }; +/** + * Retrieve a list of unique PDP beam types + * + * @param {Object} _request The *request* object represents the HTTP request and has properties for the request query + * string, parameters, body, HTTP headers, and so on. + * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets + * an HTTP request. + * @param {NextFunction} _next The *next* object represents the next middleware function which is used to pass control to + * the next middleware function. + * @returns {undefined} + */ +const listPdpBeamTypes = async (_request, response, _next) => { + try { + const pdpBeamTypes = await getAllPdpBeamTypes(); + if (pdpBeamTypes?.length > 0) { + response.status(200).json({ data: pdpBeamTypes }); + } else { + response.status(204).json({ data: [] }); + } + } catch { + response.status(502).json({ errors: ['Unable to retrieve list of PDP beam types'] }); + } +}; + // eslint-disable-next-line jsdoc/require-param /** * Retrieve distinct combination of levels of alice L3 and dipole current rounded to kilo amperes @@ -348,6 +373,7 @@ module.exports = { getFlpsByRunNumberHandler, listReasonTypes, listBeamModes, + listPdpBeamTypes, listRuns, startRun, updateRunByRunNumber, diff --git a/lib/server/routers/runs.router.js b/lib/server/routers/runs.router.js index 59d453d9bc..b0db9f58bd 100644 --- a/lib/server/routers/runs.router.js +++ b/lib/server/routers/runs.router.js @@ -30,6 +30,11 @@ module.exports = { path: 'beamModes', controller: RunsController.listBeamModes, }, + { + method: 'get', + path: 'pdpBeamTypes', + controller: RunsController.listPdpBeamTypes, + }, { method: 'get', controller: [infoLoggerListenerMiddleware(FilterLogger), RunsController.listRuns], diff --git a/lib/server/services/beam/getAllPdpBeamTypes.js b/lib/server/services/beam/getAllPdpBeamTypes.js new file mode 100644 index 0000000000..4258ca3a72 --- /dev/null +++ b/lib/server/services/beam/getAllPdpBeamTypes.js @@ -0,0 +1,32 @@ +/** + * @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. + */ + +const { repositories: { RunRepository }, sequelize } = require('../../../database'); +const { Op } = require('sequelize'); + +/** + * Return the a list of unique PDP beam types which is built from the runs data + * + * @returns {Promise} Promise resolving with the list of unique PDP beam types + */ +exports.getAllPdpBeamTypes = async () => { + const pdpBeamTypes = await RunRepository.findAll({ + where: { + pdp_beam_type: { + [Op.ne]: null, + }, + }, + attributes: [[sequelize.fn('DISTINCT', sequelize.col('pdp_beam_type')), 'name']], + }); + return pdpBeamTypes; +}; diff --git a/test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js b/test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js new file mode 100644 index 0000000000..55c3435a2e --- /dev/null +++ b/test/lib/server/services/pdpBeamTypes/getAllPdpBeamTypes.test.js @@ -0,0 +1,30 @@ +/** + * @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. + */ + +const { expect } = require('chai'); +const { getAllPdpBeamTypes } = require('../../../../../lib/server/services/beam/getAllPdpBeamTypes.js'); + +module.exports = () => { + it('should successfully return the full list of not null PDP beam types from runs table', async () => { + const pdpBeamTypes = await getAllPdpBeamTypes(); + expect(pdpBeamTypes.map(({ dataValues: { name } }) => ({ name }))).to.deep.eq([ + { name: 'cosmic' }, + { name: 'technical' }, + { name: 'pp' }, + { name: 'PbPb' }, + { name: 'pO' }, + { name: 'OO' }, + { name: 'NeNe' }, + ]); + }); +}; diff --git a/test/lib/server/services/pdpBeamTypes/index.js b/test/lib/server/services/pdpBeamTypes/index.js new file mode 100644 index 0000000000..c2b0111eaf --- /dev/null +++ b/test/lib/server/services/pdpBeamTypes/index.js @@ -0,0 +1,18 @@ +/** + * @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. + */ + +const getAllPdpBeamTypes = require('./getAllPdpBeamTypes.test.js'); + +module.exports = () => { + describe('getAllPdpBeamTypes', getAllPdpBeamTypes); +}; From 82a202d1426de192d68f00ffbc1f53c640699f1e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 14:11:09 +0200 Subject: [PATCH 18/21] Add runs endpoint option to filter by pdpBeamType --- lib/domain/dtos/filters/RunFilterDto.js | 1 + lib/usecases/run/GetAllRunsUseCase.js | 5 ++ test/api/runs.test.js | 66 ++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 726ab4220f..39cc5a4418 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -41,6 +41,7 @@ exports.RunFilterDto = Joi.object({ 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', })), beamTypes: BeamTypesDto, + pdpBeamTypes: CustomJoi.stringArray().items(Joi.string().trim().min(2).max(20)), runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index 24d7131023..3014a8e548 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -85,6 +85,7 @@ class GetAllRunsUseCase { detectorsQcNotBadFraction, beamModes, beamTypes, + pdpBeamTypes, } = filter; if (runNumbers) { @@ -120,6 +121,10 @@ class GetAllRunsUseCase { filteringQueryBuilder.where('lhcBeamMode').oneOf(...beamModes); } + if (pdpBeamTypes) { + filteringQueryBuilder.where('pdpBeamType').oneOf(...pdpBeamTypes); + } + if (beamTypes) { filteringQueryBuilder.include({ association: 'lhcFill', diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 85945ebb06..51d0de47b4 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -196,6 +196,52 @@ module.exports = () => { expect(error.detail).to.equal(`Beam type must look like "PROTON - PROTON", "NE10 - NE10", etc.`); }); + it('should successfully filter runs with pdpBeamType', async () => { + const pdpBeamType = 'pp'; + const response = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamType}`); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf(6); + expect(runs.every(({ pdpBeamType: type }) => type === pdpBeamType)).to.be.true; + }); + + it('should successfully filter runs with multiple pdpBeamTypes', async () => { + const pdpBeamTypes = 'pp,PbPb'; + const response = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamTypes}`); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf(10); + expect(runs.every(({ pdpBeamType: type }) => pdpBeamTypes.includes(type))).to.be.true; + }); + + it('should return 400 if pdpBeamTypes filter has the incorrect format', async () => { + const pdpBeamTypes = 'S'; // Too short + const response = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamTypes}`); + + expect(response.status).to.equal(400); + + let { errors: [error] } = response.body; + + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal(`"query.filter.pdpBeamTypes[0]" length must be at least 2 characters long`); + + const pdpBeamTypesLong = 'This is definitely not a pdp beam type'; // Too short + const responseLong = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamTypesLong}`); + + expect(responseLong.status).to.equal(400); + + ({ errors: [error] } = responseLong.body); + + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal(`"query.filter.pdpBeamTypes[0]" length must be less than or equal to 20 characters long`); + }); + it('should return 400 if beamModes filter has the incorrect format', async () => { const beamModeString = '*THERE\'S NON LETTERS IN HERE'; const response = await request(server).get(`/api/runs?filter[beamModes]=${beamModeString}`); @@ -727,7 +773,6 @@ module.exports = () => { }); }); - describe('GET /api/runs/beamModes', () => { it('should successfully return status 200 and list of beam modes', async () => { const { body } = await request(server) @@ -740,6 +785,25 @@ module.exports = () => { expect(body.data[0].name).to.equal('STABLE BEAMS'); }); }); + + describe('GET /api/runs/pdpBeamTypes', () => { + it('should successfully return status 200 and list of pdp beam types', async () => { + const { body } = await request(server) + .get('/api/runs/pdpBeamTypes') + .expect(200); + + expect(body.data).to.be.an('array'); + expect(body.data).to.have.lengthOf(5); + expect(body.data).to.deep.equal([ + { name: 'pp' }, + { name: 'PbPb' }, + { name: 'technical' }, + { name: 'cosmic' }, + { name: 'OO' }, + ]); + }); + }); + describe('GET /api/runs/reasonTypes', () => { it('should successfully return status 200 and list of reason types', async () => { const { body } = await request(server) From d229f01c6257aab07b0f9b6e0e74ab11951a9a87 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 3 Aug 2026 13:56:27 +0200 Subject: [PATCH 19/21] Reuse beamtype dto for pdpbeamtype as well --- lib/domain/dtos/common/BeamTypeDto.js | 18 ++++++++++++++---- lib/domain/dtos/filters/RunFilterDto.js | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index 231c5b3350..706b0dbfd4 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -14,6 +14,16 @@ const Joi = require('joi'); const { CustomJoi } = require('../CustomJoi.js'); +/** + * Used by Bookkeeping-LHC Plugin to send data as per LHC + */ +const DASH_SEPARATED_BEAM_TYPE_PATTERN = /^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/; + +/** + * User Input format declared by user at deployment of environment + */ +const COMPACT_BEAM_TYPE_PATTERN = /^[A-Za-z]{2,15}$/; + /** * @typedef {string[]} BeamTypesDto * @description An array of beam types, each represented as a string. @@ -34,12 +44,12 @@ const { CustomJoi } = require('../CustomJoi.js'); exports.BeamTypesDto = CustomJoi.stringArray() .items(Joi.string() .trim() - .min(3) + .min(2) .max(15) - .pattern(/^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/) + .pattern(new RegExp(`(?:${DASH_SEPARATED_BEAM_TYPE_PATTERN.source})|(?:${COMPACT_BEAM_TYPE_PATTERN.source})`)) .messages({ 'string.base': 'Beam type must be a string', - 'string.min': 'Beam type must be at least 3 characters long', + '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.', + 'string.pattern.base': 'Beam type must look like "PROTON - PROTON", "NE10 - NE10", "pp", "pPb", "NeNe", "OO", etc.', })); diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 39cc5a4418..aec9f669b7 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -41,7 +41,7 @@ exports.RunFilterDto = Joi.object({ 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', })), beamTypes: BeamTypesDto, - pdpBeamTypes: CustomJoi.stringArray().items(Joi.string().trim().min(2).max(20)), + pdpBeamTypes: BeamTypesDto, runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', From 1600a2dab880350fcb2e5aa331a12c1bd3a77dae Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 3 Aug 2026 14:06:27 +0200 Subject: [PATCH 20/21] Split the DTOs to provide easier maintainability --- lib/domain/dtos/common/BeamTypeDto.js | 48 ++++++++++++++++++++----- lib/domain/dtos/filters/RunFilterDto.js | 4 +-- test/api/runs.test.js | 6 ++-- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index 706b0dbfd4..5aaf77926d 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -17,17 +17,22 @@ const { CustomJoi } = require('../CustomJoi.js'); /** * Used by Bookkeeping-LHC Plugin to send data as per LHC */ -const DASH_SEPARATED_BEAM_TYPE_PATTERN = /^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/; +const DASH_SEPARATED_BEAM_TYPE_PATTERN = /^[A-Za-z0-9]{1,6} ?- ?[A-Za-z0-9]{1,6}$/; /** * User Input format declared by user at deployment of environment */ const COMPACT_BEAM_TYPE_PATTERN = /^[A-Za-z]{2,15}$/; +const BEAM_TYPE_MIN_LENGTH = 5; +const BEAM_TYPE_MAX_LENGTH = 15; +const PDP_BEAM_TYPE_MIN_LENGTH = 2; +const PDP_BEAM_TYPE_MAX_LENGTH = 10; + /** * @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 3 characters and a maximum length of 15 characters. + * 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: @@ -36,7 +41,14 @@ const COMPACT_BEAM_TYPE_PATTERN = /^[A-Za-z]{2,15}$/; * "O8 - O8" * "PB82 - PB82" * "PROTON - O8" - * "PROTON - PROTON" + * + * RUN3 has the following compact beam types: + * "pp" + * "pPb" + * "NeNe" + * "OO" + * "cosmic" + * "technical" * * @example * const beamTypes = ["PROTON - PROTON", "NE10 - NE10"]; @@ -44,12 +56,30 @@ const COMPACT_BEAM_TYPE_PATTERN = /^[A-Za-z]{2,15}$/; exports.BeamTypesDto = CustomJoi.stringArray() .items(Joi.string() .trim() - .min(2) - .max(15) - .pattern(new RegExp(`(?:${DASH_SEPARATED_BEAM_TYPE_PATTERN.source})|(?:${COMPACT_BEAM_TYPE_PATTERN.source})`)) + .min(BEAM_TYPE_MIN_LENGTH) + .max(BEAM_TYPE_MAX_LENGTH) + .pattern(DASH_SEPARATED_BEAM_TYPE_PATTERN) .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", "pp", "pPb", "NeNe", "OO", etc.', + 'string.min': `Beam type must be at least ${BEAM_TYPE_MIN_LENGTH} characters long`, + 'string.max': `Beam type must be at most ${BEAM_TYPE_MAX_LENGTH} characters long`, + 'string.pattern.base': 'Beam type must look like "PROTON - PROTON", "NE10 - NE10", etc.', + })); + +/** + * @typedef {string[]} PdpBeamTypesDto + * @description An array of compact PDP beam types represented as strings. + * Values can include examples like "pp", "pPb", "NeNe", "OO", "cosmic", and "technical". + */ +exports.PdpBeamTypesDto = CustomJoi.stringArray() + .items(Joi.string() + .trim() + .min(PDP_BEAM_TYPE_MIN_LENGTH) + .max(PDP_BEAM_TYPE_MAX_LENGTH) + .pattern(COMPACT_BEAM_TYPE_PATTERN) + .messages({ + 'string.base': 'PDP beam type must be a string', + 'string.min': `PDP beam type must be at least ${PDP_BEAM_TYPE_MIN_LENGTH} characters long`, + 'string.max': `PDP beam type must be at most ${PDP_BEAM_TYPE_MAX_LENGTH} characters long`, + 'string.pattern.base': 'PDP beam type must look like "pp", "pPb", "NeNe", "OO", "cosmic", "technical", etc.', })); diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index aec9f669b7..3745cdc7a8 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -12,7 +12,7 @@ */ const Joi = require('joi'); const { CustomJoi } = require('../CustomJoi.js'); -const { BeamTypesDto } = require('../common/BeamTypeDto.js'); +const { BeamTypesDto, PdpBeamTypesDto } = require('../common/BeamTypeDto.js'); const { FromToFilterDto } = require('./FromToFilterDto.js'); const { RUN_QUALITIES } = require('../../enums/RunQualities.js'); const { IntegerComparisonDto, FloatComparisonDto } = require('./NumericalComparisonDto.js'); @@ -41,7 +41,7 @@ exports.RunFilterDto = Joi.object({ 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', })), beamTypes: BeamTypesDto, - pdpBeamTypes: BeamTypesDto, + pdpBeamTypes: PdpBeamTypesDto, runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 51d0de47b4..f4e178bc17 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -229,9 +229,9 @@ module.exports = () => { let { errors: [error] } = response.body; expect(error.title).to.equal('Invalid Attribute'); - expect(error.detail).to.equal(`"query.filter.pdpBeamTypes[0]" length must be at least 2 characters long`); + expect(error.detail).to.equal(`PDP beam type must be at least 2 characters long`); - const pdpBeamTypesLong = 'This is definitely not a pdp beam type'; // Too short + const pdpBeamTypesLong = 'This is definitely not a pdp beam type'; // Too long const responseLong = await request(server).get(`/api/runs?filter[pdpBeamTypes]=${pdpBeamTypesLong}`); expect(responseLong.status).to.equal(400); @@ -239,7 +239,7 @@ module.exports = () => { ({ errors: [error] } = responseLong.body); expect(error.title).to.equal('Invalid Attribute'); - expect(error.detail).to.equal(`"query.filter.pdpBeamTypes[0]" length must be less than or equal to 20 characters long`); + expect(error.detail).to.equal(`PDP beam type must be at most 10 characters long`); }); it('should return 400 if beamModes filter has the incorrect format', async () => { From 26648290a0aa50af73f8166f24730ecd255560b8 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 3 Aug 2026 15:08:15 +0200 Subject: [PATCH 21/21] Fix min limit of beam type --- lib/domain/dtos/common/BeamTypeDto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index 5aaf77926d..0fc097e97e 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -24,7 +24,7 @@ const DASH_SEPARATED_BEAM_TYPE_PATTERN = /^[A-Za-z0-9]{1,6} ?- ?[A-Za-z0-9]{1,6} */ const COMPACT_BEAM_TYPE_PATTERN = /^[A-Za-z]{2,15}$/; -const BEAM_TYPE_MIN_LENGTH = 5; +const BEAM_TYPE_MIN_LENGTH = 3; const BEAM_TYPE_MAX_LENGTH = 15; const PDP_BEAM_TYPE_MIN_LENGTH = 2; const PDP_BEAM_TYPE_MAX_LENGTH = 10;