diff --git a/lib/database/adapters/DataPassAdapter.js b/lib/database/adapters/DataPassAdapter.js index 066d89c81b..4f21525727 100644 --- a/lib/database/adapters/DataPassAdapter.js +++ b/lib/database/adapters/DataPassAdapter.js @@ -20,6 +20,7 @@ class DataPassAdapter { */ constructor() { this.toEntity = this.toEntity.bind(this); + this.toMinifiedEntity = this.toMinifiedEntity.bind(this); this.dataPassVersionAdapter = null; } @@ -53,6 +54,16 @@ class DataPassAdapter { isFrozen, }; } + + /** + * Converts the given database object to a minified entity object (only identification fields). + * + * @param {SequelizeDataPass} databaseObject Object to convert. + * @returns {{id: number, name: string}} Converted minified entity object. + */ + toMinifiedEntity({ id, name }) { + return { id, name }; + } } module.exports = DataPassAdapter; diff --git a/lib/database/adapters/LogAdapter.js b/lib/database/adapters/LogAdapter.js index d2fe8b8b4e..6ed7c77879 100644 --- a/lib/database/adapters/LogAdapter.js +++ b/lib/database/adapters/LogAdapter.js @@ -29,6 +29,11 @@ class LogAdapter { */ this.attachmentAdapter = null; + /** + * @type {DataPassAdapter|null} + */ + this.dataPassAdapter = null; + /** * @typedef {EnvironmentAdapter|null} */ @@ -74,6 +79,7 @@ class LogAdapter { tags: sequelizeTags, runs, lhcFills: sequelizeLhcFills, + dataPasses: sequelizeDataPasses, attachments: sequelizeAttachments, environments: sequelizeEnvironments, } = databaseObject; @@ -82,6 +88,7 @@ class LogAdapter { const tags = (sequelizeTags || []).map(this.tagAdapter.toEntity); const attachments = (sequelizeAttachments || []).map(this.attachmentAdapter.toEntity); const lhcFills = (sequelizeLhcFills || []).map(this.lhcFillAdapter.toEntity); + const dataPasses = (sequelizeDataPasses || []).map(this.dataPassAdapter.toMinifiedEntity); const environments = (sequelizeEnvironments || []).map(this.environmentAdapter.toEntity); const entityObject = { @@ -97,6 +104,7 @@ class LogAdapter { runs: minifiedRuns, tags, lhcFills, + dataPasses, attachments, environments, }; diff --git a/lib/database/adapters/index.js b/lib/database/adapters/index.js index 15e653444b..4fe08628e8 100644 --- a/lib/database/adapters/index.js +++ b/lib/database/adapters/index.js @@ -117,6 +117,7 @@ lhcFillAdapter.statisticsAdapter = lhcFillStatisticsAdapter; lhcPeriodStatisticsAdapter.lhcPeriodAdapter = lhcPeriodAdapter; logAdapter.attachmentAdapter = attachmentAdapter; +logAdapter.dataPassAdapter = dataPassAdapter; logAdapter.environmentAdapter = environmentAdapter; logAdapter.lhcFillAdapter = lhcFillAdapter; logAdapter.runAdapter = runAdapter; diff --git a/lib/database/migrations/v1/20261010100000-create-log-data-passes.js b/lib/database/migrations/v1/20261010100000-create-log-data-passes.js new file mode 100644 index 0000000000..244b457703 --- /dev/null +++ b/lib/database/migrations/v1/20261010100000-create-log-data-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_data_passes', { + log_id: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { model: 'logs', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + data_pass_id: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { model: 'data_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_data_passes'), +}; diff --git a/lib/database/models/dataPass.js b/lib/database/models/dataPass.js index 57bd840357..b9174b0be3 100644 --- a/lib/database/models/dataPass.js +++ b/lib/database/models/dataPass.js @@ -56,6 +56,12 @@ module.exports = (sequelize) => { through: 'data_pass_quality_control_flag', }); DataPass.hasMany(models.DataPassVersion, { as: 'versions' }); + DataPass.belongsToMany(models.Log, { + as: 'logs', + through: 'log_data_passes', + foreignKey: 'data_pass_id', + otherKey: 'log_id', + }); }; return DataPass; diff --git a/lib/database/models/log.js b/lib/database/models/log.js index 7351293db6..a39166f3e9 100644 --- a/lib/database/models/log.js +++ b/lib/database/models/log.js @@ -57,6 +57,13 @@ module.exports = (sequelize) => { Log.belongsToMany(models.Environment, { as: 'environments', through: 'log_environments' }); Log.belongsToMany(models.LhcFill, { as: 'lhcFills', through: 'log_lhc_fills', foreignKey: 'log_id' }); + Log.belongsToMany(models.DataPass, { + as: 'dataPasses', + through: 'log_data_passes', + foreignKey: 'log_id', + otherKey: 'data_pass_id', + }); + Log.hasMany(models.Attachment, { as: 'attachments', foreignKey: 'log_id' }); }; diff --git a/lib/database/models/typedefs/SequelizeLog.js b/lib/database/models/typedefs/SequelizeLog.js index 86b0fc2366..6f3568095c 100644 --- a/lib/database/models/typedefs/SequelizeLog.js +++ b/lib/database/models/typedefs/SequelizeLog.js @@ -31,4 +31,5 @@ * @property {SequelizeTag[]|null} tags * @property {SequelizeAttachment[]|null} attachments * @property {SequelizeEnvironment[]|null} environments + * @property {SequelizeDataPass[]|null} dataPasses */ diff --git a/lib/database/repositories/LogDataPassesRepository.js b/lib/database/repositories/LogDataPassesRepository.js new file mode 100644 index 0000000000..39d3965f03 --- /dev/null +++ b/lib/database/repositories/LogDataPassesRepository.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: { LogDataPasses } } = require('..'); +const Repository = require('./Repository'); + +/** + * Sequelize implementation of the LogDataPassesRepository. + */ +class LogDataPassesRepository extends Repository { + /** + * Creates a new `LogDataPassesRepository` instance. + */ + constructor() { + super(LogDataPasses); + } + + /** + * Bulk insert logDataPasses. + * + * @param {{logId: number, dataPassId: number}[]} entities the links to insert + * @returns {Promise} Promise object represents the recently inserted LogDataPasses. + */ + async bulkInsert(entities) { + return LogDataPasses.bulkCreate(entities, { returning: true }); + } +} + +module.exports = new LogDataPassesRepository(); diff --git a/lib/database/repositories/index.js b/lib/database/repositories/index.js index 0c79279752..aeb1172482 100644 --- a/lib/database/repositories/index.js +++ b/lib/database/repositories/index.js @@ -34,6 +34,7 @@ const LhcPeriodRepository = require('./LhcPeriodRepository'); const LhcPeriodStatisticsRepository = require('./LhcPeriodStatisticsRepository'); const LogEnvironmentsRepository = require('./LogEnvironmentsRepository'); const LogLhcFillsRepository = require('./LogLhcFillsRepository'); +const LogDataPassesRepository = require('./LogDataPassesRepository'); const LogRepository = require('./LogRepository'); const LogRunsRepository = require('./LogRunsRepository'); const LogTagsRepository = require('./LogTagsRepository'); @@ -77,6 +78,7 @@ module.exports = { LhcPeriodStatisticsRepository, LogEnvironmentsRepository, LogLhcFillsRepository, + LogDataPassesRepository, LogRepository, LogRunsRepository, LogTagsRepository, diff --git a/lib/database/seeders/20240112102500-log-data-passes.js b/lib/database/seeders/20240112102500-log-data-passes.js new file mode 100644 index 0000000000..9e8cbb9435 --- /dev/null +++ b/lib/database/seeders/20240112102500-log-data-passes.js @@ -0,0 +1,27 @@ +/** + * @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_data_passes', [ + { log_id: 1, data_pass_id: 1 }, + { log_id: 1, data_pass_id: 2 }, + { log_id: 119, data_pass_id: 1 }, + ], { transaction })), + + down: async (queryInterface) => + queryInterface.sequelize.transaction((transaction) => + queryInterface.bulkDelete('log_data_passes', null, { transaction })), +}; diff --git a/lib/database/tables/index.js b/lib/database/tables/index.js index e85da8ae13..49f61735e8 100644 --- a/lib/database/tables/index.js +++ b/lib/database/tables/index.js @@ -16,12 +16,14 @@ const LogRuns = require('./logruns'); const LogEnvironments = require('./logenvironments'); const RunTags = require('./runtags'); const LogLhcFills = require('./loglhcfills'); +const LogDataPasses = require('./logdatapasses'); module.exports = (sequelize) => { const models = { LogTags: LogTags(sequelize), LogRuns: LogRuns(sequelize), LogLhcFills: LogLhcFills(sequelize), + LogDataPasses: LogDataPasses(sequelize), LogEnvironments: LogEnvironments(sequelize), RunTags: RunTags(sequelize), }; diff --git a/lib/database/tables/logdatapasses.js b/lib/database/tables/logdatapasses.js new file mode 100644 index 0000000000..a24d951a55 --- /dev/null +++ b/lib/database/tables/logdatapasses.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 LogDataPasses = sequelize.define('log_data_passes', { + logId: { + allowNull: false, + primaryKey: true, + type: Sequelize.INTEGER, + }, + dataPassId: { + allowNull: false, + primaryKey: true, + type: Sequelize.INTEGER, + }, + }); + + LogDataPasses.removeAttribute('id'); + + LogDataPasses.associate = (models) => { + LogDataPasses.belongsTo(models.DataPass, { foreignKey: 'data_pass_id', as: 'dataPass' }); + LogDataPasses.belongsTo(models.Log, { foreignKey: 'log_id', as: 'log' }); + }; + + return LogDataPasses; +}; diff --git a/lib/domain/dtos/CreateLogDto.js b/lib/domain/dtos/CreateLogDto.js index 7359a2e48c..8dc7ab10ac 100644 --- a/lib/domain/dtos/CreateLogDto.js +++ b/lib/domain/dtos/CreateLogDto.js @@ -32,6 +32,9 @@ const BodyDto = Joi.object({ lhcFills: Joi.array() .optional() .items(Joi.number()), + dataPasses: 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 95709c334d..de4f1aa125 100644 --- a/lib/domain/entities/Log.js +++ b/lib/domain/entities/Log.js @@ -28,5 +28,6 @@ * @property {SequelizeRun[]|null} runs * @property {SequelizeTag[]|null} tags * @property {SequelizeEnvironment[]|null} environments + * @property {{id: number, name: string}[]} dataPasses - 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 acf00ee637..d27e37ad58 100644 --- a/lib/public/views/Logs/Create/LogCreationModel.js +++ b/lib/public/views/Logs/Create/LogCreationModel.js @@ -20,6 +20,7 @@ const DATA_DISPLAY_DELIMITER = ','; * @typedef LogCreationRelations * @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 {number[]} [environmentIds] environment ids associated with log being created */ @@ -44,9 +45,10 @@ export class LogCreationModel extends Observable { this._creationTagsPickerModel.bubbleTo(this); this._creationTagsPickerModel.visualChange$.bubbleTo(this); - const { runNumbers = [], environmentIds = [], lhcFillNumbers = [] } = relations; + const { runNumbers = [], environmentIds = [], lhcFillNumbers = [], dataPassNames = [] } = relations; this._runNumbers = runNumbers.join(DATA_DISPLAY_DELIMITER); this._lhcFills = lhcFillNumbers.join(DATA_DISPLAY_DELIMITER); + this._dataPasses = dataPassNames.join(DATA_DISPLAY_DELIMITER); this._environments = environmentIds.join(DATA_DISPLAY_DELIMITER); this._isRunNumbersReadonly = Boolean(this._runNumbers); @@ -93,11 +95,13 @@ export class LogCreationModel extends Observable { this._createdLog = RemoteData.loading(); this.notify(); - const { title, text, parentLogId, runNumbers, environments, lhcFills, attachments } = this; + const { title, text, parentLogId, runNumbers, environments, lhcFills, dataPasses, 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 body = { title, text, @@ -110,11 +114,13 @@ export class LogCreationModel extends Observable { headers: { Accept: 'application/json' }, }; - if (attachments.length > 0 || tagsTexts.length > 0 || environmentsIds.length > 0 || lhcFillNumbers.length > 0) { + const hasMultipartFields = [attachments, tagsTexts, environmentsIds, lhcFillNumbers, dataPassNames].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)); tagsTexts.forEach((tagText) => formData.append('tags[]', tagText)); [...attachments].forEach((attachment, index) => formData.append(`attachments.${index}`, attachment)); @@ -228,6 +234,25 @@ export class LogCreationModel extends Observable { return `${this._lhcFills}`; } + /** + * Returns the data passes which the log will be linked to, as a comma separated string of names + * + * @returns {string} the data pass names + */ + get dataPasses() { + return `${this._dataPasses}`; + } + + /** + * Set the data passes which the log will be linked to, as a comma separated string of names + * + * @param {string} value the new data pass names list + */ + set dataPasses(value) { + this._dataPasses = 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 3a4388edb3..db58d00818 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 and lhcFills from the parent log + * Inherit the title, tags, runs, environments, lhcFills and data passes from the parent log * * @param {Log} parentLog the parent log * @return {void} */ inheritFromParentLog(parentLog) { - const { title, tags, runs, environments, lhcFills } = parentLog; + const { title, tags, runs, environments, lhcFills, dataPasses } = parentLog; this.title = `${title}`; @@ -70,6 +70,10 @@ export class LogReplyModel extends LogCreationModel { if (!this.lhcFills) { this.lhcFills = lhcFills.map(({ fillNumber }) => fillNumber).join(', '); } + + if (!this.dataPasses) { + this.dataPasses = (dataPasses ?? []).map(({ name }) => name).join(', '); + } } /** diff --git a/lib/public/views/Logs/Create/logCreationComponent.js b/lib/public/views/Logs/Create/logCreationComponent.js index 12072aee53..effaaaa537 100644 --- a/lib/public/views/Logs/Create/logCreationComponent.js +++ b/lib/public/views/Logs/Create/logCreationComponent.js @@ -49,6 +49,7 @@ export const logCreationForm = (creationModel, configuration) => { runNumbersPanel(creationModel), environmentsPanel(creationModel), lhcFillsPanel(creationModel), + dataPassesPanel(creationModel), attachmentsPanel(creationModel), tagsPanel(creationModel), ], @@ -116,6 +117,22 @@ const lhcFillsPanel = (logCreationModel) => h(PanelComponent, [ }), ]); +/** + * Builds a panel containing the label and input field for data pass names + * @param {LogCreationModel} logCreationModel - Logs creation model + * @returns {vnode} - Panel allowing user to input data pass names + */ +const dataPassesPanel = (logCreationModel) => h(PanelComponent, [ + h(LabelPanelHeaderComponent, { for: 'data-passes' }, 'Data passes'), + h('input#data-passes.form-control', { + placeholder: 'LHC22b_apass1, LHC22o_apass6, ... ', + value: logCreationModel.dataPasses, + oninput: (e) => { + logCreationModel.dataPasses = 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/dataPasses/getAllDataPassesByNameOrFail.js b/lib/server/services/dataPasses/getAllDataPassesByNameOrFail.js new file mode 100644 index 0000000000..bdcfc8bede --- /dev/null +++ b/lib/server/services/dataPasses/getAllDataPassesByNameOrFail.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: { DataPassRepository }, utilities: { QueryBuilder } } = require('../../../database'); +const { NotFoundError } = require('../../errors/NotFoundError.js'); + +/** + * Extract all the data passes (id and name only, without versions) corresponding to a list of data pass names. + * If any do not correspond to a data pass, throw an error + * + * @param {string[]} [dataPassNames] the list of data pass names + * @return {Promise<{id: number, name: string}[]>} the list of data passes + */ +const getAllDataPassesByNameOrFail = async (dataPassNames) => { + if (!dataPassNames || dataPassNames.length === 0) { + return []; + } + + const queryBuilder = new QueryBuilder() + .set('attributes', ['id', 'name']) + .where('name').oneOf(...dataPassNames); + + const dataPasses = await DataPassRepository.findAll(queryBuilder); + + const foundNames = new Set(dataPasses.map(({ name }) => name)); + const missingNames = dataPassNames.filter((name) => !foundNames.has(name)); + if (missingNames.length > 0) { + throw new NotFoundError(missingNames.length === 1 + ? `Data pass with this name (${missingNames[0]}) could not be found` + : `Data passes with these names (${missingNames.join(', ')}) could not be found`); + } + + return dataPasses; +}; + +exports.getAllDataPassesByNameOrFail = getAllDataPassesByNameOrFail; diff --git a/lib/server/services/log/LogService.js b/lib/server/services/log/LogService.js index 76fe7f9c79..4a1049f954 100644 --- a/lib/server/services/log/LogService.js +++ b/lib/server/services/log/LogService.js @@ -41,6 +41,7 @@ class LogService { .include('tags') .include('runs') .include('lhcFills') + .include({ association: 'dataPasses', attributes: ['id', 'name'] }) .include('environments') .include('attachments'); }); @@ -185,6 +186,7 @@ class LogService { return dataSource.createQueryBuilder() .include({ association: 'runs', attributes: ['id', 'runNumber'] }) .include({ association: 'lhcFills', attributes: ['fillNumber'] }) + .include({ association: 'dataPasses', attributes: ['id', 'name'] }) .include({ association: 'environments', attributes: ['id'] }) .include({ association: 'user', attributes: ['name'] }) .include({ association: 'tags', attributes: ['text'] }); @@ -199,9 +201,10 @@ class LogService { * @param {string[]} environments the environmentIds to attach to log * @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 * @return {Promise} resolve with the created log instance */ - async create(newLog, runNumbers = [], tagsTexts = [], environments = [], lhcFills = [], attachments = []) { + async create(newLog, runNumbers = [], tagsTexts = [], environments = [], lhcFills = [], attachments = [], dataPassNames = []) { const logId = await createLog( logAdapter.toDatabase(newLog), runNumbers, @@ -209,6 +212,7 @@ class LogService { environments, lhcFills, attachments.map((attachment) => attachmentAdapter.toDatabase(attachment)), + dataPassNames, ); // 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 29ed59f3f1..e1a507da66 100644 --- a/lib/server/services/log/createLog.js +++ b/lib/server/services/log/createLog.js @@ -26,7 +26,8 @@ const LogLhcFillsRepository = require('../../../database/repositories/LogLhcFill const { getLogOrFail } = require('./getLogOrFail.js'); const { NotFoundError } = require('../../errors/NotFoundError.js'); const { checkLhcFillsExistOrFail } = require('../lhcFill/checkLhcFillsExistOrFail'); -const { LogEnvironmentsRepository } = require('../../../database/repositories'); +const { LogEnvironmentsRepository, LogDataPassesRepository } = require('../../../database/repositories'); +const { getAllDataPassesByNameOrFail } = require('../dataPasses/getAllDataPassesByNameOrFail.js'); const logger = LogManager.getLogger('log-creation'); @@ -94,10 +95,11 @@ const getRootLogId = async (parentLogId) => { * @param {string[]} environmentIds the environmentIds to attach to log * @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 {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, transaction) => { +exports.createLog = async (log, runNumbers, tagsTexts, environmentIds, lhcFills, attachments, dataPassNames, transaction) => { const runIds = await getRunIdsFromRunNumbersOrFail(runNumbers); const tags = await getAllTagsByTextOrFail(tagsTexts); const environments = await getAllEnvironmentsOrFail(environmentIds); @@ -105,6 +107,8 @@ exports.createLog = async (log, runNumbers, tagsTexts, environmentIds, lhcFills, await checkLhcFillsExistOrFail(lhcFills); } + const dataPasses = await getAllDataPassesByNameOrFail(dataPassNames); + const { id: newLogId } = await TransactionHelper.provide( async () => { log.rootLogId = await getRootLogId(log.parentLogId); @@ -141,6 +145,13 @@ exports.createLog = async (log, runNumbers, tagsTexts, environmentIds, lhcFills, ); } + if (dataPasses.length > 0) { + await TransactionHelper.provide( + () => LogDataPassesRepository.bulkInsert(dataPasses.map(({ id }) => ({ logId: newLogId, dataPassId: 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 913b653fdd..d072fb6924 100644 --- a/lib/server/services/run/RunService.js +++ b/lib/server/services/run/RunService.js @@ -117,6 +117,7 @@ const logRunDetectorQualityChange = async (runNumber, runDetectors, transaction, [], [], [], + [], transaction, ); if (error) { @@ -164,6 +165,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 24b20af839..a637d69a19 100644 --- a/lib/server/services/run/logEntriesCreation/logCalibrationStatusChange.js +++ b/lib/server/services/run/logEntriesCreation/logCalibrationStatusChange.js @@ -63,6 +63,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 b7408f7828..e4aa02fd34 100644 --- a/lib/server/services/run/logEntriesCreation/logRunQualityChange.js +++ b/lib/server/services/run/logEntriesCreation/logRunQualityChange.js @@ -69,6 +69,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 84f7a82cd1..e15058726c 100644 --- a/lib/server/services/run/logEntriesCreation/logSpecificRunTag.js +++ b/lib/server/services/run/logEntriesCreation/logSpecificRunTag.js @@ -47,6 +47,7 @@ exports.logSpecificRunTag = async ( [], [], [], + [], transaction, ); }; diff --git a/lib/usecases/log/CreateLogUseCase.js b/lib/usecases/log/CreateLogUseCase.js index ae4f874b7e..569924b83b 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 } = body; + const { tags: tagsTexts, runNumbers: serializedRunNumbers, environments, lhcFills, dataPasses } = body; // Parse comma-separated string to array of numbers const runNumbers = serializedRunNumbers ? [ @@ -35,12 +35,23 @@ class CreateLogUseCase { .map((x) => x.trim()) .map((x) => parseInt(x, 10))), ] : []; - const lhcFillNumbers = lhcFills ? [...new Set(lhcFills)] : []; + const lhcFillsSet = lhcFills ? [...new Set(lhcFills)] : []; + const dataPassesSet = dataPasses ? [...new Set(dataPasses)] : []; body.userId = dto?.session?.id || null; try { - return { result: await logService.create(body, runNumbers, tagsTexts || [], environments || [], lhcFillNumbers || [], files || []) }; + return { + result: await logService.create( + body, + runNumbers, + tagsTexts || [], + environments || [], + lhcFillsSet || [], + files || [], + dataPassesSet || [], + ), + }; } catch (e) { return { error: { diff --git a/test/lib/server/services/dataPasses/getAllDataPassesByNameOrFail.test.js b/test/lib/server/services/dataPasses/getAllDataPassesByNameOrFail.test.js new file mode 100644 index 0000000000..e9b1a2c2c1 --- /dev/null +++ b/test/lib/server/services/dataPasses/getAllDataPassesByNameOrFail.test.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 { expect } = require('chai'); +const assert = require('assert'); +const { NotFoundError } = require('../../../../../lib/server/errors/NotFoundError.js'); +const { + getAllDataPassesByNameOrFail, +} = require('../../../../../lib/server/services/dataPasses/getAllDataPassesByNameOrFail.js'); + +module.exports = () => { + it('should successfully return the data passes matching the given names', async () => { + const dataPasses = await getAllDataPassesByNameOrFail(['LHC22b_apass1', 'LHC22b_skimming']); + + expect(dataPasses.map(({ name }) => name)).to.deep.equal(['LHC22b_apass1', 'LHC22b_skimming']); + }); + + it('should return an empty list when no names are given', async () => { + expect(await getAllDataPassesByNameOrFail([])).to.deep.equal([]); + expect(await getAllDataPassesByNameOrFail(undefined)).to.deep.equal([]); + }); + + it('should throw when one of the data passes can not be found', async () => { + await assert.rejects( + () => getAllDataPassesByNameOrFail(['LHC22b_apass1', 'NOT_A_DATA_PASS']), + new NotFoundError('Data pass with this name (NOT_A_DATA_PASS) could not be found'), + ); + }); + + it('should throw listing every missing data pass', async () => { + await assert.rejects( + () => getAllDataPassesByNameOrFail(['LHC22b_apass1', 'MISSING_1', 'MISSING_2']), + new NotFoundError('Data passes with these names (MISSING_1, MISSING_2) could not be found'), + ); + }); +}; diff --git a/test/lib/server/services/dataPasses/index.js b/test/lib/server/services/dataPasses/index.js index 2d91f3e395..32b8c73926 100644 --- a/test/lib/server/services/dataPasses/index.js +++ b/test/lib/server/services/dataPasses/index.js @@ -12,7 +12,9 @@ */ const DataPassService = require('./DataPassService.test.js'); +const getAllDataPassesByNameOrFail = require('./getAllDataPassesByNameOrFail.test.js'); module.exports = () => { describe('DataPassService', DataPassService); + describe('getAllDataPassesByNameOrFail', getAllDataPassesByNameOrFail); }; diff --git a/test/lib/server/services/log/LogService.test.js b/test/lib/server/services/log/LogService.test.js index 33dd00a4d9..76dd93eaf8 100644 --- a/test/lib/server/services/log/LogService.test.js +++ b/test/lib/server/services/log/LogService.test.js @@ -42,6 +42,19 @@ module.exports = () => { expect(logsForFill.map(({ id }) => id)).to.eql([1, 119]); }); + it('Should successfully create a log linked to data passes', async () => { + const log = await logService.create(logMock, [], [], [], [], [], ['LHC22b_apass1']); + + expect(log.dataPasses.map(({ name }) => name)).to.deep.equal(['LHC22b_apass1']); + }); + + it('Should throw when creating a log linked to a non-existing data pass', async () => { + await assert.rejects( + () => logService.create(logMock, [], [], [], [], [], ['NOT_A_DATA_PASS']), + new NotFoundError('Data pass with this name (NOT_A_DATA_PASS) could not be found'), + ); + }); + it('Should successfully return all the logs linked to a given environment', async () => { const logsForFill = await logService.getAllByEnvironment('8E4aZTjY'); expect(logsForFill).to.lengthOf(3); diff --git a/test/lib/server/services/log/index.js b/test/lib/server/services/log/index.js index 5235281033..1f3032867a 100644 --- a/test/lib/server/services/log/index.js +++ b/test/lib/server/services/log/index.js @@ -17,7 +17,10 @@ const LogServiceTest = require('./LogService.test.js'); const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent.js'); module.exports = () => { + // Logs created in this suite must not shift the ids expected by the suites that run afterward before(resetDatabaseContent); + after(resetDatabaseContent); + describe('getLogsByTagsInPeriodTest', getLogsByTagsInPeriodTest); describe('getLogsByUserInPeriodTest', getLogsByUserInPeriodTest); describe('LogService', LogServiceTest); diff --git a/test/lib/usecases/log/CreateLogUseCase.test.js b/test/lib/usecases/log/CreateLogUseCase.test.js index a861d3283f..461f5a5f26 100644 --- a/test/lib/usecases/log/CreateLogUseCase.test.js +++ b/test/lib/usecases/log/CreateLogUseCase.test.js @@ -75,6 +75,21 @@ module.exports = () => { expect(result.tags.map(({ text }) => text)).to.deep.equal(expectedTagTexts); }); + it('should insert a new Log linked to the provided data passes', async () => { + createLogDto.body.dataPasses = ['LHC22b_apass1', 'LHC22b_apass1', 'LHC22b_skimming']; + const { result } = await new CreateLogUseCase().execute(createLogDto); + + expect(result.dataPasses.map(({ name }) => name).sort()).to.deep.equal(['LHC22b_apass1', 'LHC22b_skimming']); + }); + + 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); + + expect(result).to.be.undefined; + expect(error.title).to.equal('Data pass with this name (NOT_A_DATA_PASS) could not be found'); + }); + it('should successfully take the parent\'s title if none has been provided', async () => { const expectedTitle = 'Third entry'; const expectedParentLogId = 3; diff --git a/test/public/logs/create.test.js b/test/public/logs/create.test.js index d4d5669b00..40ed9f9008 100644 --- a/test/public/logs/create.test.js +++ b/test/public/logs/create.test.js @@ -127,6 +127,7 @@ module.exports = () => { await expectInputValue(page, 'input#run-numbers', '2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22'); await expectInputValue(page, 'input#environments', 'Dxi029djX, eZF99lH6'); await expectInputValue(page, 'input#lhc-fills', '1, 4, 6'); + // await expectInputValue(page, 'input#data-passes', 'LHC22b_apass1'); }); it('Should verify that form autofill all inputs with provided full parameters.', async () => { @@ -770,4 +771,19 @@ ${aob}\ expect(tags).to.lengthOf(2); expect(tags).to.have.members(['P2INFO', 'RC']); }); + + it.skip('can create a log with linked data passes', async () => { + await goToPage(page, 'log-create'); + + await fillInput(page, '#title', 'A log with data passes'); + await pressElement(page, '#text ~ .CodeMirror'); + await page.keyboard.type('Sample Text'); + await fillInput(page, 'input#data-passes', 'LHC22b_apass1, LHC22b_skimming'); + + await pressElement(page, '#send:not([disabled])'); + await page.waitForNavigation(); + + const lastLog = await getLastLog(); + expect(lastLog.dataPasses.map(({ name }) => name).sort()).to.eql(['LHC22b_apass1', 'LHC22b_skimming']); + }); };