diff --git a/lib/domain/entities/GaqDetector.js b/lib/domain/entities/GaqDetector.js index 3478c2381c..54f6c26c6c 100644 --- a/lib/domain/entities/GaqDetector.js +++ b/lib/domain/entities/GaqDetector.js @@ -17,6 +17,4 @@ * @property {number} dataPassId * @property {number} runNumber * @property {number} detectorId - * @property {number} createdAt - * @property {number} updatedAt */ diff --git a/lib/server/api-docs/API.md b/lib/server/api-docs/API.md index 80b5632eb0..5441019907 100644 --- a/lib/server/api-docs/API.md +++ b/lib/server/api-docs/API.md @@ -491,3 +491,60 @@ Responses: ```bash curl -g "$BOOKKEEPING_URL/api/lhcPeriodsStatistics/1?token=$TOKEN" ``` + +## GAQ detectors + +Detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass. + +### `GET /gaqDetectors` - List the detectors contributing to the GAQ of a run in a data pass + +Operation id: `listGaqDetectors` + +Returns the detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass, sorted by name. Not paginated. +The list is empty when no detector is set for this run and data pass, **or when the run or the data pass does not exist** (never `404`). + +| Parameter | In | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| `dataPassId` | query | integer | yes | Data pass identifier. | +| `runNumber` | query | integer | yes | Run number. | + +Responses: + +- `200`: The detectors. +- `400`: The request failed validation. HTTP status is 400 although each error's `status` field is `"422"`. `source.pointer` has the form `/data/attributes//`. + +```bash +curl -g "$BOOKKEEPING_URL/api/gaqDetectors?dataPassId=3&runNumber=56&token=$TOKEN" +``` + +### `POST /gaqDetectors` - Set the detectors contributing to the GAQ of runs in a data pass (not for LLM use) + +Operation id: `setGaqDetectors` + +Requires the role `admin` or `gaq`. For each given run, replaces the detectors contributing to the GAQ in the data pass by the given ones. +Every run must be linked to the data pass and have every given detector. + +**Not meant for LLM or MCP tool use** (`x-llm-exclude`). + +**Required role:** `admin`, `gaq` + +Request body (JSON): + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `dataPassId` | integer | yes | | +| `runNumbers` | array | yes | Run numbers of the runs to update, without duplicates. | +| `detectorIds` | array | yes | Ids of the detectors contributing to the GAQ, without duplicates. | + +Responses: + +- `201`: The created associations between runs and detectors, one per run and detector. +- `400`: The request failed validation (see the validation errors format), **or** a run is not linked to the data pass, a detector does not exist or a run does not have a given detector. In these last cases the error has `title: Service unavailable` and the reason in `detail`. +- `403`: The token lacks the role required by this operation. +- `404`: The data pass does not exist. + +```bash +curl -g -X POST "$BOOKKEEPING_URL/api/gaqDetectors?token=$TOKEN" \ + -H 'content-type: application/json' \ + -d '{"dataPassId":3,"runNumbers":[49,56],"detectorIds":[4,7]}' +``` diff --git a/lib/server/api-docs/index.html b/lib/server/api-docs/index.html index 39c3c7d91c..0025838022 100644 --- a/lib/server/api-docs/index.html +++ b/lib/server/api-docs/index.html @@ -896,6 +896,96 @@

LHC periods statistics

curl -g "$BOOKKEEPING_URL/api/lhcPeriodsStatistics/1?token=$TOKEN"
 
+

GAQ detectors

+

Detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass.

+
+

GET /gaqDetectors - List the detectors contributing to the GAQ of a run in a data pass

+ +

Operation id: listGaqDetectors

+

Returns the detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass, sorted by name. Not paginated. +The list is empty when no detector is set for this run and data pass, or when the run or the data pass does not exist (never 404).

+ + + + + + + + + + + + + + + + + + + + + + + + +
ParameterInTypeRequiredNotes
dataPassIdqueryintegeryesData pass identifier.
runNumberqueryintegeryesRun number.
+

Responses:

+
    +
  • 200: The detectors.
  • +
  • 400: The request failed validation. HTTP status is 400 although each error's status field is "422". source.pointer has the form /data/attributes/<query|params|body>/<field>.
  • +
+
curl -g "$BOOKKEEPING_URL/api/gaqDetectors?dataPassId=3&runNumber=56&token=$TOKEN"
+
+
+
+

POST /gaqDetectors - Set the detectors contributing to the GAQ of runs in a data pass not for LLM use

+ +

Operation id: setGaqDetectors

+

Requires the role admin or gaq. For each given run, replaces the detectors contributing to the GAQ in the data pass by the given ones. +Every run must be linked to the data pass and have every given detector.

+

Not meant for LLM or MCP tool use (x-llm-exclude).

+

Required role: admin, gaq

+

Request body (JSON):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredNotes
dataPassIdintegeryes
runNumbersarrayyesRun numbers of the runs to update, without duplicates.
detectorIdsarrayyesIds of the detectors contributing to the GAQ, without duplicates.
+

Responses:

+
    +
  • 201: The created associations between runs and detectors, one per run and detector.
  • +
  • 400: The request failed validation (see the validation errors format), or a run is not linked to the data pass, a detector does not exist or a run does not have a given detector. In these last cases the error has title: Service unavailable and the reason in detail.
  • +
  • 403: The token lacks the role required by this operation.
  • +
  • 404: The data pass does not exist.
  • +
+
curl -g -X POST "$BOOKKEEPING_URL/api/gaqDetectors?token=$TOKEN" \
+  -H 'content-type: application/json' \
+  -d '{"dataPassId":3,"runNumbers":[49,56],"detectorIds":[4,7]}'
+
+
Generated from openapi.yaml by npm run docs:api · ALICE Bookkeeping · internal use only
diff --git a/lib/server/api-docs/openapi.yaml b/lib/server/api-docs/openapi.yaml index 86cbd3d200..bbb30a0192 100644 --- a/lib/server/api-docs/openapi.yaml +++ b/lib/server/api-docs/openapi.yaml @@ -53,6 +53,8 @@ tags: description: Monte Carlo simulation productions. - name: LHC periods statistics description: LHC periods with statistics on their physics runs. + - name: GAQ detectors + description: Detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass. paths: /tags: @@ -606,6 +608,84 @@ paths: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } + /gaqDetectors: + get: + tags: [GAQ detectors] + operationId: listGaqDetectors + summary: List the detectors contributing to the GAQ of a run in a data pass + description: | + Returns the detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass, sorted by name. Not paginated. + The list is empty when no detector is set for this run and data pass, **or when the run or the data pass does not exist** (never `404`). + parameters: + - name: dataPassId + in: query + required: true + description: Data pass identifier. + schema: { type: integer, minimum: 1, examples: [3] } + - name: runNumber + in: query + required: true + description: Run number. + schema: { type: integer, minimum: 1, examples: [56] } + responses: + '200': + description: The detectors. + content: + application/json: + schema: { $ref: '#/components/schemas/DetectorListResponse' } + example: + data: + - { id: 7, name: FT0, type: PHYSICAL } + - { id: 4, name: ITS, type: PHYSICAL } + '400': { $ref: '#/components/responses/ValidationError' } + post: + tags: [GAQ detectors] + operationId: setGaqDetectors + x-llm-exclude: true + x-required-roles: [admin, gaq] + summary: Set the detectors contributing to the GAQ of runs in a data pass + description: | + Requires the role `admin` or `gaq`. For each given run, replaces the detectors contributing to the GAQ in the data pass by the given ones. + Every run must be linked to the data pass and have every given detector. + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/SetGaqDetectors' } + example: + dataPassId: 3 + runNumbers: [49, 56] + detectorIds: [4, 7] + responses: + '201': + description: The created associations between runs and detectors, one per run and detector. + content: + application/json: + schema: { $ref: '#/components/schemas/GaqDetectorEntriesResponse' } + example: + data: + - { dataPassId: 3, runNumber: 49, detectorId: 4 } + - { dataPassId: 3, runNumber: 49, detectorId: 7 } + '400': + description: | + The request failed validation (see the validation errors format), **or** a run is not linked to the data pass, a detector does not exist or + a run does not have a given detector. In these last cases the error has `title: Service unavailable` and the reason in `detail`. + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + example: + errors: + - { status: 400, title: Service unavailable, detail: 'No association between data pass with id 3 and following runs: 1' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': + description: The data pass does not exist. + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + example: + errors: + - { status: 404, title: Not found, detail: Data pass with this id (99999) could not be found } + components: securitySchemes: jwtQuery: @@ -936,3 +1016,32 @@ components: type: array items: { $ref: '#/components/schemas/LhcPeriodStatistics' } meta: { $ref: '#/components/schemas/PageMeta' } + SetGaqDetectors: + type: object + required: [dataPassId, runNumbers, detectorIds] + properties: + dataPassId: { type: integer, minimum: 1 } + runNumbers: + type: array + description: Run numbers of the runs to update, without duplicates. + uniqueItems: true + items: { type: integer, minimum: 1 } + detectorIds: + type: array + description: Ids of the detectors contributing to the GAQ, without duplicates. + uniqueItems: true + items: { type: integer, minimum: 1 } + GaqDetectorEntry: + type: object + required: [dataPassId, runNumber, detectorId] + properties: + dataPassId: { type: integer } + runNumber: { type: integer } + detectorId: { type: integer } + GaqDetectorEntriesResponse: + type: object + required: [data] + properties: + data: + type: array + items: { $ref: '#/components/schemas/GaqDetectorEntry' } diff --git a/lib/server/controllers/gaqDetectors.controller.js b/lib/server/controllers/gaqDetectors.controller.js index 04056f3b2b..39c2259cd4 100644 --- a/lib/server/controllers/gaqDetectors.controller.js +++ b/lib/server/controllers/gaqDetectors.controller.js @@ -13,6 +13,7 @@ const Joi = require('joi'); const { DtoFactory } = require('../../domain/dtos/DtoFactory.js'); +const EntityIdDto = require('../../domain/dtos/EntityIdDto.js'); const { dtoValidator } = require('../utilities/dtoValidator.js'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js'); const { gaqDetectorService } = require('../services/gaq/GaqDetectorsService.js'); @@ -26,9 +27,9 @@ const { gaqDetectorService } = require('../services/gaq/GaqDetectorsService.js') const setGaqDetectorsHandler = async (req, res) => { const validatedDTO = await dtoValidator( DtoFactory.bodyOnly(Joi.object({ - dataPassId: Joi.number(), - runNumbers: Joi.array().items(Joi.number()).unique(), - detectorIds: Joi.array().items(Joi.number()).unique(), + dataPassId: EntityIdDto.required(), + runNumbers: Joi.array().items(EntityIdDto).unique().required(), + detectorIds: Joi.array().items(EntityIdDto).unique().required(), })), req, res, @@ -63,8 +64,8 @@ const setGaqDetectorsHandler = async (req, res) => { const listGaqDetectorsHandler = async (req, res) => { const validatedDTO = await dtoValidator( DtoFactory.queryOnly(Joi.object({ - dataPassId: Joi.number().required(), - runNumber: Joi.number().required(), + dataPassId: EntityIdDto.required(), + runNumber: EntityIdDto.required(), })), req, res, diff --git a/lib/server/services/gaq/GaqDetectorsService.js b/lib/server/services/gaq/GaqDetectorsService.js index 99108bfddd..d92afeafcc 100644 --- a/lib/server/services/gaq/GaqDetectorsService.js +++ b/lib/server/services/gaq/GaqDetectorsService.js @@ -12,11 +12,16 @@ */ const { gaqDetectorAdapter, detectorAdapter } = require('../../../database/adapters'); -const { GaqDetectorRepository, RunRepository, DetectorRepository } = require('../../../database/repositories'); const { BadParameterError } = require('../../errors/BadParameterError'); const { dataSource } = require('../../../database/DataSource.js'); -const { Op } = require('sequelize'); const { getOneDataPassOrFail } = require('../dataPasses/getOneDataPassOrFail'); +const { getAllDetectors } = require('../detector/getAllDetectors.js'); +const { createGaqDetectors } = require('./createGaqDetectors.js'); +const { getAllGaqDetectors } = require('./getAllGaqDetectors.js'); +const { getRunNumbersLinkedToDataPass } = require('./getRunNumbersLinkedToDataPass.js'); +const { getRunsWithDefaultGaqDetectors } = require('./getRunsWithDefaultGaqDetectors.js'); +const { getRunsWithDetectorsByIds } = require('./getRunsWithDetectorsByIds.js'); +const { removeGaqDetectors } = require('./removeGaqDetectors.js'); /** * List of default GAQ detectors for pp runs @@ -39,7 +44,7 @@ class GaqDetectorService { * @param {number} dataPassId data pass id * @param {number[]} runNumbers list of run numbers * @param {number[]} detectorIds list of ids of detectors included in GAQ computation for the given runs and data pass - * @return {Promise} promise resolved once association between data pass, runs and detectors was set + * @return {Promise} promise resolved with the created associations between data pass, runs and detectors */ setGaqDetectors(dataPassId, runNumbers, detectorIds) { return dataSource.transaction(async () => { @@ -47,16 +52,11 @@ class GaqDetectorService { await this._verifyDataPassAndRunsAssociation(dataPassId, runNumbers); await this._verifyRunsAndDetectorsAssociations(runNumbers, detectorIds); - await GaqDetectorRepository.removeAll({ - where: { - dataPassId, - runNumber: { [Op.in]: runNumbers }, - }, - }); + await removeGaqDetectors(dataPassId, runNumbers); const gaqEntries = runNumbers .flatMap((runNumber) => detectorIds .map((detectorId) => ({ dataPassId, runNumber, detectorId }))); - const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + const createdEntries = await createGaqDetectors(gaqEntries); return createdEntries.map(gaqDetectorAdapter.toEntity); }); } @@ -70,37 +70,24 @@ class GaqDetectorService { * @param {number[]} runNumbers list of run numbers * @param {object} [options] additional options * @param {sequelize.Transaction} [options.transaction] sequelize transaction - * @return {Promise} promise resolved once association between data pass, runs and detectors was set + * @return {Promise} promise resolved with the created associations between data pass, runs and detectors */ useDefaultGaqDetectors(dataPassId, runNumbers, { transaction } = {}) { return dataSource.transaction(async () => { await getOneDataPassOrFail({ id: dataPassId }); await this._verifyDataPassAndRunsAssociation(dataPassId, runNumbers); - await GaqDetectorRepository.removeAll({ - where: { - dataPassId, - runNumber: { [Op.in]: runNumbers }, - }, - }); + await removeGaqDetectors(dataPassId, runNumbers); - const runsWithDetectors = await RunRepository.findAll({ - where: { runNumber: { [Op.in]: runNumbers } }, - include: [ - { - association: 'detectors', - where: { [Op.or]: [ - { '$Run.pdp_beam_type$': 'pp', name: { [Op.in]: DEFAULT_GAQ_DETECTORS_FOR_PROTON_PROTON_RUNS } }, - { '$Run.pdp_beam_type$': 'PbPb', name: { [Op.in]: DEFAULT_GAQ_DETECTORS_FOR_LEAD_LEAD_RUNS } }, - ] }, - }, - ], + const runsWithDetectors = await getRunsWithDefaultGaqDetectors(runNumbers, { + pp: DEFAULT_GAQ_DETECTORS_FOR_PROTON_PROTON_RUNS, + PbPb: DEFAULT_GAQ_DETECTORS_FOR_LEAD_LEAD_RUNS, }); const gaqEntries = runsWithDetectors .flatMap(({ runNumber, detectors }) => detectors .map(({ id: detectorId }) => ({ dataPassId, runNumber, detectorId }))); - const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + const createdEntries = await createGaqDetectors(gaqEntries); return createdEntries.map(gaqDetectorAdapter.toEntity); }, { transaction }); } @@ -113,13 +100,11 @@ class GaqDetectorService { * @return {Promise[]>} promise of detectors */ async getGaqDetectors(dataPassId, runNumber) { - const queryBuilder = dataSource.createQueryBuilder() + const gaqDetectors = await getAllGaqDetectors((queryBuilder) => queryBuilder .where('dataPassId').is(dataPassId) .where('runNumber').is(runNumber) .include({ association: 'detector' }) - .orderBy((sequelize) => sequelize.col('`detector`.name'), 'ASC'); - - const gaqDetectors = await GaqDetectorRepository.findAll(queryBuilder); + .orderBy((sequelize) => sequelize.col('`detector`.name'), 'ASC')); return gaqDetectors.map(({ detector }) => detectorAdapter.toMinifiedEntity(detector)); } @@ -132,12 +117,7 @@ class GaqDetectorService { * @throws {BadParameterError} if some association does not exist */ async _verifyDataPassAndRunsAssociation(dataPassId, runNumbers) { - const validRunNumbers = await RunRepository.findAll({ - attributes: ['runNumber'], - where: { runNumber: { [Op.in]: runNumbers } }, - include: [{ association: 'dataPass', where: { id: dataPassId }, required: true }], - }); - const validRunNumbersSet = new Set(validRunNumbers.map(({ runNumber }) => runNumber)); + const validRunNumbersSet = new Set(await getRunNumbersLinkedToDataPass(dataPassId, runNumbers)); const invalidRunNumbers = runNumbers.filter((runNumber) => !validRunNumbersSet.has(runNumber)); if (invalidRunNumbers.length > 0) { const errorMessage = `No association between data pass with id ${dataPassId} and following runs: ${invalidRunNumbers}`; @@ -156,9 +136,9 @@ class GaqDetectorService { * @throws {BadParameterError} if some association does not exist */ async _verifyRunsAndDetectorsAssociations(runNumbers, detectorIds) { - const detectors = await DetectorRepository.findAll({ - attributes: ['id', 'name'], - where: { id: { [Op.in]: detectorIds } }, + const detectors = await getAllDetectors((queryBuilder) => { + queryBuilder.set('attributes', ['id', 'name']); + queryBuilder.where('id').oneOf(...detectorIds); }); const validDetectorIdsSet = new Set(detectors.map(({ id }) => id)); const invalidIds = detectorIds.filter((detectorId) => !validDetectorIdsSet.has(detectorId)); @@ -167,19 +147,8 @@ class GaqDetectorService { } const validDetectorNames = detectors.map(({ name }) => name); - const runAndDetector = (await RunRepository.findAll({ - attributes: ['runNumber'], - where: { runNumber: { [Op.in]: runNumbers } }, - include: [ - { - association: 'detectors', - attributes: ['name'], - where: { id: { [Op.in]: detectorIds } }, - through: { attributes: [] }, - required: false, - }, - ], - })).map(({ runNumber, detectors }) => [runNumber, detectors.map(({ name }) => name)]); + const runAndDetector = (await getRunsWithDetectorsByIds(runNumbers, detectorIds)) + .map(({ runNumber, detectors }) => [runNumber, detectors.map(({ name }) => name)]); const validRunToDetectors = Object.fromEntries(runAndDetector); diff --git a/lib/server/services/gaq/createGaqDetectors.js b/lib/server/services/gaq/createGaqDetectors.js new file mode 100644 index 0000000000..b531b14d6c --- /dev/null +++ b/lib/server/services/gaq/createGaqDetectors.js @@ -0,0 +1,22 @@ +/** + * @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 { GaqDetectorRepository } = require('../../../database/repositories/index.js'); + +/** + * Create GAQ (global aggregated quality) detectors entries + * + * @param {{dataPassId: number, runNumber: number, detectorId: number}[]} gaqDetectors the entries to create + * @return {Promise} resolves with the entries created + */ +exports.createGaqDetectors = (gaqDetectors) => GaqDetectorRepository.insertAll(gaqDetectors); diff --git a/lib/server/services/gaq/getAllGaqDetectors.js b/lib/server/services/gaq/getAllGaqDetectors.js new file mode 100644 index 0000000000..357b7251fa --- /dev/null +++ b/lib/server/services/gaq/getAllGaqDetectors.js @@ -0,0 +1,32 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { dataSource } = require('../../../database/DataSource.js'); +const { GaqDetectorRepository } = require('../../../database/repositories/index.js'); + +/** + * Find and return the GAQ (global aggregated quality) detectors entries + * + * @param {(function(QueryBuilder):void)|null} [qbConfiguration=null] function called with the GAQ detectors find query builder as parameter to + * add specific configuration to the query + * @return {Promise} resolves with the GAQ detectors entries found + */ +exports.getAllGaqDetectors = (qbConfiguration = null) => { + const queryBuilder = dataSource.createQueryBuilder(); + + if (qbConfiguration) { + qbConfiguration(queryBuilder); + } + + return GaqDetectorRepository.findAll(queryBuilder); +}; diff --git a/lib/server/services/gaq/getRunNumbersLinkedToDataPass.js b/lib/server/services/gaq/getRunNumbersLinkedToDataPass.js new file mode 100644 index 0000000000..ba5ade0bc5 --- /dev/null +++ b/lib/server/services/gaq/getRunNumbersLinkedToDataPass.js @@ -0,0 +1,32 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { Op } = require('sequelize'); +const { RunRepository } = require('../../../database/repositories/index.js'); + +/** + * Return, among the given run numbers, the ones of the runs linked to a data pass + * + * @param {number} dataPassId the id of the data pass + * @param {number[]} runNumbers the run numbers to check + * @return {Promise} resolves with the run numbers of the runs linked to the data pass + */ +exports.getRunNumbersLinkedToDataPass = async (dataPassId, runNumbers) => { + const runs = await RunRepository.findAll({ + attributes: ['runNumber'], + where: { runNumber: { [Op.in]: runNumbers } }, + include: [{ association: 'dataPass', where: { id: dataPassId }, required: true }], + }); + + return runs.map(({ runNumber }) => runNumber); +}; diff --git a/lib/server/services/gaq/getRunsWithDefaultGaqDetectors.js b/lib/server/services/gaq/getRunsWithDefaultGaqDetectors.js new file mode 100644 index 0000000000..fd91036c8f --- /dev/null +++ b/lib/server/services/gaq/getRunsWithDefaultGaqDetectors.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. + */ + +const { Op } = require('sequelize'); +const { RunRepository } = require('../../../database/repositories/index.js'); + +/** + * Find the given runs with, as detectors, the ones that are in the list of the default GAQ detectors of the PDP beam type of the run + * + * @param {number[]} runNumbers the run numbers of the runs to find + * @param {Object} defaultDetectorsNamesByPdpBeamType for each PDP beam type, the names of its default GAQ detectors. Runs of + * another PDP beam type are not returned + * @return {Promise} resolves with the runs found, each with its default GAQ `detectors` + */ +exports.getRunsWithDefaultGaqDetectors = (runNumbers, defaultDetectorsNamesByPdpBeamType) => RunRepository.findAll({ + where: { runNumber: { [Op.in]: runNumbers } }, + include: [ + { + association: 'detectors', + where: { + [Op.or]: Object.entries(defaultDetectorsNamesByPdpBeamType).map(([pdpBeamType, detectorsNames]) => ({ + '$Run.pdp_beam_type$': pdpBeamType, + name: { [Op.in]: detectorsNames }, + })), + }, + }, + ], +}); diff --git a/lib/server/services/gaq/getRunsWithDetectorsByIds.js b/lib/server/services/gaq/getRunsWithDetectorsByIds.js new file mode 100644 index 0000000000..f3947fc92c --- /dev/null +++ b/lib/server/services/gaq/getRunsWithDetectorsByIds.js @@ -0,0 +1,36 @@ +/** + * @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 { Op } = require('sequelize'); +const { RunRepository } = require('../../../database/repositories/index.js'); + +/** + * Find the given runs with their detectors (name only) restricted to the ones of the given ids + * + * @param {number[]} runNumbers the run numbers of the runs to find + * @param {number[]} detectorIds the ids of the detectors to include, the other detectors of the runs are ignored + * @return {Promise} resolves with the runs found, each with its `detectors` (an empty list if it has none of the given ones) + */ +exports.getRunsWithDetectorsByIds = (runNumbers, detectorIds) => RunRepository.findAll({ + attributes: ['runNumber'], + where: { runNumber: { [Op.in]: runNumbers } }, + include: [ + { + association: 'detectors', + attributes: ['name'], + where: { id: { [Op.in]: detectorIds } }, + through: { attributes: [] }, + required: false, + }, + ], +}); diff --git a/lib/server/services/gaq/removeGaqDetectors.js b/lib/server/services/gaq/removeGaqDetectors.js new file mode 100644 index 0000000000..4a55b2f8dc --- /dev/null +++ b/lib/server/services/gaq/removeGaqDetectors.js @@ -0,0 +1,31 @@ +/** + * @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 { Op } = require('sequelize'); +const { GaqDetectorRepository } = require('../../../database/repositories/index.js'); + +/** + * Remove all the GAQ (global aggregated quality) detectors entries of the given runs for a data pass + * + * @param {number} dataPassId the id of the data pass + * @param {number[]} runNumbers the run numbers of the runs for which entries must be removed + * @return {Promise} resolves once the entries have been removed + */ +exports.removeGaqDetectors = async (dataPassId, runNumbers) => { + await GaqDetectorRepository.removeAll({ + where: { + dataPassId, + runNumber: { [Op.in]: runNumbers }, + }, + }); +}; diff --git a/test/api/gaqDetectors.test.js b/test/api/gaqDetectors.test.js index b146c07343..46a0ddd32c 100644 --- a/test/api/gaqDetectors.test.js +++ b/test/api/gaqDetectors.test.js @@ -18,6 +18,16 @@ const { resetDatabaseContent } = require('../utilities/resetDatabaseContent.js') const { DetectorType } = require('../../lib/domain/enums/DetectorTypes'); const { BkpRoles } = require('../../lib/domain/enums/BkpRoles'); +/** + * Return the validation errors of a response as an object mapping the pointer of each error to its detail + * + * @param {object} response the response + * @return {Object} the details of the errors by pointer + */ +const getValidationErrors = (response) => Object.fromEntries( + response.body.errors.map(({ source: { pointer }, detail }) => [pointer.replace('/data/attributes/', ''), detail]), +); + module.exports = () => { before(resetDatabaseContent); @@ -36,6 +46,43 @@ module.exports = () => { .flatMap((runNumber) => detectorIds.map((detectorId) => ({ dataPassId, runNumber, detectorId })))); }); + it('should fail to set GAQ detectors if a field is missing', async () => { + const response = await request(server).post(`/api/gaqDetectors?token=${BkpRoles.GAQ}`).send({}); + + expect(response.status).to.be.equal(400); + expect(getValidationErrors(response)).to.eql({ + 'body/dataPassId': '"body.dataPassId" is required', + 'body/runNumbers': '"body.runNumbers" is required', + 'body/detectorIds': '"body.detectorIds" is required', + }); + }); + + it('should fail to set GAQ detectors if an id is not a positive integer or is duplicated', async () => { + let response = await request(server).post(`/api/gaqDetectors?token=${BkpRoles.GAQ}`).send({ + dataPassId: 0, + runNumbers: [0], + detectorIds: [-1], + }); + expect(response.status).to.be.equal(400); + expect(getValidationErrors(response)).to.eql({ + 'body/dataPassId': '"body.dataPassId" must be a positive number', + 'body/runNumbers/0': '"body.runNumbers[0]" must be a positive number', + 'body/detectorIds/0': '"body.detectorIds[0]" must be a positive number', + }); + + response = await request(server).post(`/api/gaqDetectors?token=${BkpRoles.GAQ}`).send({ + dataPassId: 'abc', + runNumbers: [1.5], + detectorIds: [1, 1], + }); + expect(response.status).to.be.equal(400); + expect(getValidationErrors(response)).to.eql({ + 'body/dataPassId': '"body.dataPassId" must be a number', + 'body/runNumbers/0': '"body.runNumbers[0]" must be an integer', + 'body/detectorIds/1': '"body.detectorIds[1]" contains a duplicate value', + }); + }); + it('should fail to set GAQ detectors because of insufficient permission', async () => { const dataPassId = 3; const runNumbers = [49, 56]; @@ -53,6 +100,26 @@ module.exports = () => { }); describe('GET /api/gaqDetectors', () => { + it('should return 400 if the data pass id or the run number is missing', async () => { + const response = await request(server).get('/api/gaqDetectors'); + + expect(response.status).to.equal(400); + expect(getValidationErrors(response)).to.eql({ + 'query/dataPassId': '"query.dataPassId" is required', + 'query/runNumber': '"query.runNumber" is required', + }); + }); + + it('should return 400 if the data pass id or the run number is not a positive integer', async () => { + const response = await request(server).get('/api/gaqDetectors?dataPassId=-1&runNumber=abc'); + + expect(response.status).to.equal(400); + expect(getValidationErrors(response)).to.eql({ + 'query/dataPassId': '"query.dataPassId" must be a positive number', + 'query/runNumber': '"query.runNumber" must be a number', + }); + }); + it('should return 200 with the list of GAQ detectors', async () => { const response = await request(server).get('/api/gaqDetectors?dataPassId=3&runNumber=56'); diff --git a/test/lib/server/services/gaq/gaqDetectorsQueries.test.js b/test/lib/server/services/gaq/gaqDetectorsQueries.test.js new file mode 100644 index 0000000000..99d8b91036 --- /dev/null +++ b/test/lib/server/services/gaq/gaqDetectorsQueries.test.js @@ -0,0 +1,97 @@ +/** + * @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 { getAllGaqDetectors } = require('../../../../../lib/server/services/gaq/getAllGaqDetectors.js'); +const { createGaqDetectors } = require('../../../../../lib/server/services/gaq/createGaqDetectors.js'); +const { removeGaqDetectors } = require('../../../../../lib/server/services/gaq/removeGaqDetectors.js'); +const { getRunNumbersLinkedToDataPass } = require('../../../../../lib/server/services/gaq/getRunNumbersLinkedToDataPass.js'); +const { getRunsWithDetectorsByIds } = require('../../../../../lib/server/services/gaq/getRunsWithDetectorsByIds.js'); +const { getRunsWithDefaultGaqDetectors } = require('../../../../../lib/server/services/gaq/getRunsWithDefaultGaqDetectors.js'); +const { runService } = require('../../../../../lib/server/services/run/RunService.js'); + +const DATA_PASS_ID = 3; + +/** + * Return the GAQ detectors entries of a run for the data pass used in the tests + * + * @param {number} runNumber the run number + * @return {Promise} the entries + */ +const getEntriesOfRun = (runNumber) => getAllGaqDetectors((queryBuilder) => { + queryBuilder.where('dataPassId').is(DATA_PASS_ID); + queryBuilder.where('runNumber').is(runNumber); +}); + +module.exports = () => { + describe('getAllGaqDetectors, createGaqDetectors and removeGaqDetectors', () => { + it('should successfully remove, create and find the GAQ detectors entries of a run', async () => { + await removeGaqDetectors(DATA_PASS_ID, [49]); + expect(await getEntriesOfRun(49)).to.eql([]); + + const created = await createGaqDetectors([ + { dataPassId: DATA_PASS_ID, runNumber: 49, detectorId: 4 }, + { dataPassId: DATA_PASS_ID, runNumber: 49, detectorId: 7 }, + ]); + expect(created).to.lengthOf(2); + + const entries = await getEntriesOfRun(49); + expect(entries.map(({ detectorId }) => detectorId)).to.have.members([4, 7]); + + await removeGaqDetectors(DATA_PASS_ID, [49]); + expect(await getEntriesOfRun(49)).to.eql([]); + }); + }); + + describe('getRunNumbersLinkedToDataPass', () => { + it('should return only the run numbers of the runs linked to the data pass', async () => { + expect(await getRunNumbersLinkedToDataPass(DATA_PASS_ID, [49, 56, 1])).to.have.members([49, 56]); + }); + + it('should return an empty list when no run is linked to the data pass', async () => { + expect(await getRunNumbersLinkedToDataPass(DATA_PASS_ID, [1])).to.eql([]); + }); + }); + + describe('getRunsWithDetectorsByIds', () => { + it('should return the runs with only the requested detectors', async () => { + const [run] = await getRunsWithDetectorsByIds([56], [4, 7]); + + expect(run.runNumber).to.equal(56); + expect(run.detectors.map(({ name }) => name)).to.have.members(['ITS', 'FT0']); + }); + + it('should return the runs with no detectors when they have none of the requested ones', async () => { + const [run] = await getRunsWithDetectorsByIds([56], [1]); + + expect(run.runNumber).to.equal(56); + expect(run.detectors).to.eql([]); + }); + }); + + describe('getRunsWithDefaultGaqDetectors', () => { + it('should return the runs with only the default GAQ detectors of their PDP beam type', async () => { + await runService.create({ runNumber: 777780, pdpBeamType: 'pp', detectors: 'CPV,TPC,ITS' }); + await runService.create({ runNumber: 777781, pdpBeamType: 'PbPb', detectors: 'CPV,TPC,ZDC' }); + // None of its detectors is a default one: the run is not returned + await runService.create({ runNumber: 777782, pdpBeamType: 'pp', detectors: 'CPV' }); + + const runs = await getRunsWithDefaultGaqDetectors([777780, 777781, 777782], { pp: ['TPC', 'FT0'], PbPb: ['ZDC'] }); + const detectorsNamesByRun = Object.fromEntries( + runs.map(({ runNumber, detectors }) => [runNumber, detectors.map(({ name }) => name)]), + ); + + expect(detectorsNamesByRun).to.eql({ 777780: ['TPC'], 777781: ['ZDC'] }); + }); + }); +}; diff --git a/test/lib/server/services/gaq/index.js b/test/lib/server/services/gaq/index.js index 985f80fe84..42c939a992 100644 --- a/test/lib/server/services/gaq/index.js +++ b/test/lib/server/services/gaq/index.js @@ -11,8 +11,13 @@ * or submit itself to any jurisdiction. */ +const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent.js'); const GaqDetectorServiceSuite = require('./GaqDetectorService.test.js'); +const GaqDetectorsQueriesSuite = require('./gaqDetectorsQueries.test.js'); module.exports = () => { + after(resetDatabaseContent); + describe('GaqDetectorService', GaqDetectorServiceSuite); + describe('GAQ detectors small functions', GaqDetectorsQueriesSuite); };