From 17586546252ffb1e83093167f8f8344babc7e98e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 31 Jul 2026 14:10:35 +0200 Subject: [PATCH 1/2] 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 2/2] 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)