diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js index 231c5b3350..0fc097e97e 100644 --- a/lib/domain/dtos/common/BeamTypeDto.js +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -14,10 +14,25 @@ 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]{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 = 3; +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: @@ -26,7 +41,14 @@ const { CustomJoi } = require('../CustomJoi.js'); * "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"]; @@ -34,12 +56,30 @@ const { CustomJoi } = require('../CustomJoi.js'); exports.BeamTypesDto = CustomJoi.stringArray() .items(Joi.string() .trim() - .min(3) - .max(15) - .pattern(/^[A-Za-z0-9]+ ?- ?[A-Za-z0-9]+$/) + .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 3 characters long', - 'string.max': 'Beam type must be at most 15 characters long', + '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 726ab4220f..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,6 +41,7 @@ exports.RunFilterDto = Joi.object({ 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', })), beamTypes: 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/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/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..f4e178bc17 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(`PDP beam type must be at least 2 characters long`); + + 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); + + ({ errors: [error] } = responseLong.body); + + expect(error.title).to.equal('Invalid Attribute'); + 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 () => { 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) 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); +};