Skip to content

Commit

Permalink
[O2B-1200] Add QC flags creation API (#1499)
Browse files Browse the repository at this point in the history
* refactor

* typos

* add testS

* fix

* fix

* test

* refactor'

* fix

* refactor

* refactor

* add controller

* test

* test

* more rigid

* mr

* cleanup

* ceanup

* refactor

* refactor

* cleanup

* ref

* ch err

* test

* use default timestamps

* refactor

* refactor

* fix

* docs

* handle missing timestamps

* typo

* dates

* test

* test

* test

* cleanup

* rename

* date

* syntax

* rename

* correct

* correct

* typo

* typo
  • Loading branch information
xsalonx authored Apr 10, 2024
1 parent dac7be0 commit 0c6da29
Show file tree
Hide file tree
Showing 5 changed files with 712 additions and 7 deletions.
54 changes: 54 additions & 0 deletions lib/server/controllers/qcFlag.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,61 @@ const listQcFlagsHandler = async (req, res) => {
}
};

// eslint-disable-next-line valid-jsdoc
/**
* Create QcFlag
*/
const createQcFlagHandler = async (req, res) => {
const validatedDTO = await dtoValidator(
DtoFactory.bodyOnly(Joi.object({
from: Joi.number().required().allow(null),
to: Joi.number().required().allow(null),
comment: Joi.string().optional().allow(null),
flagTypeId: Joi.number().required(),
runNumber: Joi.number().required(),
dplDetectorId: Joi.number().required(),
dataPassId: Joi.number(),
simulationPassId: Joi.number(),
}).xor('dataPassId', 'simulationPassId')),
req,
res,
);
if (validatedDTO) {
try {
const {
from,
to,
comment,
flagTypeId,
runNumber,
dplDetectorId,
dataPassId,
simulationPassId,
} = validatedDTO.body;
const parameters = { from, to, comment };
const relations = {
user: { externalUserId: validatedDTO.session.externalId },
flagTypeId,
runNumber,
dplDetectorId,
dataPassId,
simulationPassId,
};
let createdFlag;
if (dataPassId) {
createdFlag = await qcFlagService.createForDataPass(parameters, relations);
} else {
createdFlag = await qcFlagService.createForSimulationPass(parameters, relations);
}
res.status(201).json({ data: createdFlag });
} catch (error) {
updateExpressResponseFromNativeError(res, error);
}
}
};

exports.QcFlagController = {
getQcFlagByIdHandler,
listQcFlagsHandler,
createQcFlagHandler,
};
4 changes: 4 additions & 0 deletions lib/server/routers/qcFlag.router.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,9 @@ exports.qcFlagsRouter = {
method: 'get',
controller: QcFlagController.listQcFlagsHandler,
},
{
method: 'post',
controller: QcFlagController.createQcFlagHandler,
},
],
};
210 changes: 209 additions & 1 deletion lib/server/services/qualityControlFlag/QcFlagService.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
* or submit itself to any jurisdiction.
*/

const { repositories: { QcFlagRepository } } = require('../../../database/index.js');
const { repositories: { QcFlagRepository, DplDetectorRepository, RunRepository } } = require('../../../database/index.js');
const { dataSource } = require('../../../database/DataSource.js');
const { qcFlagAdapter } = require('../../../database/adapters/index.js');
const { BadParameterError } = require('../../errors/BadParameterError.js');
const { NotFoundError } = require('../../errors/NotFoundError.js');
const { getUserOrFail } = require('../user/getUserOrFail.js');

const NON_QC_DETECTORS = new Set(['TST']);

/**
* Quality control flags service
Expand Down Expand Up @@ -53,6 +56,211 @@ class QcFlagService {
return qcFlag;
}

/**
* Validate QC flag timestamps
* If null timestamp was provided, given timestamp is replaced by run's startTime or endTime
* @param {{from: number, to: number}} timestamps QC flag timestamps
* @param {Run} targetRun run which for QC flag is to be set
* @throws {BadParameterError}
* @return {{fromTime: number, toTime: number}} prepared timestamps
*/
_prepareQcFlagPeriod({ from, to }, targetRun) {
const fromTime = from ?? targetRun.startTime;
const toTime = to ?? targetRun.endTime;

if (!fromTime || !toTime) {
if (!fromTime && !toTime) {
return { fromTime: null, toTime: null };
} else {
throw new BadParameterError('Only null QC flag timestamps are accepted as run.startTime or run.endTime is missing');
}
}

if (fromTime >= toTime) {
throw new BadParameterError('Parameter "to" timestamp must be greater than "from" timestamp');
}

if (fromTime < targetRun.startTime || targetRun.endTime < toTime) {
// eslint-disable-next-line max-len
throw new BadParameterError(`Given QC flag period (${fromTime}, ${toTime}) is out of run (${targetRun.startTime}, ${targetRun.endTime}) period`);
}
return { fromTime, toTime };
}

/**
* Validate QC flag DPL detector
* @param {number} dplDetectorId DPL detector
* @throws {BadParameterError}
* @return {void}
*/
async _validateQcFlagDplDetector(dplDetectorId) {
const dplDetector = await DplDetectorRepository.findOne({ where: { id: dplDetectorId } });
if (!dplDetector?.name || NON_QC_DETECTORS.has(dplDetector.name)) {
throw new BadParameterError(`Invalid DPL detector (${dplDetector.name})`);
}
return dplDetector;
}

/**
* Create new instance of quality control flags for data pass
* @param {Partial<QcFlag>} parameters flag instance parameters
* @param {object} [relations] QC Flag Type entity relations
* @param {Partial<UserIdentifier>} [relations.user] user identifiers
* @param {number} [parameters.flagTypeId] flag type id
* @param {number} [parameters.runNumber] associated run's number
* @param {number} [parameters.dataPassId] associated dataPass' id
* @param {number} [parameters.dplDetectorId] associated dplDetector's id
* @return {Promise<QcFlag>} promise
* @throws {BadParameterError, NotFoundError}
*/
async createForDataPass(parameters, relations = {}) {
const {
from = null,
to = null,
comment,
} = parameters;
const {
user: { userId, externalUserId } = {},
flagTypeId,
runNumber,
dataPassId,
dplDetectorId,
} = relations;

return dataSource.transaction(async () => {
// Check user
const user = await getUserOrFail({ userId, externalUserId });

// Check associations
const dplDetector = await this._validateQcFlagDplDetector(dplDetectorId);

const targetRun = await RunRepository.findOne({
subQuery: false,
attributes: ['timeTrgStart', 'timeTrgEnd'],
where: { runNumber },
include: [
{
association: 'dataPass',
where: { id: dataPassId },
through: { attributes: [] },
attributes: ['id'],
required: true,
},
{
association: 'detectors',
where: { name: dplDetector.name },
through: { attributes: [] },
attributes: [],
required: true,
},
],
});
if (!targetRun) {
// eslint-disable-next-line max-len
throw new BadParameterError(`There is not association between data pass with this id (${dataPassId}), run with this number (${runNumber}) and detector with this name (${dplDetector.name})`);
}

const { fromTime, toTime } = this._prepareQcFlagPeriod({ from, to }, targetRun);

// Insert
const newInstance = await QcFlagRepository.insert({
from: fromTime,
to: toTime,
comment,
createdById: user.id,
flagTypeId,
runNumber,
dplDetectorId,
});

const createdFlag = await QcFlagRepository.findOne(this.prepareQueryBuilder().where('id').is(newInstance.id));

await createdFlag.addDataPasses(targetRun.dataPass);

return qcFlagAdapter.toEntity(createdFlag);
});
}

/**
* Create new instance of quality control flags for simulation pass
* @param {Partial<QcFlag>} parameters flag instance parameters
* @param {object} [relations] QC Flag Type entity relations
* @param {Partial<UserIdentifier>} [relations.user] user identifiers
* @param {number} [parameters.flagTypeId] flag type id
* @param {number} [parameters.runNumber] associated run's number
* @param {number} [parameters.simulationPassId] associated simulationPass' id
* @param {number} [parameters.dplDetectorId] associated dplDetector's id
* @return {Promise<QcFlag>} promise
* @throws {BadParameterError, NotFoundError}
*/
async createForSimulationPass(parameters, relations = {}) {
const {
from = null,
to = null,
comment,
} = parameters;
const {
user: { userId, externalUserId } = {},
flagTypeId,
runNumber,
simulationPassId,
dplDetectorId,
} = relations;

return dataSource.transaction(async () => {
// Check user
const user = await getUserOrFail({ userId, externalUserId });

// Check associations
const dplDetector = await this._validateQcFlagDplDetector(dplDetectorId);

const targetRun = await RunRepository.findOne({
subQuery: false,
attributes: ['timeTrgStart', 'timeTrgEnd'],
where: { runNumber },
include: [
{
association: 'simulationPasses',
where: { id: simulationPassId },
through: { attributes: [] },
attributes: ['id'],
required: true,
},
{
association: 'detectors',
where: { name: dplDetector.name },
through: { attributes: [] },
attributes: [],
required: true,
},
],
});
if (!targetRun) {
// eslint-disable-next-line max-len
throw new BadParameterError(`There is not association between simulation pass with this id (${simulationPassId}), run with this number (${runNumber}) and detector with this name (${dplDetector.name})`);
}

const { fromTime, toTime } = this._prepareQcFlagPeriod({ from, to }, targetRun);

// Insert
const newInstance = await QcFlagRepository.insert({
from: fromTime,
to: toTime,
comment,
createdById: user.id,
flagTypeId,
runNumber,
dplDetectorId,
});

const createdFlag = await QcFlagRepository.findOne(this.prepareQueryBuilder().where('id').is(newInstance.id));

await createdFlag.addSimulationPasses(targetRun.simulationPasses);

return qcFlagAdapter.toEntity(createdFlag);
});
}

/**
* Get all quality control flags instances
* @param {object} [options.filter] filtering defintion
Expand Down
Loading

0 comments on commit 0c6da29

Please sign in to comment.