diff --git a/lib/database/adapters/LogAdapter.js b/lib/database/adapters/LogAdapter.js index 6ed7c77879..3494d869bc 100644 --- a/lib/database/adapters/LogAdapter.js +++ b/lib/database/adapters/LogAdapter.js @@ -34,6 +34,11 @@ class LogAdapter { */ this.dataPassAdapter = null; + /** + * @type {SimulationPassAdapter|null} + */ + this.simulationPassAdapter = null; + /** * @typedef {EnvironmentAdapter|null} */ @@ -80,6 +85,7 @@ class LogAdapter { runs, lhcFills: sequelizeLhcFills, dataPasses: sequelizeDataPasses, + simulationPasses: sequelizeSimulationPasses, attachments: sequelizeAttachments, environments: sequelizeEnvironments, } = databaseObject; @@ -89,6 +95,7 @@ class LogAdapter { const attachments = (sequelizeAttachments || []).map(this.attachmentAdapter.toEntity); const lhcFills = (sequelizeLhcFills || []).map(this.lhcFillAdapter.toEntity); const dataPasses = (sequelizeDataPasses || []).map(this.dataPassAdapter.toMinifiedEntity); + const simulationPasses = (sequelizeSimulationPasses || []).map(this.simulationPassAdapter.toMinifiedEntity); const environments = (sequelizeEnvironments || []).map(this.environmentAdapter.toEntity); const entityObject = { @@ -105,6 +112,7 @@ class LogAdapter { tags, lhcFills, dataPasses, + simulationPasses, attachments, environments, }; diff --git a/lib/database/adapters/SimulationPassAdapter.js b/lib/database/adapters/SimulationPassAdapter.js index f09fc185b4..6b09fd1356 100644 --- a/lib/database/adapters/SimulationPassAdapter.js +++ b/lib/database/adapters/SimulationPassAdapter.js @@ -20,6 +20,7 @@ class SimulationPassAdapter { */ constructor() { this.toEntity = this.toEntity.bind(this); + this.toMinifiedEntity = this.toMinifiedEntity.bind(this); } /** @@ -58,6 +59,16 @@ class SimulationPassAdapter { pdpBeamTypes, }; } + + /** + * Converts the given database object to a minified entity object (only identification fields). + * + * @param {SequelizeSimulationPass} databaseObject Object to convert. + * @returns {{id: number, name: string}} Converted minified entity object. + */ + toMinifiedEntity({ id, name }) { + return { id, name }; + } } module.exports = SimulationPassAdapter; diff --git a/lib/database/adapters/index.js b/lib/database/adapters/index.js index 4fe08628e8..433a4febeb 100644 --- a/lib/database/adapters/index.js +++ b/lib/database/adapters/index.js @@ -118,6 +118,7 @@ lhcPeriodStatisticsAdapter.lhcPeriodAdapter = lhcPeriodAdapter; logAdapter.attachmentAdapter = attachmentAdapter; logAdapter.dataPassAdapter = dataPassAdapter; +logAdapter.simulationPassAdapter = simulationPassAdapter; logAdapter.environmentAdapter = environmentAdapter; logAdapter.lhcFillAdapter = lhcFillAdapter; logAdapter.runAdapter = runAdapter; diff --git a/lib/database/migrations/v1/20261019100000-create-log-simulation-passes.js b/lib/database/migrations/v1/20261019100000-create-log-simulation-passes.js new file mode 100644 index 0000000000..8c1c968566 --- /dev/null +++ b/lib/database/migrations/v1/20261019100000-create-log-simulation-passes.js @@ -0,0 +1,48 @@ +/** + * @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. + */ + +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => queryInterface.createTable('log_simulation_passes', { + log_id: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { model: 'logs', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + simulation_pass_id: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { model: 'simulation_passes', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), + }, + }), + + down: async (queryInterface) => queryInterface.dropTable('log_simulation_passes'), +}; diff --git a/lib/database/models/log.js b/lib/database/models/log.js index a39166f3e9..84905683a8 100644 --- a/lib/database/models/log.js +++ b/lib/database/models/log.js @@ -64,6 +64,13 @@ module.exports = (sequelize) => { otherKey: 'data_pass_id', }); + Log.belongsToMany(models.SimulationPass, { + as: 'simulationPasses', + through: 'log_simulation_passes', + foreignKey: 'log_id', + otherKey: 'simulation_pass_id', + }); + Log.hasMany(models.Attachment, { as: 'attachments', foreignKey: 'log_id' }); }; diff --git a/lib/database/models/simulationPass.js b/lib/database/models/simulationPass.js index 0fb7cea424..10ec9b8ee6 100644 --- a/lib/database/models/simulationPass.js +++ b/lib/database/models/simulationPass.js @@ -66,6 +66,13 @@ module.exports = (sequelize) => { foreignKey: 'simulation_pass_id', through: 'simulation_pass_quality_control_flag', }); + + SimulationPass.belongsToMany(models.Log, { + as: 'logs', + through: 'log_simulation_passes', + foreignKey: 'simulation_pass_id', + otherKey: 'log_id', + }); }; return SimulationPass; diff --git a/lib/database/models/typedefs/SequelizeLog.js b/lib/database/models/typedefs/SequelizeLog.js index 6f3568095c..a2e295f3cb 100644 --- a/lib/database/models/typedefs/SequelizeLog.js +++ b/lib/database/models/typedefs/SequelizeLog.js @@ -32,4 +32,5 @@ * @property {SequelizeAttachment[]|null} attachments * @property {SequelizeEnvironment[]|null} environments * @property {SequelizeDataPass[]|null} dataPasses + * @property {SequelizeSimulationPass[]|null} simulationPasses */ diff --git a/lib/database/repositories/LogSimulationPassesRepository.js b/lib/database/repositories/LogSimulationPassesRepository.js new file mode 100644 index 0000000000..ef822877a4 --- /dev/null +++ b/lib/database/repositories/LogSimulationPassesRepository.js @@ -0,0 +1,39 @@ +/** + * @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 { tables: { LogSimulationPasses } } = require('..'); +const Repository = require('./Repository'); + +/** + * Sequelize implementation of the LogSimulationPassesRepository. + */ +class LogSimulationPassesRepository extends Repository { + /** + * Creates a new `LogSimulationPassesRepository` instance. + */ + constructor() { + super(LogSimulationPasses); + } + + /** + * Bulk insert logSimulationPasses. + * + * @param {{logId: number, simulationPassId: number}[]} entities the links to insert + * @returns {Promise} Promise object represents the recently inserted LogSimulationPasses. + */ + async bulkInsert(entities) { + return LogSimulationPasses.bulkCreate(entities, { returning: true }); + } +} + +module.exports = new LogSimulationPassesRepository(); diff --git a/lib/database/repositories/index.js b/lib/database/repositories/index.js index aeb1172482..53673cc9c1 100644 --- a/lib/database/repositories/index.js +++ b/lib/database/repositories/index.js @@ -35,6 +35,7 @@ const LhcPeriodStatisticsRepository = require('./LhcPeriodStatisticsRepository') const LogEnvironmentsRepository = require('./LogEnvironmentsRepository'); const LogLhcFillsRepository = require('./LogLhcFillsRepository'); const LogDataPassesRepository = require('./LogDataPassesRepository'); +const LogSimulationPassesRepository = require('./LogSimulationPassesRepository'); const LogRepository = require('./LogRepository'); const LogRunsRepository = require('./LogRunsRepository'); const LogTagsRepository = require('./LogTagsRepository'); @@ -79,6 +80,7 @@ module.exports = { LogEnvironmentsRepository, LogLhcFillsRepository, LogDataPassesRepository, + LogSimulationPassesRepository, LogRepository, LogRunsRepository, LogTagsRepository, diff --git a/lib/database/seeders/20240125141300-log-simulation-passes.js b/lib/database/seeders/20240125141300-log-simulation-passes.js new file mode 100644 index 0000000000..21d1c542b9 --- /dev/null +++ b/lib/database/seeders/20240125141300-log-simulation-passes.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. + */ + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface) => + queryInterface.sequelize.transaction((transaction) => + queryInterface.bulkInsert('log_simulation_passes', [ + { log_id: 1, simulation_pass_id: 1 }, + { log_id: 1, simulation_pass_id: 2 }, + { log_id: 2, simulation_pass_id: 3 }, + { log_id: 119, simulation_pass_id: 1 }, + ], { transaction })), + + down: async (queryInterface) => + queryInterface.sequelize.transaction((transaction) => + queryInterface.bulkDelete('log_simulation_passes', null, { transaction })), +}; diff --git a/lib/database/tables/index.js b/lib/database/tables/index.js index 49f61735e8..d1f3642ab0 100644 --- a/lib/database/tables/index.js +++ b/lib/database/tables/index.js @@ -17,6 +17,7 @@ const LogEnvironments = require('./logenvironments'); const RunTags = require('./runtags'); const LogLhcFills = require('./loglhcfills'); const LogDataPasses = require('./logdatapasses'); +const LogSimulationPasses = require('./logsimulationpasses'); module.exports = (sequelize) => { const models = { @@ -24,6 +25,7 @@ module.exports = (sequelize) => { LogRuns: LogRuns(sequelize), LogLhcFills: LogLhcFills(sequelize), LogDataPasses: LogDataPasses(sequelize), + LogSimulationPasses: LogSimulationPasses(sequelize), LogEnvironments: LogEnvironments(sequelize), RunTags: RunTags(sequelize), }; diff --git a/lib/database/tables/logsimulationpasses.js b/lib/database/tables/logsimulationpasses.js new file mode 100644 index 0000000000..4bdbaaff50 --- /dev/null +++ b/lib/database/tables/logsimulationpasses.js @@ -0,0 +1,38 @@ +/** + * @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. + */ + +module.exports = (sequelize) => { + const Sequelize = require('sequelize'); + + const LogSimulationPasses = sequelize.define('log_simulation_passes', { + logId: { + allowNull: false, + primaryKey: true, + type: Sequelize.INTEGER, + }, + simulationPassId: { + allowNull: false, + primaryKey: true, + type: Sequelize.INTEGER, + }, + }); + + LogSimulationPasses.removeAttribute('id'); + + LogSimulationPasses.associate = (models) => { + LogSimulationPasses.belongsTo(models.SimulationPass, { foreignKey: 'simulation_pass_id', as: 'simulationPass' }); + LogSimulationPasses.belongsTo(models.Log, { foreignKey: 'log_id', as: 'log' }); + }; + + return LogSimulationPasses; +}; diff --git a/lib/domain/dtos/CreateLogDto.js b/lib/domain/dtos/CreateLogDto.js index 8dc7ab10ac..ff4accdb30 100644 --- a/lib/domain/dtos/CreateLogDto.js +++ b/lib/domain/dtos/CreateLogDto.js @@ -35,6 +35,9 @@ const BodyDto = Joi.object({ dataPasses: Joi.array() .optional() .items(Joi.string()), + simulationPasses: Joi.array() + .optional() + .items(Joi.string()), parentLogId: EntityIdDto.optional(), runNumbers: Joi.string().optional(), }); diff --git a/lib/domain/entities/Log.js b/lib/domain/entities/Log.js index de4f1aa125..3d12954eba 100644 --- a/lib/domain/entities/Log.js +++ b/lib/domain/entities/Log.js @@ -29,5 +29,6 @@ * @property {SequelizeTag[]|null} tags * @property {SequelizeEnvironment[]|null} environments * @property {{id: number, name: string}[]} dataPasses - minified version of it + * @property {{id: number, name: string}[]} simulationPasses - minified version of it * @property {SequelizeAttachment[]|null} attachments */ diff --git a/lib/public/views/Logs/Create/LogCreationModel.js b/lib/public/views/Logs/Create/LogCreationModel.js index d27e37ad58..1829ab51dc 100644 --- a/lib/public/views/Logs/Create/LogCreationModel.js +++ b/lib/public/views/Logs/Create/LogCreationModel.js @@ -21,6 +21,7 @@ const DATA_DISPLAY_DELIMITER = ','; * @property {number[]} [runNumbers] run numbers associated with log being created * @property {number[]} [lhcFillNumbers] lhc fill numbers associated with log being created * @property {string[]} [dataPassNames] data pass names associated with log being created + * @property {string[]} [simulationPassNames] simulation pass names associated with log being created * @property {number[]} [environmentIds] environment ids associated with log being created */ @@ -45,10 +46,11 @@ export class LogCreationModel extends Observable { this._creationTagsPickerModel.bubbleTo(this); this._creationTagsPickerModel.visualChange$.bubbleTo(this); - const { runNumbers = [], environmentIds = [], lhcFillNumbers = [], dataPassNames = [] } = relations; + const { runNumbers = [], environmentIds = [], lhcFillNumbers = [], dataPassNames = [], simulationPassNames = [] } = relations; this._runNumbers = runNumbers.join(DATA_DISPLAY_DELIMITER); this._lhcFills = lhcFillNumbers.join(DATA_DISPLAY_DELIMITER); this._dataPasses = dataPassNames.join(DATA_DISPLAY_DELIMITER); + this._simulationPasses = simulationPassNames.join(DATA_DISPLAY_DELIMITER); this._environments = environmentIds.join(DATA_DISPLAY_DELIMITER); this._isRunNumbersReadonly = Boolean(this._runNumbers); @@ -95,12 +97,13 @@ export class LogCreationModel extends Observable { this._createdLog = RemoteData.loading(); this.notify(); - const { title, text, parentLogId, runNumbers, environments, lhcFills, dataPasses, attachments } = this; + const { title, text, parentLogId, runNumbers, environments, lhcFills, dataPasses, simulationPasses, attachments } = this; const tagsTexts = this._creationTagsPickerModel.selected; const environmentsIds = environments ? environments.split(',').map((environment) => environment.trim()) : []; const lhcFillNumbers = lhcFills ? lhcFills.split(',').map((lhcFillNumber) => lhcFillNumber.trim()) : []; const dataPassNames = dataPasses.split(',').map((name) => name.trim()).filter(Boolean); + const simulationPassNames = simulationPasses.split(',').map((name) => name.trim()).filter(Boolean); const body = { title, @@ -114,13 +117,15 @@ export class LogCreationModel extends Observable { headers: { Accept: 'application/json' }, }; - const hasMultipartFields = [attachments, tagsTexts, environmentsIds, lhcFillNumbers, dataPassNames].some(({ length }) => length > 0); + const multipartFields = [attachments, tagsTexts, environmentsIds, lhcFillNumbers, dataPassNames, simulationPassNames]; + const hasMultipartFields = multipartFields.some(({ length }) => length > 0); if (hasMultipartFields) { const formData = new FormData(); Object.entries(body).forEach(([key, value]) => formData.append(key, value)); environmentsIds.forEach((environmentId) => formData.append('environments[]', environmentId)); lhcFillNumbers.forEach((fillNumber) => formData.append('lhcFills[]', fillNumber)); dataPassNames.forEach((name) => formData.append('dataPasses[]', name)); + simulationPassNames.forEach((name) => formData.append('simulationPasses[]', name)); tagsTexts.forEach((tagText) => formData.append('tags[]', tagText)); [...attachments].forEach((attachment, index) => formData.append(`attachments.${index}`, attachment)); @@ -253,6 +258,25 @@ export class LogCreationModel extends Observable { this.notify(); } + /** + * Returns the simulation passes which the log will be linked to, as a comma separated string of names + * + * @returns {string} the simulation pass names + */ + get simulationPasses() { + return `${this._simulationPasses}`; + } + + /** + * Set the simulation passes which the log will be linked to, as a comma separated string of names + * + * @param {string} value the new simulation pass names list + */ + set simulationPasses(value) { + this._simulationPasses = value; + this.notify(); + } + /** * Set the environments which the log will be linked to, as a comma separated string * diff --git a/lib/public/views/Logs/Create/LogReplyModel.js b/lib/public/views/Logs/Create/LogReplyModel.js index db58d00818..7b7137a326 100644 --- a/lib/public/views/Logs/Create/LogReplyModel.js +++ b/lib/public/views/Logs/Create/LogReplyModel.js @@ -45,13 +45,13 @@ export class LogReplyModel extends LogCreationModel { } /** - * Inherit the title, tags, runs, environments, lhcFills and data passes from the parent log + * Inherit the title, tags, runs, environments, lhcFills, data passes and simulation passes from the parent log * * @param {Log} parentLog the parent log * @return {void} */ inheritFromParentLog(parentLog) { - const { title, tags, runs, environments, lhcFills, dataPasses } = parentLog; + const { title, tags, runs, environments, lhcFills, dataPasses, simulationPasses } = parentLog; this.title = `${title}`; @@ -74,6 +74,10 @@ export class LogReplyModel extends LogCreationModel { if (!this.dataPasses) { this.dataPasses = (dataPasses ?? []).map(({ name }) => name).join(', '); } + + if (!this.simulationPasses) { + this.simulationPasses = (simulationPasses ?? []).map(({ name }) => name).join(', '); + } } /** diff --git a/lib/public/views/Logs/Create/logCreationComponent.js b/lib/public/views/Logs/Create/logCreationComponent.js index effaaaa537..bdb519e03a 100644 --- a/lib/public/views/Logs/Create/logCreationComponent.js +++ b/lib/public/views/Logs/Create/logCreationComponent.js @@ -50,6 +50,7 @@ export const logCreationForm = (creationModel, configuration) => { environmentsPanel(creationModel), lhcFillsPanel(creationModel), dataPassesPanel(creationModel), + simulationPassesPanel(creationModel), attachmentsPanel(creationModel), tagsPanel(creationModel), ], @@ -133,6 +134,22 @@ const dataPassesPanel = (logCreationModel) => h(PanelComponent, [ }), ]); +/** + * Builds a panel containing the label and input field for simulation pass names + * @param {LogCreationModel} logCreationModel - Logs creation model + * @returns {vnode} - Panel allowing user to input simulation pass names + */ +const simulationPassesPanel = (logCreationModel) => h(PanelComponent, [ + h(LabelPanelHeaderComponent, { for: 'simulation-passes' }, 'Simulation passes'), + h('input#simulation-passes.form-control', { + placeholder: 'LHC23k6c, LHC23k6b, ... ', + value: logCreationModel.simulationPasses, + oninput: (e) => { + logCreationModel.simulationPasses = e.target.value; + }, + }), +]); + /** * Builds a panel containing the label and input field for environment IDs * @param {LogCreationModel} logCreationModel - Logs creation model diff --git a/lib/server/services/log/LogService.js b/lib/server/services/log/LogService.js index 9391cf81b5..b7896972af 100644 --- a/lib/server/services/log/LogService.js +++ b/lib/server/services/log/LogService.js @@ -42,6 +42,7 @@ class LogService { .include('runs') .include('lhcFills') .include({ association: 'dataPasses', attributes: ['id', 'name'] }) + .include({ association: 'simulationPasses', attributes: ['id', 'name'] }) .include('environments') .include('attachments'); }); @@ -222,9 +223,19 @@ class LogService { * @param {number[]} lhcFills the LHC fill numbers associated with this log * @param {Attachment[]} attachments the list of attachments to link to the log * @param {string[]} dataPassNames the names of the data passes associated with this log + * @param {string[]} simulationPassNames the names of the simulation passes associated with this log * @return {Promise} resolve with the created log instance */ - async create(newLog, runNumbers = [], tagsTexts = [], environments = [], lhcFills = [], attachments = [], dataPassNames = []) { + async create( + newLog, + runNumbers = [], + tagsTexts = [], + environments = [], + lhcFills = [], + attachments = [], + dataPassNames = [], + simulationPassNames = [], + ) { const logId = await createLog( logAdapter.toDatabase(newLog), runNumbers, @@ -233,6 +244,7 @@ class LogService { lhcFills, attachments.map((attachment) => attachmentAdapter.toDatabase(attachment)), dataPassNames, + simulationPassNames, ); // No transaction, log is ready here and can not be null return this.get(logId); diff --git a/lib/server/services/log/createLog.js b/lib/server/services/log/createLog.js index e1a507da66..8416c317bf 100644 --- a/lib/server/services/log/createLog.js +++ b/lib/server/services/log/createLog.js @@ -26,8 +26,9 @@ const LogLhcFillsRepository = require('../../../database/repositories/LogLhcFill const { getLogOrFail } = require('./getLogOrFail.js'); const { NotFoundError } = require('../../errors/NotFoundError.js'); const { checkLhcFillsExistOrFail } = require('../lhcFill/checkLhcFillsExistOrFail'); -const { LogEnvironmentsRepository, LogDataPassesRepository } = require('../../../database/repositories'); +const { LogEnvironmentsRepository, LogDataPassesRepository, LogSimulationPassesRepository } = require('../../../database/repositories'); const { getAllDataPassesByNameOrFail } = require('../dataPasses/getAllDataPassesByNameOrFail.js'); +const { getAllSimulationPassesByNameOrFail } = require('../simulationPasses/getAllSimulationPassesByNameOrFail.js'); const logger = LogManager.getLogger('log-creation'); @@ -96,10 +97,21 @@ const getRootLogId = async (parentLogId) => { * @param {number[]} lhcFills the LHC fill numbers associated with this log * @param {SequelizeAttachment[]} attachments the list of attachments to link to the log * @param {string[]} dataPassNames the names of the data passes associated with this log + * @param {string[]} simulationPassNames the names of the simulation passes associated with this log * @param {Object} [transaction] optionally the transaction in which one the log creation is executed * @return {Promise} resolve once the creation is done providing the id of the log that have been (or will be) created */ -exports.createLog = async (log, runNumbers, tagsTexts, environmentIds, lhcFills, attachments, dataPassNames, transaction) => { +exports.createLog = async ( + log, + runNumbers, + tagsTexts, + environmentIds, + lhcFills, + attachments, + dataPassNames, + simulationPassNames, + transaction, +) => { const runIds = await getRunIdsFromRunNumbersOrFail(runNumbers); const tags = await getAllTagsByTextOrFail(tagsTexts); const environments = await getAllEnvironmentsOrFail(environmentIds); @@ -108,6 +120,7 @@ exports.createLog = async (log, runNumbers, tagsTexts, environmentIds, lhcFills, } const dataPasses = await getAllDataPassesByNameOrFail(dataPassNames); + const simulationPasses = await getAllSimulationPassesByNameOrFail(simulationPassNames); const { id: newLogId } = await TransactionHelper.provide( async () => { @@ -152,6 +165,14 @@ exports.createLog = async (log, runNumbers, tagsTexts, environmentIds, lhcFills, ); } + if (simulationPasses.length > 0) { + await TransactionHelper.provide( + () => LogSimulationPassesRepository + .bulkInsert(simulationPasses.map(({ id }) => ({ logId: newLogId, simulationPassId: id }))), + { transaction }, + ); + } + if (environments.length > 0) { await TransactionHelper.provide( () => LogEnvironmentsRepository.bulkInsert(environments.map((environment) => ({ environmentId: environment.id, logId: newLogId }))), diff --git a/lib/server/services/run/RunService.js b/lib/server/services/run/RunService.js index d072fb6924..de378d530a 100644 --- a/lib/server/services/run/RunService.js +++ b/lib/server/services/run/RunService.js @@ -118,6 +118,7 @@ const logRunDetectorQualityChange = async (runNumber, runDetectors, transaction, [], [], [], + [], transaction, ); if (error) { @@ -166,6 +167,7 @@ const logEorReasonChange = async (runNumber, userName, eorReasons, transaction) [], [], [], + [], transaction, ); diff --git a/lib/server/services/run/logEntriesCreation/logCalibrationStatusChange.js b/lib/server/services/run/logEntriesCreation/logCalibrationStatusChange.js index a637d69a19..06a0031684 100644 --- a/lib/server/services/run/logEntriesCreation/logCalibrationStatusChange.js +++ b/lib/server/services/run/logEntriesCreation/logCalibrationStatusChange.js @@ -64,6 +64,7 @@ exports.logCalibrationStatusChange = async ( [], [], [], + [], transaction, ); if (error) { diff --git a/lib/server/services/run/logEntriesCreation/logRunQualityChange.js b/lib/server/services/run/logEntriesCreation/logRunQualityChange.js index e4aa02fd34..18ac340cb6 100644 --- a/lib/server/services/run/logEntriesCreation/logRunQualityChange.js +++ b/lib/server/services/run/logEntriesCreation/logRunQualityChange.js @@ -70,6 +70,7 @@ exports.logRunQualityChange = async ( [], [], [], + [], transaction, ); if (error) { diff --git a/lib/server/services/run/logEntriesCreation/logSpecificRunTag.js b/lib/server/services/run/logEntriesCreation/logSpecificRunTag.js index e15058726c..4b29acb1dd 100644 --- a/lib/server/services/run/logEntriesCreation/logSpecificRunTag.js +++ b/lib/server/services/run/logEntriesCreation/logSpecificRunTag.js @@ -48,6 +48,7 @@ exports.logSpecificRunTag = async ( [], [], [], + [], transaction, ); }; diff --git a/lib/server/services/simulationPasses/getAllSimulationPassesByNameOrFail.js b/lib/server/services/simulationPasses/getAllSimulationPassesByNameOrFail.js new file mode 100644 index 0000000000..4d9284081a --- /dev/null +++ b/lib/server/services/simulationPasses/getAllSimulationPassesByNameOrFail.js @@ -0,0 +1,46 @@ +/** + * @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: { SimulationPassRepository }, utilities: { QueryBuilder } } = require('../../../database'); +const { NotFoundError } = require('../../errors/NotFoundError.js'); + +/** + * Extract all the simulation passes (id and name only) corresponding to a list of simulation pass names. + * If any do not correspond to a simulation pass, throw an error + * + * @param {string[]} [simulationPassNames] the list of simulation pass names + * @return {Promise<{id: number, name: string}[]>} the list of simulation passes + */ +const getAllSimulationPassesByNameOrFail = async (simulationPassNames) => { + if (!simulationPassNames || simulationPassNames.length === 0) { + return []; + } + + const queryBuilder = new QueryBuilder() + .set('attributes', ['id', 'name']) + .where('name').oneOf(...simulationPassNames); + + const simulationPasses = await SimulationPassRepository.findAll(queryBuilder); + + const foundNames = new Set(simulationPasses.map(({ name }) => name)); + const missingNames = simulationPassNames.filter((name) => !foundNames.has(name)); + if (missingNames.length > 0) { + throw new NotFoundError(missingNames.length === 1 + ? `Simulation pass with this name (${missingNames[0]}) could not be found` + : `Simulation passes with these names (${missingNames.join(', ')}) could not be found`); + } + + return simulationPasses; +}; + +exports.getAllSimulationPassesByNameOrFail = getAllSimulationPassesByNameOrFail; diff --git a/lib/usecases/log/CreateLogUseCase.js b/lib/usecases/log/CreateLogUseCase.js index 569924b83b..8c9c12b6fc 100644 --- a/lib/usecases/log/CreateLogUseCase.js +++ b/lib/usecases/log/CreateLogUseCase.js @@ -27,7 +27,7 @@ class CreateLogUseCase { async execute(dto) { const { body, files } = dto; - const { tags: tagsTexts, runNumbers: serializedRunNumbers, environments, lhcFills, dataPasses } = body; + const { tags: tagsTexts, runNumbers: serializedRunNumbers, environments, lhcFills, dataPasses, simulationPasses } = body; // Parse comma-separated string to array of numbers const runNumbers = serializedRunNumbers ? [ @@ -37,6 +37,7 @@ class CreateLogUseCase { ] : []; const lhcFillsSet = lhcFills ? [...new Set(lhcFills)] : []; const dataPassesSet = dataPasses ? [...new Set(dataPasses)] : []; + const simulationPassesSet = simulationPasses ? [...new Set(simulationPasses)] : []; body.userId = dto?.session?.id || null; @@ -50,6 +51,7 @@ class CreateLogUseCase { lhcFillsSet || [], files || [], dataPassesSet || [], + simulationPassesSet || [], ), }; } catch (e) { diff --git a/lib/usecases/log/GetLogUseCase.js b/lib/usecases/log/GetLogUseCase.js index 794b096c02..6035639ae1 100644 --- a/lib/usecases/log/GetLogUseCase.js +++ b/lib/usecases/log/GetLogUseCase.js @@ -42,6 +42,7 @@ class GetLogUseCase { .include({ association: 'runs', attributes: ['id', 'runNumber'] }) .include({ association: 'lhcFills', attributes: ['fillNumber'] }) .include({ association: 'dataPasses', attributes: ['id', 'name'] }) + .include({ association: 'simulationPasses', attributes: ['id', 'name'] }) .include('attachments') .include({ association: 'environments', attributes: ['id'] }) .where('id').is(logId); diff --git a/test/api/logs.test.js b/test/api/logs.test.js index 8be89656a5..1801f5785a 100644 --- a/test/api/logs.test.js +++ b/test/api/logs.test.js @@ -1425,6 +1425,30 @@ module.exports = () => { expect(logs.find(({ id }) => id === log.id).dataPasses).to.have.lengthOf(2); }); + it('should return 201 and link the simulation passes if a proper body with simulation pass names was sent', async () => { + const { body: { data: log } } = await request(server) + .post('/api/logs') + .field('title', 'Log with simulation passes') + .field('text', 'Text of log with simulation passes') + .field('simulationPasses[]', 'LHC23k6c') + .field('simulationPasses[]', 'LHC23k6a') + .expect(201); + + expect(log.simulationPasses.map(({ name }) => name).sort()).to.deep.equal(['LHC23k6a', 'LHC23k6c']); + + const { body: { data: fetchedLog } } = await request(server).get(`/api/logs/${log.id}`).expect(200); + expect(fetchedLog.simulationPasses.map(({ name }) => name).sort()).to.deep.equal(['LHC23k6a', 'LHC23k6c']); + }); + + it('should return 400 if one of the simulation passes does not exist', async () => { + await request(server) + .post('/api/logs') + .field('title', 'Log with unknown simulation pass') + .field('text', 'Text of log with unknown simulation pass') + .field('simulationPasses[]', 'NOT_A_SIMULATION_PASS') + .expect(400); + }); + it('should return 400 if one of the data passes does not exist', async () => { await request(server) .post('/api/logs') @@ -1462,6 +1486,11 @@ module.exports = () => { expect(data.dataPasses).to.deep.equal([{ id: 1, name: 'LHC22b_apass1' }]); }); + it('should return the simulation passes linked to the log', async () => { + const { body: { data } } = await request(server).get('/api/logs/119').expect(200); + expect(data.simulationPasses).to.deep.equal([{ id: 1, name: 'LHC23k6c' }]); + }); + it('should return 400 if the log id is not a number', (done) => { request(server) .get('/api/logs/abc') diff --git a/test/lib/server/services/log/LogService.test.js b/test/lib/server/services/log/LogService.test.js index 038238c265..3d272cdf02 100644 --- a/test/lib/server/services/log/LogService.test.js +++ b/test/lib/server/services/log/LogService.test.js @@ -42,6 +42,20 @@ module.exports = () => { expect(logsForFill.map(({ id }) => id)).to.eql([1, 119]); }); + it('Should successfully create a log linked to simulation passes', async () => { + const log = await logService.create(logMock, [], [], [], [], [], [], ['LHC23k6c', 'LHC23k6a']); + + expect(log.simulationPasses.map(({ name }) => name)).to.have.members(['LHC23k6c', 'LHC23k6a']); + expect(log.simulationPasses.every(({ id }) => typeof id === 'number')).to.be.true; + }); + + it('Should throw when creating a log linked to a non-existing simulation pass', async () => { + await assert.rejects( + () => logService.create(logMock, [], [], [], [], [], [], ['NOT_A_SIMULATION_PASS']), + new NotFoundError('Simulation pass with this name (NOT_A_SIMULATION_PASS) could not be found'), + ); + }); + it('Should successfully create a log linked to data passes', async () => { const log = await logService.create(logMock, [], [], [], [], [], ['LHC22b_apass1']); diff --git a/test/lib/server/services/simulationPasses/getAllSimulationPassesByNameOrFail.test.js b/test/lib/server/services/simulationPasses/getAllSimulationPassesByNameOrFail.test.js new file mode 100644 index 0000000000..08ae0cf06b --- /dev/null +++ b/test/lib/server/services/simulationPasses/getAllSimulationPassesByNameOrFail.test.js @@ -0,0 +1,47 @@ +/** + * @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 assert = require('assert'); +const { NotFoundError } = require('../../../../../lib/server/errors/NotFoundError.js'); +const { + getAllSimulationPassesByNameOrFail, +} = require('../../../../../lib/server/services/simulationPasses/getAllSimulationPassesByNameOrFail.js'); + +module.exports = () => { + it('should successfully return the simulation passes matching the given names', async () => { + const simulationPasses = await getAllSimulationPassesByNameOrFail(['LHC23k6c', 'LHC23k6b']); + + expect(simulationPasses.map(({ name }) => name)).to.have.members(['LHC23k6c', 'LHC23k6b']); + expect(simulationPasses.every(({ id }) => typeof id === 'number')).to.be.true; + }); + + it('should return an empty list when no names are given', async () => { + expect(await getAllSimulationPassesByNameOrFail([])).to.deep.equal([]); + expect(await getAllSimulationPassesByNameOrFail(undefined)).to.deep.equal([]); + }); + + it('should throw when one of the simulation passes can not be found', async () => { + await assert.rejects( + () => getAllSimulationPassesByNameOrFail(['LHC23k6c', 'NOT_A_SIMULATION_PASS']), + new NotFoundError('Simulation pass with this name (NOT_A_SIMULATION_PASS) could not be found'), + ); + }); + + it('should throw listing every missing simulation pass', async () => { + await assert.rejects( + () => getAllSimulationPassesByNameOrFail(['LHC23k6c', 'MISSING_1', 'MISSING_2']), + new NotFoundError('Simulation passes with these names (MISSING_1, MISSING_2) could not be found'), + ); + }); +}; diff --git a/test/lib/server/services/simulationPasses/index.js b/test/lib/server/services/simulationPasses/index.js index f3fac1b3c6..2a6a4fbb9b 100644 --- a/test/lib/server/services/simulationPasses/index.js +++ b/test/lib/server/services/simulationPasses/index.js @@ -12,7 +12,9 @@ */ const SimulationPassesService = require('./SimulationPassesService.test.js'); +const getAllSimulationPassesByNameOrFail = require('./getAllSimulationPassesByNameOrFail.test.js'); module.exports = () => { describe('SimulationPassesService', SimulationPassesService); + describe('getAllSimulationPassesByNameOrFail', getAllSimulationPassesByNameOrFail); }; diff --git a/test/lib/usecases/log/CreateLogUseCase.test.js b/test/lib/usecases/log/CreateLogUseCase.test.js index 461f5a5f26..3dddb166c9 100644 --- a/test/lib/usecases/log/CreateLogUseCase.test.js +++ b/test/lib/usecases/log/CreateLogUseCase.test.js @@ -82,6 +82,21 @@ module.exports = () => { expect(result.dataPasses.map(({ name }) => name).sort()).to.deep.equal(['LHC22b_apass1', 'LHC22b_skimming']); }); + it('should insert a new Log linked to the provided simulation passes', async () => { + createLogDto.body.simulationPasses = ['LHC23k6c', 'LHC23k6c', 'LHC23k6b']; + const { result } = await new CreateLogUseCase().execute(createLogDto); + + expect(result.simulationPasses.map(({ name }) => name).sort()).to.deep.equal(['LHC23k6b', 'LHC23k6c']); + }); + + it('should fail to create a Log linked to a non-existing simulation pass', async () => { + createLogDto.body.simulationPasses = ['NOT_A_SIMULATION_PASS']; + const { result, error } = await new CreateLogUseCase().execute(createLogDto); + + expect(result).to.be.undefined; + expect(error.title).to.equal('Simulation pass with this name (NOT_A_SIMULATION_PASS) could not be found'); + }); + it('should fail to create a Log linked to a non-existing data pass', async () => { createLogDto.body.dataPasses = ['NOT_A_DATA_PASS']; const { result, error } = await new CreateLogUseCase().execute(createLogDto); diff --git a/test/public/logs/create.test.js b/test/public/logs/create.test.js index 6fb7a1696a..c50545c3fc 100644 --- a/test/public/logs/create.test.js +++ b/test/public/logs/create.test.js @@ -26,6 +26,7 @@ const { } = require('../defaults.js'); const path = require('path'); const { GetAllLogsUseCase } = require('../../../lib/usecases/log/index.js'); +const { logService } = require('../../../lib/server/services/log/LogService.js'); const fs = require('fs'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -128,6 +129,7 @@ module.exports = () => { await expectInputValue(page, 'input#environments', 'Dxi029djX, eZF99lH6'); await expectInputValue(page, 'input#lhc-fills', '1, 4, 6'); await expectInputValue(page, 'input#data-passes', 'LHC22b_apass1'); + await expectInputValue(page, 'input#simulation-passes', 'LHC23k6c'); }); it('Should verify that form autofill all inputs with provided full parameters.', async () => { @@ -786,4 +788,20 @@ ${aob}\ const lastLog = await getLastLog(); expect(lastLog.dataPasses.map(({ name }) => name).sort()).to.eql(['LHC22b_apass1', 'LHC22b_skimming']); }); + + it('can create a log with linked simulation passes', async () => { + await goToPage(page, 'log-create'); + + await fillInput(page, '#title', 'A log with simulation passes'); + await pressElement(page, '#text ~ .CodeMirror'); + await page.keyboard.type('Sample Text'); + await fillInput(page, 'input#simulation-passes', 'LHC23k6c, LHC23k6b'); + + await pressElement(page, '#send:not([disabled])'); + await page.waitForNavigation(); + + const { id } = await getLastLog(); + const createdLog = await logService.get(id); + expect(createdLog.simulationPasses.map(({ name }) => name).sort()).to.eql(['LHC23k6b', 'LHC23k6c']); + }); };