From 787d106e0da829288532980b91111577695ebfed Mon Sep 17 00:00:00 2001 From: Alejandro Mariscal Romero <87366244+mariscalromeroalejandro@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:23:00 +0200 Subject: [PATCH 01/23] create sequelize repositories (data layer) --- .../database/repositories/BaseRepository.js | 41 +++ .../repositories/ChartOptionsRepository.js | 63 ++++ .../database/repositories/ChartRepository.js | 72 +++++ .../repositories/GridTabCellRepository.js | 106 +++++++ .../database/repositories/LayoutRepository.js | 290 ++++++++++++++++++ .../database/repositories/OptionRepository.js | 46 +++ .../database/repositories/TabRepository.js | 76 +++++ .../database/repositories/UserRepository.js | 45 +++ .../ChartOptionsRepository.test.js | 93 ++++++ .../repositories/ChartRepository.test.js | 86 ++++++ .../GridTabCellRepository.test.js | 103 +++++++ .../repositories/LayoutRepository.test.js | 68 ++++ .../repositories/OptionRepository.test.js | 121 ++++++++ .../repositories/TabRepository.test.js | 112 +++++++ .../repositories/UserRepository.test.js | 77 +++++ QualityControl/test/test-index.js | 20 +- 16 files changed, 1413 insertions(+), 6 deletions(-) create mode 100644 QualityControl/lib/database/repositories/BaseRepository.js create mode 100644 QualityControl/lib/database/repositories/ChartOptionsRepository.js create mode 100644 QualityControl/lib/database/repositories/ChartRepository.js create mode 100644 QualityControl/lib/database/repositories/GridTabCellRepository.js create mode 100644 QualityControl/lib/database/repositories/LayoutRepository.js create mode 100644 QualityControl/lib/database/repositories/OptionRepository.js create mode 100644 QualityControl/lib/database/repositories/TabRepository.js create mode 100644 QualityControl/lib/database/repositories/UserRepository.js create mode 100644 QualityControl/test/lib/database/repositories/ChartOptionsRepository.test.js create mode 100644 QualityControl/test/lib/database/repositories/ChartRepository.test.js create mode 100644 QualityControl/test/lib/database/repositories/GridTabCellRepository.test.js create mode 100644 QualityControl/test/lib/database/repositories/LayoutRepository.test.js create mode 100644 QualityControl/test/lib/database/repositories/OptionRepository.test.js create mode 100644 QualityControl/test/lib/database/repositories/TabRepository.test.js create mode 100644 QualityControl/test/lib/database/repositories/UserRepository.test.js diff --git a/QualityControl/lib/database/repositories/BaseRepository.js b/QualityControl/lib/database/repositories/BaseRepository.js new file mode 100644 index 000000000..9724a06bf --- /dev/null +++ b/QualityControl/lib/database/repositories/BaseRepository.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. + */ + +/** + * @typedef {import('sequelize').Model} Model - Generic Sequelize model type + */ + +/** + * Parent class for repositories + */ +export class BaseRepository { + /** + * Creates an instance of the BaseRepository class + * @param {Model} model Sequelize model to be used by the repository + * @throws {Error} Throws an error if model is not provided. + */ + constructor(model) { + if (!model) { + throw new Error('A Sequelize model must be provided to BaseRepository.'); + } + this._model = model; + } + + /** + * The Sequelize model associated with this repository. + * @returns {Model} the Sequelize model + */ + get model() { + return this._model; + } +} diff --git a/QualityControl/lib/database/repositories/ChartOptionsRepository.js b/QualityControl/lib/database/repositories/ChartOptionsRepository.js new file mode 100644 index 000000000..a481e3867 --- /dev/null +++ b/QualityControl/lib/database/repositories/ChartOptionsRepository.js @@ -0,0 +1,63 @@ +/** + * @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 { BaseRepository } from './BaseRepository.js'; + +/** + * @typedef {object} ChartOptionAttributes + * @property {number} chart_id chart ID + * @property {number} option_id - ID de la opción. + */ + +/** + * Repository for managing chart options. + */ +export class ChartOptionsRepository extends BaseRepository { + /** + * Creates an instance of the ChartOptionsRepository class + * @param {typeof ChartOption} chartOptionModel - Sequelize ChartOption model. + */ + constructor(chartOptionModel) { + super(chartOptionModel); + } + + /** + * Creates a new chart option. + * @param {Partial} optionData - Data for the new chart option. + * @returns {Promise} The created chart option. + */ + async createChartOption(optionData) { + return this.model.create(optionData); + } + + /** + * Finds all chart options by chart ID. + * @param {number} chartId - Chart identifier. + * @returns {Promise} List of chart options. + */ + async findChartOptionsByChartId(chartId) { + return this.model.findAll({ where: { chart_id: chartId } }); + } + + /** + * Deletes a chart option by chart and option ID. + * @param {object} params - identifiers + * @param {number} params.chartId - Chart identifier. + * @param {number} params.optionId - Option identifier. + * @returns {Promise} Number of deleted records. + */ + async deleteChartOption(params) { + const { chartId, optionId } = params; + return this.model.destroy({ where: { chart_id: chartId, option_id: optionId } }); + } +} diff --git a/QualityControl/lib/database/repositories/ChartRepository.js b/QualityControl/lib/database/repositories/ChartRepository.js new file mode 100644 index 000000000..da1178cdb --- /dev/null +++ b/QualityControl/lib/database/repositories/ChartRepository.js @@ -0,0 +1,72 @@ +/** + * @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 { BaseRepository } from './BaseRepository.js'; + +/** + * @typedef {object} ChartAttributes + * @property {string} id id of the chart + * @property {string} object_name name of the object + * @property {boolean} ignore_defaults whether to ignore defaults + */ + +/** + * Repository for managing chart options. + */ +export class ChartRepository extends BaseRepository { + /** + * Creates an instance of the ChartRepository class + * @param {typeof Chart} chartModel - Sequelize Chart model. + */ + constructor(chartModel) { + super(chartModel); + } + + /** + * Finds a chart by its ID. + * @param {string} chartId id of the chart + * @returns {Promise} The chart or null if not found. + */ + async findChartById(chartId) { + return this.model.findByPk(chartId); + } + + /** + * Creates a new chart. + * @param {Partial} chartData new chart + * @returns {Promise} The created chart. + */ + async createChart(chartData) { + return this.model.create(chartData); + } + + /** + * Updates an existing chart by ID. + * @param {string} chartId id of the chart + * @param {Partial} updateData new chart + * @returns {Promise} Number of updated rows (0 or 1). + */ + async updateChart(chartId, updateData) { + const [updatedCount] = await this.model.update(updateData, { where: { id: chartId } }); + return updatedCount; + } + + /** + * Deletes a chart by ID. + * @param {string} chartId id of the chart + * @returns {Promise} Number of deleted rows (0 or 1). + */ + async deleteChart(chartId) { + return this.model.destroy({ where: { id: chartId } }); + } +} diff --git a/QualityControl/lib/database/repositories/GridTabCellRepository.js b/QualityControl/lib/database/repositories/GridTabCellRepository.js new file mode 100644 index 000000000..c106b11b3 --- /dev/null +++ b/QualityControl/lib/database/repositories/GridTabCellRepository.js @@ -0,0 +1,106 @@ +import { BaseRepository } from './BaseRepository.js'; + +/** + * @typedef {object} GridTabCellAttributes + * @property {number} id + * @property {string} chart_id + * @property {number} row + * @property {number} col + * @property {string} tab_id + * @property {number} [row_span] + * @property {number} [col_span] + * @property {Date} created_at + * @property {Date} updated_at + */ + +/** + * Repository for managing grid tab cells. + */ +export class GridTabCellRepository extends BaseRepository { + /** + * Creates an instance of the GridTabCellRepository class + * @param {typeof GridTabCell} gridTabCellModel - Sequelize GridTabCell model + */ + constructor(gridTabCellModel) { + super(gridTabCellModel); + } + + /** + * Finds all grid tab cells by tab ID. + * @param {string} tabId id of the tab + * @returns {Promise} + */ + async findByTabId(tabId) { + return this.model.findAll({ where: { tab_id: tabId } }); + } + + /** + * Finds all grid tab cells by chart ID. + * @param {string} chartId id of the chart + * @returns {Promise} + */ + async findByChartId(chartId) { + return this.model.findAll({ where: { chart_id: chartId } }); + } + + /** + * Finds grid tab cells as a plain object by chart ID. + * @param {string} chartId id of the chart + * @returns {Promise} + */ + async findObjectByChartId(chartId) { + const include = [ + { + association: 'tab', + include: [{ association: 'layout', attributes: ['name'] }], + attributes: ['name'], + }, + { + association: 'chart', + include: [ + { + association: 'chartOptions', + include: [ + { + association: 'option', + attributes: ['name'], + }, + ], + }, + ], + attributes: ['object_name', 'ignore_defaults'], + }, + ]; + + return this.model.findAll({ where: { chart_id: chartId }, include }); + } + + /** + * Creates a new grid tab cell. + * @param {Partial} cellData new data + * @returns {Promise} + */ + async createGridTabCell(cellData) { + return this.model.create(cellData); + } + + /** + * Updates a grid tab cell by ID. + * @param {number} id + * @param {Partial} updateData updated data + * @returns {Promise} Number of updated rows + */ + async updateGridTabCell(id, updateData) { + const [updatedCount] = await this.model.update(updateData, { where: { id } }); + return updatedCount; + } + + /** + * Deletes a grid tab cell by ID. + * @param {number} id + * @returns {Promise} Number of deleted rows + */ + async deleteGridTabCell(id) { + return this.model.destroy({ where: { id } }); + } +} diff --git a/QualityControl/lib/database/repositories/LayoutRepository.js b/QualityControl/lib/database/repositories/LayoutRepository.js new file mode 100644 index 000000000..3b4146ec9 --- /dev/null +++ b/QualityControl/lib/database/repositories/LayoutRepository.js @@ -0,0 +1,290 @@ +/** + * @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 { BaseRepository } from './BaseRepository.js'; +import { Op } from 'sequelize'; + +/** + * @typedef {object} LayoutAttributes + * @property {string} id + * @property {string} name + * @property {string} [description] + * @property {boolean} display_timestamp + * @property {number} auto_tab_change_interval + * @property {string} owner_username + * @property {boolean} is_official + * @property {Date} created_at + * @property {Date} updated_at + */ + +/** + * Repository for managing layouts. + */ +export class LayoutRepository extends BaseRepository { + constructor(layoutModel) { + super(layoutModel); + + // Build common include structure for all find queries + this._layoutInfoToInclude = [ + { + association: 'tabs', + include: [ + { + association: 'gridTabCells', + include: [ + { + association: 'chart', + include: [ + { + association: 'chartOptions', + include: [{ association: 'option' }], + }, + ], + }, + ], + }, + ], + }, + { + association: 'owner', + attributes: ['id', 'username', 'name'], + }, + ]; + } + + /** + * Finds a layout by its ID + * @param {string} id id of the layout + * @returns {Promise} + */ + async findLayoutById(id) { + return this.model.findByPk(id, { include: this._layoutInfoToInclude }); + } + + /** + * Finds all layouts + * @returns {Promise} + */ + async findAllLayouts() { + return this.model.findAll({ include: this._layoutInfoToInclude }); + } + + //TODO: this replaces the listLayouts method + /** + * Finds layouts by filters using Op.and and optionally selects specific fields. + * @param {object[]} filters - Array of Sequelize filter objects + * @param {string[]} [fields] - Optional array of fields/columns to return + * @returns {Promise} + */ + async findLayoustByFilters(filters, fields) { + return this.model.findAll({ + where: { [Op.and]: filters }, + attributes: fields || undefined, // return all columns if fields not specified + include: this._layoutInfoToInclude, + }); + } + + /** + * Finds layouts by name + * @param {string} name name of the layout + * @returns {Promise} + */ + async findLayoutByName(name) { + return this.model.findOne({ + where: { name }, + include: this._layoutInfoToInclude, + }); + } + + /** + * Creates a new layout + * @param {Partial} layoutData new layout + * @returns {Promise} + */ + async createLayout(layoutData) { + return this.model.create(layoutData); + } + + /** + * Updates an existing layout by ID + * @param {string} id + * @param {Partial} updateData updated layout + * @returns {Promise} Number of updated rows + */ + async updateLayout(id, updateData) { + const [updatedCount] = await this.model.update(updateData, { where: { id } }); + return updatedCount; + } + + /** + * Deletes a layout by ID + * @param {string} id id of the layout to delete + * @returns {Promise} Number of deleted rows + */ + async deleteLayout(id) { + return this.model.destroy({ where: { id } }); + } +} + +// export class LayoutRepository extends BaseRepository { +// /** +// * Retrieves a filtered list of layouts with optional field selection +// * @param {object} [options] - Filtering and field selection options +// * @param {string} [options.name] - Filter layouts by exact name match +// * @param {Array} [options.fields] - Array of field names to include in each returned layout object +// * @param {object} [options.filter] - Filter layouts by containing filter.objectPath, case insensitive +// * @returns {Array} Array of layout objects matching the filters, containing only the specified fields +// */ +// listLayouts({ name, fields = [], filter } = {}) { +// const { layouts } = this._jsonFileService.data; +// const filteredLayouts = this._filterLayouts(layouts, { ...filter, name }); + +// if (fields.length === 0) { +// return filteredLayouts; +// } +// return filteredLayouts.map((layout) => { +// const layoutObj = {}; +// fields.forEach((field) => { +// layoutObj[field] = layout[field]; +// }); +// return layoutObj; +// }); +// } + +// /** +// * Filters layouts by filter object +// * @param {Array} layouts - Array of layouts to filter +// * @param {object} filter - Filtering object +// * @param {number} [filter.owner_id] - owner id to filter by +// * @param {string} [filter.name] - name to filter by +// * @param {string} [filter.objectPath] - object path prefix for potential objects to be contained by layout +// * @returns {Array} Filtered layouts. +// */ +// _filterLayouts(layouts, { owner_id, name, objectPath } = {}) { +// const objectPathLowerCase = objectPath?.toLowerCase(); +// return layouts.filter((layout) => { +// if (owner_id !== undefined && layout.owner_id !== owner_id) { +// return false; +// } +// if (name !== undefined && layout.name !== name) { +// return false; +// } +// if (objectPathLowerCase) { +// const hasMatchingObject = layout.tabs?.some((tab) => +// tab.objects?.some((obj) => +// obj.name?.toLowerCase().includes(objectPathLowerCase))); +// if (!hasMatchingObject) { +// return false; +// } +// } +// return true; +// }); +// } + +// /** +// * Retrieve a layout by its id or throws an error +// * @param {string} layoutId - layout id +// * @returns {Layout} - layout object +// * @throws {NotFoundError} - if the layout is not found +// */ +// readLayoutById(layoutId) { +// const foundLayout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId); +// if (!foundLayout) { +// throw new NotFoundError(`layout (${layoutId}) not found`); +// } +// return foundLayout; +// } + +// /** +// * Given a string, representing layout name, retrieve the layout if it exists +// * @param {string} layoutName - name of the layout to retrieve +// * @returns {Layout} - object with layout information +// * @throws +// */ +// readLayoutByName(layoutName) { +// const layout = this._jsonFileService.data.layouts.find((layout) => layout.name === layoutName); +// if (!layout) { +// throw new NotFoundError(`Layout (${layoutName}) not found`); +// } +// return layout; +// } + +// /** +// * Create a layout +// * @param {Layout} newLayout - layout object to be saved +// * @returns {object} Empty details +// */ +// async createLayout(newLayout) { +// if (!newLayout.id) { +// throw new Error('layout id is mandatory'); +// } +// if (!newLayout.name) { +// throw new Error('layout name is mandatory'); +// } + +// const layout = this._jsonFileService.data.layouts.find((layout) => layout.id === newLayout.id); +// if (layout) { +// throw new Error(`layout with this id (${layout.id}) already exists`); +// } +// this._jsonFileService.data.layouts.push(newLayout); +// await this._jsonFileService.writeToFile(); +// return newLayout; +// } + +// /** +// * Update a single layout by its id +// * @param {string} layoutId - id of the layout to be updated +// * @param {LayoutDto} newData - layout new data +// * @returns {string} id of the layout updated +// */ +// async updateLayout(layoutId, newData) { +// const layout = this.readLayoutById(layoutId); +// Object.assign(layout, newData); +// await this._jsonFileService.writeToFile(); +// return layoutId; +// } + +// /** +// * Delete a single layout by its id +// * @param {string} layoutId - id of the layout to be removed +// * @returns {string} id of the layout deleted +// */ +// async deleteLayout(layoutId) { +// const layout = this.readLayoutById(layoutId); +// const index = this._jsonFileService.data.layouts.indexOf(layout); +// this._jsonFileService.data.layouts.splice(index, 1); +// await this._jsonFileService.writeToFile(); +// return layoutId; +// } + +// /** +// * Return an object by its id that is saved within a layout +// * @param {string} id - id of the object to retrieve +// * @returns {{object: object, layoutName: string}} - object configuration stored +// */ +// getObjectById(id) { +// if (!id) { +// throw new Error('Missing mandatory parameter: id'); +// } +// for (const layout of this._jsonFileService.data.layouts) { +// for (const tab of layout.tabs) { +// for (const object of tab.objects) { +// if (object.id === id) { +// return { object, layoutName: layout.name, tabName: tab.name }; +// } +// } +// } +// } +// throw new Error(`Object with ${id} could not be found`); +// } +// } diff --git a/QualityControl/lib/database/repositories/OptionRepository.js b/QualityControl/lib/database/repositories/OptionRepository.js new file mode 100644 index 000000000..d900d6489 --- /dev/null +++ b/QualityControl/lib/database/repositories/OptionRepository.js @@ -0,0 +1,46 @@ +/** + * @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. + */ + +import { BaseRepository } from './BaseRepository.js'; + +/** + * @typedef {object} OptionAttributes + * @property {string} id + * @property {string} name + * @property {string} type + * @property {Date} created_at + * @property {Date} updated_at + */ + +/** + * Repository class for managing Option entities. + */ +export class OptionRepository extends BaseRepository { + /** + * Creates an instance of OptionRepository. + * @param {object} optionModel - The Sequelize model for options. + */ + constructor(optionModel) { + super(optionModel); + } + + /** + * Retrieves option by name + * @param {string} name The name of the option + * @returns {Promise