From 618daa19a6a4468557a5daf57b58d179a98a605d Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 16:59:05 +0100 Subject: [PATCH 01/11] [O2B-1508] Frontend Beams type filter working. --- .../Filters/LhcFillsFilter/beamsTypeFilter.js | 25 +++++++++++++++++++ lib/public/domain/enums/BeamType.js | 19 ++++++++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 2 ++ .../Overview/LhcFillsOverviewModel.js | 3 +++ 4 files changed, 49 insertions(+) create mode 100644 lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js create mode 100644 lib/public/domain/enums/BeamType.js diff --git a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js new file mode 100644 index 0000000000..070b37f4ea --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js @@ -0,0 +1,25 @@ +/** + * @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. + */ + +import { checkboxes } from '../common/filters/checkboxFilter.js'; + +/** + * Renders a list of checkboxes that lets the user look for beam types + * + * @param {SelectionFilterModel} selectionFilterModel selectionFilterModel + * @return {Component} the filter + */ +export const beamsTypeFilter = (selectionFilterModel) => checkboxes( + selectionFilterModel._selectionModel, + { selector: 'beams-types' }, +); diff --git a/lib/public/domain/enums/BeamType.js b/lib/public/domain/enums/BeamType.js new file mode 100644 index 0000000000..99fd2bb35c --- /dev/null +++ b/lib/public/domain/enums/BeamType.js @@ -0,0 +1,19 @@ +/** + * @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 BeamType = Object.freeze({ + PROTON_PROTON: 'Proton-Proton', + LEAD_LEAD: 'Lead-Lead', +}); + +export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 1a0cd8aa9d..897b407d13 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -27,6 +27,7 @@ import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFills import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fillNumberFilter.js'; import { beamDurationFilter } from '../../../components/Filters/LhcFillsFilter/beamDurationFilter.js'; import { runDurationFilter } from '../../../components/Filters/LhcFillsFilter/runDurationFilter.js'; +import { beamsTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamsTypeFilter.js'; /** * List of active columns for a lhc fills table @@ -170,6 +171,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), + filter: (lhcFillModel) => beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 9837d58473..ab90525178 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,6 +17,8 @@ import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilte import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; +import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; +import { BEAM_TYPES } from '../../../domain/enums/BeamType.js'; const defaultBeamDurationOperator = '='; const defaultRunDurationOperator = '='; @@ -40,6 +42,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { beamDuration: new RawTextFilterModel(), runDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), + beamsType: new SelectionFilterModel({ availableOptions: BEAM_TYPES.map((type) => ({ value: type })) }), }); this._beamDurationOperator = defaultBeamDurationOperator; From 870124689d0b24b62c98c88c99af4bb76db6f11a Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 17:28:20 +0100 Subject: [PATCH 02/11] [O2B-1508] Basic backend UseCase works --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 1 + lib/public/domain/enums/BeamType.js | 4 ++-- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 7c5d6853f7..2942844758 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -27,4 +27,5 @@ exports.LhcFillsFilterDto = Joi.object({ 'any.invalid': '{{#message}}', }), runDurationOperator: Joi.string().trim().min(1).max(2), + beamsType: Joi.string(), }); diff --git a/lib/public/domain/enums/BeamType.js b/lib/public/domain/enums/BeamType.js index 99fd2bb35c..c2266fda6f 100644 --- a/lib/public/domain/enums/BeamType.js +++ b/lib/public/domain/enums/BeamType.js @@ -12,8 +12,8 @@ */ export const BeamType = Object.freeze({ - PROTON_PROTON: 'Proton-Proton', - LEAD_LEAD: 'Lead-Lead', + PROTON_PROTON: 'PROTON-PROTON', + LEAD_LEAD: 'LEAD-LEAD', }); export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index e961548817..155861806c 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -47,7 +47,7 @@ class GetAllLhcFillsUseCase { let associatedStatisticsRequired = false; if (filter) { - const { hasStableBeams, fillNumbers, beamDurationOperator, beamDuration, runDurationOperator, runDuration } = filter; + const { hasStableBeams, fillNumbers, beamDurationOperator, beamDuration, runDurationOperator, runDuration, beamsType } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -77,6 +77,12 @@ class GetAllLhcFillsUseCase { beamDuration === 0 ? queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, null) : queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, beamDuration); } + + // Beams type. + if (beamsType) { + const beamTypes = beamsType.split(','); + queryBuilder.where('beamType').oneOfSubstrings(beamTypes); + } } const { count, rows } = await TransactionHelper.provide(async () => { From e64d28f6d3e13c8abc337dfaa7d67c94bbb451ec Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 10 Dec 2025 08:59:48 +0100 Subject: [PATCH 03/11] [O2B-1508] Load possible beamstypes for the beam_type filter from the backend --- lib/domain/dtos/GetAllBeamsTypesDto.js | 28 +++++++++ lib/domain/dtos/index.js | 2 + lib/{public => }/domain/enums/BeamType.js | 2 +- .../services/beamsTypes/beamsTypesProvider.js | 30 +++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 3 +- .../Overview/LhcFillsOverviewModel.js | 21 ++++++- .../controllers/beamsTypes.controller.js | 62 +++++++++++++++++++ lib/server/controllers/index.js | 2 + lib/server/routers/beamsTypes.router.js | 20 ++++++ lib/server/routers/index.js | 2 + .../beamsType/GetAllBeamsTypesUseCase.js | 41 ++++++++++++ lib/usecases/beamsType/index.js | 17 +++++ lib/usecases/index.js | 2 + 13 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 lib/domain/dtos/GetAllBeamsTypesDto.js rename lib/{public => }/domain/enums/BeamType.js (95%) create mode 100644 lib/public/services/beamsTypes/beamsTypesProvider.js create mode 100644 lib/server/controllers/beamsTypes.controller.js create mode 100644 lib/server/routers/beamsTypes.router.js create mode 100644 lib/usecases/beamsType/GetAllBeamsTypesUseCase.js create mode 100644 lib/usecases/beamsType/index.js diff --git a/lib/domain/dtos/GetAllBeamsTypesDto.js b/lib/domain/dtos/GetAllBeamsTypesDto.js new file mode 100644 index 0000000000..c4e78bfec5 --- /dev/null +++ b/lib/domain/dtos/GetAllBeamsTypesDto.js @@ -0,0 +1,28 @@ +/** + * @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 Joi = require('joi'); +const PaginationDto = require('./PaginationDto'); + +const QueryDto = Joi.object({ + page: PaginationDto, + token: Joi.string(), +}); + +const GetAllBeamsTypesDto = Joi.object({ + body: Joi.object({}), + params: Joi.object({}), + query: QueryDto, +}); + +module.exports = GetAllBeamsTypesDto; diff --git a/lib/domain/dtos/index.js b/lib/domain/dtos/index.js index 01eb348934..d1bb88492f 100644 --- a/lib/domain/dtos/index.js +++ b/lib/domain/dtos/index.js @@ -18,6 +18,7 @@ const CreateLhcFillDto = require('./CreateLhcFillDto'); const CreateLogDto = require('./CreateLogDto'); const CreateTagDto = require('./CreateTagDto'); const EntityIdDto = require('./EntityIdDto'); +const GetAllBeamsTypesDto = require('./GetAllBeamsTypesDto.js'); const GetAllEnvironmentsDto = require('./GetAllEnvironmentsDto'); const GetAllLhcFillsDto = require('./GetAllLhcFillsDto'); const GetAllLogAttachmentsDto = require('./GetAllLogAttachmentsDto'); @@ -56,6 +57,7 @@ module.exports = { CreateTagDto, EndRunDto, EntityIdDto, + GetAllBeamsTypesDto, GetAllEnvironmentsDto, GetAllLhcFillsDto, GetAllLogAttachmentsDto, diff --git a/lib/public/domain/enums/BeamType.js b/lib/domain/enums/BeamType.js similarity index 95% rename from lib/public/domain/enums/BeamType.js rename to lib/domain/enums/BeamType.js index c2266fda6f..9b7a1285b1 100644 --- a/lib/public/domain/enums/BeamType.js +++ b/lib/domain/enums/BeamType.js @@ -13,7 +13,7 @@ export const BeamType = Object.freeze({ PROTON_PROTON: 'PROTON-PROTON', - LEAD_LEAD: 'LEAD-LEAD', + LEAD_LEAD: 'PB82-PB82', }); export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/public/services/beamsTypes/beamsTypesProvider.js b/lib/public/services/beamsTypes/beamsTypesProvider.js new file mode 100644 index 0000000000..20a688d0a2 --- /dev/null +++ b/lib/public/services/beamsTypes/beamsTypesProvider.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. + */ + +import { getRemoteData } from '../../utilities/fetch/getRemoteData.js'; +import { RemoteDataProvider } from '../RemoteDataProvider.js'; + +/** + * Service class to fetch beams types from the backend + */ +export class BeamsTypesProvider extends RemoteDataProvider { + /** + * @inheritDoc + */ + async getRemoteData() { + const { data } = await getRemoteData('/api/beamsTypes'); + return data; + } +} + +export const beamsTypesProvider = new BeamsTypesProvider(); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 897b407d13..1b1ceca149 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -171,7 +171,8 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), - filter: (lhcFillModel) => beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), + filter: (lhcFillModel) => lhcFillModel.getBeamsTypes().length === 0 ? + undefined : beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index ab90525178..3734968576 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -18,7 +18,7 @@ import { RawTextFilterModel } from '../../../components/Filters/common/filters/R import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; -import { BEAM_TYPES } from '../../../domain/enums/BeamType.js'; +import { beamsTypesProvider } from '../../../services/beamsTypes/beamsTypesProvider.js'; const defaultBeamDurationOperator = '='; const defaultRunDurationOperator = '='; @@ -37,12 +37,22 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); + this._beamsTypes = []; + this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), beamDuration: new RawTextFilterModel(), runDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), - beamsType: new SelectionFilterModel({ availableOptions: BEAM_TYPES.map((type) => ({ value: type })) }), + }); + + beamsTypesProvider.items$.observe(() => { + beamsTypesProvider.items$.getCurrent().apply({ + Success: (types) => { + this._beamsTypes = types.map((type) => ({ value: type })); + this._filteringModel.put('beamsType', new SelectionFilterModel({ availableOptions: this._beamsTypes })); + }, + }); }); this._beamDurationOperator = defaultBeamDurationOperator; @@ -84,6 +94,13 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return buildUrl('/api/lhcFills', params); } + /** + * Getter + */ + getBeamsTypes() { + return this._beamsTypes; + } + /** * Setter function for runDurationOperator */ diff --git a/lib/server/controllers/beamsTypes.controller.js b/lib/server/controllers/beamsTypes.controller.js new file mode 100644 index 0000000000..e67b21718c --- /dev/null +++ b/lib/server/controllers/beamsTypes.controller.js @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * 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 { + beamsType: { + GetAllBeamsTypesUseCase, + }, +} = require('../../usecases/index.js'); +const { + dtos: { + GetAllBeamsTypesDto, + }, +} = require('../../domain/index.js'); +const { dtoValidator } = require('../utilities/index.js'); +const { ApiConfig } = require('../../config/index.js'); + +/** + * Get all beams 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. + * @returns {undefined} + */ +const listBeamsTypes = async (request, response) => { + const value = await dtoValidator(GetAllBeamsTypesDto, request, response); + if (!value) { + return; + } + + const { count, beamsTypes } = await new GetAllBeamsTypesUseCase() + .execute(value); + + const { query: { page: { limit = ApiConfig.pagination.limit } = {} } } = value; + const totalPages = Math.ceil(count / limit); + + response.status(200).json({ + data: beamsTypes, + meta: { + page: { + pageCount: totalPages, + totalCount: count, + }, + }, + }); +}; + +module.exports = { + listBeamsTypes, +}; diff --git a/lib/server/controllers/index.js b/lib/server/controllers/index.js index 48184378b2..904dadaeb1 100644 --- a/lib/server/controllers/index.js +++ b/lib/server/controllers/index.js @@ -12,6 +12,7 @@ */ const AttachmentsController = require('./attachments.controller'); +const BeamsTypeController = require('./beamsTypes.controller.js'); const ConfigurationController = require('./configuration.controller.js'); const DetectorsController = require('./detectors.controller'); const EnvironmentsController = require('./environments.controller'); @@ -26,6 +27,7 @@ const CtpTriggerCountersController = require('./ctpTriggerCounters.controller'); module.exports = { AttachmentsController, + BeamsTypeController, ConfigurationController, DetectorsController, EnvironmentsController, diff --git a/lib/server/routers/beamsTypes.router.js b/lib/server/routers/beamsTypes.router.js new file mode 100644 index 0000000000..a64aa6b4f8 --- /dev/null +++ b/lib/server/routers/beamsTypes.router.js @@ -0,0 +1,20 @@ +/** + * @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 { BeamsTypeController } = require('../controllers'); + +exports.beamsTypesRouter = { + path: '/beamsTypes', + controller: BeamsTypeController.listBeamsTypes, + method: 'get', +}; diff --git a/lib/server/routers/index.js b/lib/server/routers/index.js index cb83465446..a064f5a246 100644 --- a/lib/server/routers/index.js +++ b/lib/server/routers/index.js @@ -15,6 +15,7 @@ const { deepmerge, isPromise } = require('../../utilities'); const attachmentRoute = require('./attachments.router'); const { configurationRouter } = require('./configuration.router.js'); +const { beamsTypesRouter } = require('./beamsTypes.router.js') const detectorsRoute = require('./detectors.router'); const { dplProcessRouter } = require('./dplProcess.router.js'); const environmentRoute = require('./environments.router'); @@ -40,6 +41,7 @@ const { ctpTriggerCountersRouter } = require('./ctpTriggerCounters.router.js'); const routes = [ attachmentRoute, + beamsTypesRouter, configurationRouter, detectorsRoute, dataPassesRouter, diff --git a/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js new file mode 100644 index 0000000000..51a8ddc6cc --- /dev/null +++ b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js @@ -0,0 +1,41 @@ +/** + * @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 { ApiConfig } = require('../../config/index.js'); +const { BEAM_TYPES } = require('../../domain/enums/BeamType.js'); + +/** + * GetAllBeamsTypesUseCase + */ +class GetAllBeamsTypesUseCase { + /** + * Executes this use case. + * + * @param {Object} dto The GetAllBeamsTypes DTO which contains all request data. + * @returns {Promise} Promise object represents the result of this use case. + */ + async execute(dto = {}) { + // DTO data to be used if Database data is desired. + const { query = {} } = dto; + const { page = {} } = query; + const { limit = ApiConfig.pagination.limit, offset = 0 } = page; + + const beamsTypes = BEAM_TYPES; + return { + count: beamsTypes.length, + beamsTypes, + }; + } +} + +module.exports = GetAllBeamsTypesUseCase; diff --git a/lib/usecases/beamsType/index.js b/lib/usecases/beamsType/index.js new file mode 100644 index 0000000000..5a60a1e65f --- /dev/null +++ b/lib/usecases/beamsType/index.js @@ -0,0 +1,17 @@ +/** + * @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 GetAllBeamsTypesUseCase = require('./GetAllBeamsTypesUseCase.js'); + +module.exports = { + GetAllBeamsTypesUseCase, +}; diff --git a/lib/usecases/index.js b/lib/usecases/index.js index 2dfa7807e5..413ffe4791 100644 --- a/lib/usecases/index.js +++ b/lib/usecases/index.js @@ -12,6 +12,7 @@ */ const attachment = require('./attachment'); +const beamsType = require('./beamsType'); const environment = require('./environment'); const flp = require('./flp'); const lhcFill = require('./lhcFill'); @@ -24,6 +25,7 @@ const tag = require('./tag'); module.exports = { attachment, + beamsType, environment, flp, lhcFill, From 19269b8a3625bffd38a20402db97fe17d41a26b9 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 11 Dec 2025 17:08:44 +0100 Subject: [PATCH 04/11] [O2B-1508] Beams types filter works with data from backend. --- .../repositories/LhcFillRepository.js | 12 ++++ lib/domain/enums/BeamType.js | 19 ------ .../LhcFillsFilter/BeamsTypeFilterModel.js | 59 +++++++++++++++++++ .../Filters/LhcFillsFilter/beamsTypeFilter.js | 11 ++-- .../ActiveColumns/lhcFillsActiveColumns.js | 3 +- .../Overview/LhcFillsOverviewModel.js | 13 +--- .../beamsType/GetAllBeamsTypesUseCase.js | 17 ++---- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 2 +- 8 files changed, 86 insertions(+), 50 deletions(-) delete mode 100644 lib/domain/enums/BeamType.js create mode 100644 lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js diff --git a/lib/database/repositories/LhcFillRepository.js b/lib/database/repositories/LhcFillRepository.js index 8ed50d16cb..c74f9ffdd4 100644 --- a/lib/database/repositories/LhcFillRepository.js +++ b/lib/database/repositories/LhcFillRepository.js @@ -34,6 +34,10 @@ const getFillNumbersWithStableBeamsEndedInPeriodQuery = (period) => ` AND stable_beams_start IS NOT NULL `; +const getLhcFillDistinctBeamTypesQuery = () => ` + SELECT DISTINCT beam_type FROM bookkeeping.lhc_fills +`; + /** * Sequelize implementation of the RunRepository. */ @@ -45,6 +49,14 @@ class LhcFillRepository extends Repository { super(LhcFill); } + /** + * Return the list of LHC fills distict beam types. + * @returns {Promise} + */ + async getLhcFillDistinctBeamTypes() { + return await sequelize.query(getLhcFillDistinctBeamTypesQuery(), { type: QueryTypes.SELECT, raw: true }); + } + /** * Return the list of LHC fills numbers for fills with stable beams that ended in the given period * diff --git a/lib/domain/enums/BeamType.js b/lib/domain/enums/BeamType.js deleted file mode 100644 index 9b7a1285b1..0000000000 --- a/lib/domain/enums/BeamType.js +++ /dev/null @@ -1,19 +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 BeamType = Object.freeze({ - PROTON_PROTON: 'PROTON-PROTON', - LEAD_LEAD: 'PB82-PB82', -}); - -export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js new file mode 100644 index 0000000000..389328b220 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js @@ -0,0 +1,59 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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. + */ + +import { beamsTypesProvider } from '../../../services/beamsTypes/beamsTypesProvider.js'; +import { SelectionModel } from '../../common/selection/SelectionModel.js'; + +/** + * Beam type filter model + */ +export class BeamsTypeFilterModel extends SelectionModel { + /** + * Constructor + */ + constructor() { + let beamTypes = []; + super({ availableOptions: beamTypes, + defaultSelection: [], + multiple: true, + allowEmpty: true }); + + beamsTypesProvider.items$.observe(() => { + beamsTypesProvider.items$.getCurrent().apply({ + Success: (types) => { + beamTypes = types.map((type) => ({ value: String(type.beam_type) })); + this.setAvailableOptions(beamTypes); + }, + }); + }); + } + + /** + * Get normalized selected option + */ + get normalized() { + return this.selected.join(','); + } + + /** + * Reset the filter to default values + * + * @return {void} + */ + resetDefaults() { + if (!this.isEmpty) { + this.reset(); + this.notify(); + } + } +} diff --git a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js index 070b37f4ea..94a5887d42 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js @@ -16,10 +16,11 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; /** * Renders a list of checkboxes that lets the user look for beam types * - * @param {SelectionFilterModel} selectionFilterModel selectionFilterModel + * @param {BeamsTypeFilterModel} beamsTypeFilterModel beamsTypeFilterModel * @return {Component} the filter */ -export const beamsTypeFilter = (selectionFilterModel) => checkboxes( - selectionFilterModel._selectionModel, - { selector: 'beams-types' }, -); +export const beamsTypeFilter = (beamsTypeFilterModel) => + checkboxes( + beamsTypeFilterModel, + { selector: 'beams-types' }, + ); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 1b1ceca149..897b407d13 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -171,8 +171,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), - filter: (lhcFillModel) => lhcFillModel.getBeamsTypes().length === 0 ? - undefined : beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), + filter: (lhcFillModel) => beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 3734968576..5689428c96 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,8 +17,7 @@ import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilte import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; -import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; -import { beamsTypesProvider } from '../../../services/beamsTypes/beamsTypesProvider.js'; +import { BeamsTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js'; const defaultBeamDurationOperator = '='; const defaultRunDurationOperator = '='; @@ -44,15 +43,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { beamDuration: new RawTextFilterModel(), runDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), - }); - - beamsTypesProvider.items$.observe(() => { - beamsTypesProvider.items$.getCurrent().apply({ - Success: (types) => { - this._beamsTypes = types.map((type) => ({ value: type })); - this._filteringModel.put('beamsType', new SelectionFilterModel({ availableOptions: this._beamsTypes })); - }, - }); + beamsType: new BeamsTypeFilterModel(), }); this._beamDurationOperator = defaultBeamDurationOperator; diff --git a/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js index 51a8ddc6cc..405e9b5f14 100644 --- a/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js +++ b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js @@ -11,8 +11,7 @@ * or submit itself to any jurisdiction. */ -const { ApiConfig } = require('../../config/index.js'); -const { BEAM_TYPES } = require('../../domain/enums/BeamType.js'); +const LhcFillRepository = require('../../database/repositories/LhcFillRepository.js'); /** * GetAllBeamsTypesUseCase @@ -21,19 +20,13 @@ class GetAllBeamsTypesUseCase { /** * Executes this use case. * - * @param {Object} dto The GetAllBeamsTypes DTO which contains all request data. * @returns {Promise} Promise object represents the result of this use case. */ - async execute(dto = {}) { - // DTO data to be used if Database data is desired. - const { query = {} } = dto; - const { page = {} } = query; - const { limit = ApiConfig.pagination.limit, offset = 0 } = page; - - const beamsTypes = BEAM_TYPES; + async execute() { + const result = await LhcFillRepository.getLhcFillDistinctBeamTypes(); return { - count: beamsTypes.length, - beamsTypes, + count: result.length, + beamsTypes: result, }; } } diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 155861806c..5a6d2dfa80 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -81,7 +81,7 @@ class GetAllLhcFillsUseCase { // Beams type. if (beamsType) { const beamTypes = beamsType.split(','); - queryBuilder.where('beamType').oneOfSubstrings(beamTypes); + queryBuilder.where('beamType').oneOf(beamTypes); } } From 18ea3868a260f345e1ec0afc613b9c5ffefd78b2 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 16 Dec 2025 13:33:58 +0100 Subject: [PATCH 05/11] [O2B-1508] Handle beamtype 'null'. Added tests --- lib/database/utilities/QueryBuilder.js | 35 +++++++++++++++- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 10 ++++- .../lhcFill/GetAllLhcFillsUseCase.test.js | 40 +++++++++++++++++++ test/public/lhcFills/overview.test.js | 2 + 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/lib/database/utilities/QueryBuilder.js b/lib/database/utilities/QueryBuilder.js index 2f522902e8..ab16d35ccf 100644 --- a/lib/database/utilities/QueryBuilder.js +++ b/lib/database/utilities/QueryBuilder.js @@ -88,7 +88,7 @@ class WhereQueryBuilder { } /** - * Sets an **OR** match filter using the provided values. + * Sets an **IN** match filter using the provided values. * * @param {...any} values The required values. * @returns {QueryBuilder} The current QueryBuilder instance. @@ -104,6 +104,39 @@ class WhereQueryBuilder { return this._op(operation); } + /** + * Sets an **OR** match filter using the provided values. + * Adds an **OR NULL** filter. + * If the spread returns empty array the filter becomes an **IS NULL** filter (**OR** is not valid anymore) + * + * @param {...any} values The required values. + * @returns {QueryBuilder} The current QueryBuilder instance. + */ + oneOfOrNull(...values) { + let operation; + if (this.notFlag) { + operation = values[0]?.length === 0 ? { + [Op.not]: null, + } : { + [Op.or]: { + [Op.notIn]: values, + [Op.not]: null, + }, + }; + } else { + operation = values[0]?.length === 0 ? { + [Op.is]: null, + } : { + [Op.or]: { + [Op.in]: values, + [Op.is]: null, + }, + }; + } + + return this._op(operation); + } + /** * Set a max range limit using the provided value * diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 5a6d2dfa80..958f07cbe2 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -80,8 +80,14 @@ class GetAllLhcFillsUseCase { // Beams type. if (beamsType) { - const beamTypes = beamsType.split(','); - queryBuilder.where('beamType').oneOf(beamTypes); + let beamTypes = beamsType.split(','); + // Check if 'null' is included in the request + if (beamTypes.find((type) => type.trim() === 'null') !== undefined) { + beamTypes = beamTypes.filter((type) => type.trim() !== 'null'); + queryBuilder.where('beamType').oneOfOrNull(beamTypes); + } else { + queryBuilder.where('beamType').oneOf(beamTypes); + } } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 6c317588ff..c9284001f5 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -220,4 +220,44 @@ module.exports = () => { expect(lhcFill.statistics.runsCoverage).greaterThan(23459) }); }) + + 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: { beamsType: beamTypes.join(',') } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(4) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.beamType).oneOf(beamTypes) + }); + }) + + it('should only contain specified beam types, OR NULL, {p-p, PROTON-PROTON, Pb-Pb, null}', async () => { + let beamTypes = ['p-p', ' PROTON-PROTON', 'Pb-Pb', 'null'] + + getAllLhcFillsDto.query = { filter: { beamsType: beamTypes.join(',') } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(5) + + const nullIndex = beamTypes.findIndex((value) => value ==='null') + beamTypes[nullIndex] = null; + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.beamType).oneOf(beamTypes) + }); + }) + + it('should only contain specified beam type, IS NULL, {null}', async () => { + const beamTypes = ['null'] + + getAllLhcFillsDto.query = { filter: { beamsType: beamTypes.join(',') } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.beamType).oneOf([null]) + }); + }) }; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index c67a1ebb3c..adea7f85fa 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -273,6 +273,7 @@ module.exports = () => { const filterSBDurationPlaceholderExpect = {selector: '.beam-duration-filter', value: 'e.g 16:14:15 (HH:MM:SS)'} const filterRunDurationExpect = {selector: 'div.flex-row:nth-child(4) > div:nth-child(1)', value: 'Total runs duration'} const filterRunDurationPlaceholderExpect = {selector: '.run-duration-filter', value: 'e.g 16:14:15 (HH:MM:SS)'} + const filterBeamTypeExpect = {selector: 'div.flex-row:nth-child(5) > div:nth-child(1)', value: 'Beam Type'} await goToPage(page, 'lhc-fill-overview'); @@ -284,6 +285,7 @@ module.exports = () => { await expectAttributeValue(page, filterSBDurationPlaceholderExpect.selector, 'placeholder', filterSBDurationPlaceholderExpect.value); await expectInnerText(page, filterRunDurationExpect.selector, filterRunDurationExpect.value); await expectAttributeValue(page, filterRunDurationPlaceholderExpect.selector, 'placeholder', filterRunDurationPlaceholderExpect.value); + await expectInnerText(page, filterBeamTypeExpect.selector, filterBeamTypeExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { From 2aec67f658de1a6daf0cecc6fa084e04acac6d72 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 15:41:55 +0100 Subject: [PATCH 06/11] [O2B-1508] Tests --- test/api/lhcFills.test.js | 50 +++++++++++++++++++++++++++ test/public/lhcFills/overview.test.js | 12 +++++++ 2 files changed, 62 insertions(+) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index a9f4ace6b4..1fcdc820ce 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -469,6 +469,56 @@ module.exports = () => { done(); }); }); + + it('should return 200 and an LHCFill array for beam types filter, correct', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(1); + expect(res.body.data[0].fillNumber).to.equal(3); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for beam types filter, multiple correct', (done) => { + request(server) + .get('/api/lhcFills?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,p-p,p-Pb') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(5); + expect(res.body.data[0].fillNumber).to.equal(6); + + done(); + }); + }); + + it('should return 400 for beam types filter, one wrong', (done) => { + request(server) + .get('/api/lhcFills?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); }); describe('POST /api/lhcFills', () => { it('should return 201 if valid data is provided', async () => { diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 049e5d25eb..b2868cd66e 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -335,4 +335,16 @@ module.exports = () => { await fillInput(page, filterRunDurationOperand, '00:00:00', ['change']); await waitForTableLength(page, 5); }); + + it('should successfully apply beam types filter', async () => { + const filterBeamTypeP_Pb = '#beams-types-checkbox-p-Pb'; + const filterBeamTypePb_Pb = '#beams-types-checkbox-Pb-Pb'; + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + await openFilteringPanel(page); + await pressElement(page, filterBeamTypeP_Pb); + await waitForTableLength(page, 1); + await pressElement(page, filterBeamTypePb_Pb); + await waitForTableLength(page, 2); + }); }; From 516bbbc3bb86d8ee43d18565c0293484a667d33d Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 16:48:20 +0100 Subject: [PATCH 07/11] [O2B-1508] fixed test --- test/api/lhcFills.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 2c43a607f0..20ae6ccb1b 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -539,7 +539,7 @@ module.exports = () => { it('should return 200 and an LHCFill array for beam types filter, multiple correct', (done) => { request(server) - .get('/api/lhcFills?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,p-p,p-Pb') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,p-p,p-Pb') .expect(200) .end((err, res) => { if (err) { @@ -547,8 +547,8 @@ module.exports = () => { return; } - expect(res.body.data).to.have.lengthOf(5); - expect(res.body.data[0].fillNumber).to.equal(6); + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data[0].fillNumber).to.equal(4); done(); }); From c6e68674af97f93c247615790077290f7d83176b Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 16:50:45 +0100 Subject: [PATCH 08/11] [O2B-1508] Linting, missing semicolon --- lib/server/routers/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server/routers/index.js b/lib/server/routers/index.js index a064f5a246..4c4774a4af 100644 --- a/lib/server/routers/index.js +++ b/lib/server/routers/index.js @@ -15,7 +15,7 @@ const { deepmerge, isPromise } = require('../../utilities'); const attachmentRoute = require('./attachments.router'); const { configurationRouter } = require('./configuration.router.js'); -const { beamsTypesRouter } = require('./beamsTypes.router.js') +const { beamsTypesRouter } = require('./beamsTypes.router.js'); const detectorsRoute = require('./detectors.router'); const { dplProcessRouter } = require('./dplProcess.router.js'); const environmentRoute = require('./environments.router'); From 475c273e3cf8baac5f4d7f00cf36494ca27387ee Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 17:16:24 +0100 Subject: [PATCH 09/11] [O2B-1508] fixed api test --- test/api/lhcFills.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 20ae6ccb1b..432aa47c7b 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -554,17 +554,19 @@ module.exports = () => { }); }); - it('should return 400 for beam types filter, one wrong', (done) => { + // API accepts filters that do not exist, this is because it does not affect the results + it('should return 200 for beam types filter, one wrong', (done) => { request(server) - .get('/api/lhcFills?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') - .expect(400) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') + .expect(200) .end((err, res) => { if (err) { done(err); return; } - expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data[0].fillNumber).to.equal(4); done(); }); From e3014041c84825cc446a9a16bcd4a3a0e4374fc6 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:33:27 +0100 Subject: [PATCH 10/11] [O2B-1508] Refactor distinct beam types query in LhcFillRepository Replaces raw SQL query for fetching distinct beam types with a Sequelize-based implementation. --- lib/database/repositories/LhcFillRepository.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/database/repositories/LhcFillRepository.js b/lib/database/repositories/LhcFillRepository.js index c74f9ffdd4..48f61260d9 100644 --- a/lib/database/repositories/LhcFillRepository.js +++ b/lib/database/repositories/LhcFillRepository.js @@ -34,10 +34,6 @@ const getFillNumbersWithStableBeamsEndedInPeriodQuery = (period) => ` AND stable_beams_start IS NOT NULL `; -const getLhcFillDistinctBeamTypesQuery = () => ` - SELECT DISTINCT beam_type FROM bookkeeping.lhc_fills -`; - /** * Sequelize implementation of the RunRepository. */ @@ -54,7 +50,11 @@ class LhcFillRepository extends Repository { * @returns {Promise} */ async getLhcFillDistinctBeamTypes() { - return await sequelize.query(getLhcFillDistinctBeamTypesQuery(), { type: QueryTypes.SELECT, raw: true }); + return await LhcFill.findAll({ + attributes: [[sequelize.fn('DISTINCT', sequelize.col('beam_type')), 'beam_type']], + order: [['beam_type', 'ASC']], + raw: true, + }); } /** From 92c7f4fa1439e62f0337d88893b923858f2a0760 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:47:59 +0100 Subject: [PATCH 11/11] [O2B-1508] Remove support for 'null' beamType filtering Eliminated the ability to filter LHC fills by 'null' beamType values. Updates repository to exclude null beam types in distinct queries. Removed the oneOfOrNull method from QueryBuilder. Simplifies the beam type filtering in GetAllLhcFillsUseCase by removing the special handling for 'null' values. Adjusts related tests to match the new logic. --- .../repositories/LhcFillRepository.js | 7 +++- lib/database/utilities/QueryBuilder.js | 33 ------------------- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 10 ++---- .../lhcFill/GetAllLhcFillsUseCase.test.js | 19 ++--------- 4 files changed, 10 insertions(+), 59 deletions(-) diff --git a/lib/database/repositories/LhcFillRepository.js b/lib/database/repositories/LhcFillRepository.js index 48f61260d9..d4ab4ad314 100644 --- a/lib/database/repositories/LhcFillRepository.js +++ b/lib/database/repositories/LhcFillRepository.js @@ -15,7 +15,7 @@ const { models: { LhcFill }, sequelize } = require('../'); const Repository = require('./Repository'); const { timestampToMysql } = require('../../server/utilities/timestampToMysql.js'); -const { QueryTypes } = require('sequelize'); +const { QueryTypes, Op } = require('sequelize'); /** * Return the SQL query to use to fetch the list of LHC fills numbers for fills with stable beams that ended in the given period @@ -52,6 +52,11 @@ class LhcFillRepository extends Repository { async getLhcFillDistinctBeamTypes() { return await LhcFill.findAll({ attributes: [[sequelize.fn('DISTINCT', sequelize.col('beam_type')), 'beam_type']], + where: { + beamType: { + [Op.not]: null, + }, + }, order: [['beam_type', 'ASC']], raw: true, }); diff --git a/lib/database/utilities/QueryBuilder.js b/lib/database/utilities/QueryBuilder.js index 95d8841559..1f0a22fe0c 100644 --- a/lib/database/utilities/QueryBuilder.js +++ b/lib/database/utilities/QueryBuilder.js @@ -104,39 +104,6 @@ class WhereQueryBuilder { return this._op(operation); } - /** - * Sets an **OR** match filter using the provided values. - * Adds an **OR NULL** filter. - * If the spread returns empty array the filter becomes an **IS NULL** filter (**OR** is not valid anymore) - * - * @param {...any} values The required values. - * @returns {QueryBuilder} The current QueryBuilder instance. - */ - oneOfOrNull(...values) { - let operation; - if (this.notFlag) { - operation = values[0]?.length === 0 ? { - [Op.not]: null, - } : { - [Op.or]: { - [Op.notIn]: values, - [Op.not]: null, - }, - }; - } else { - operation = values[0]?.length === 0 ? { - [Op.is]: null, - } : { - [Op.or]: { - [Op.in]: values, - [Op.is]: null, - }, - }; - } - - return this._op(operation); - } - /** * Set a max range limit using the provided value * diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 2e610ac326..434a473033 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -88,14 +88,8 @@ class GetAllLhcFillsUseCase { // Beams type. if (beamsType) { - let beamTypes = beamsType.split(','); - // Check if 'null' is included in the request - if (beamTypes.find((type) => type.trim() === 'null') !== undefined) { - beamTypes = beamTypes.filter((type) => type.trim() !== 'null'); - queryBuilder.where('beamType').oneOfOrNull(beamTypes); - } else { - queryBuilder.where('beamType').oneOf(beamTypes); - } + const beamTypes = beamsType.split(','); + queryBuilder.where('beamType').oneOf(beamTypes); } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index a9b84b83ca..9b03fb887e 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -229,31 +229,16 @@ module.exports = () => { }); }) - it('should only contain specified beam types, OR NULL, {p-p, PROTON-PROTON, Pb-Pb, null}', async () => { + it('should only contain specified beam types, {p-p, PROTON-PROTON, Pb-Pb, null}', async () => { let beamTypes = ['p-p', ' PROTON-PROTON', 'Pb-Pb', 'null'] getAllLhcFillsDto.query = { filter: { beamsType: beamTypes.join(',') } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - expect(lhcFills).to.be.an('array').and.lengthOf(5) - - const nullIndex = beamTypes.findIndex((value) => value ==='null') - beamTypes[nullIndex] = null; + expect(lhcFills).to.be.an('array').and.lengthOf(4) lhcFills.forEach((lhcFill) => { expect(lhcFill.beamType).oneOf(beamTypes) }); }) - - it('should only contain specified beam type, IS NULL, {null}', async () => { - const beamTypes = ['null'] - - getAllLhcFillsDto.query = { filter: { beamsType: beamTypes.join(',') } }; - const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - - expect(lhcFills).to.be.an('array').and.lengthOf(1) - lhcFills.forEach((lhcFill) => { - expect(lhcFill.beamType).oneOf([null]) - }); - }) };