Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[O2B-1120] Add QC flags creation API #1499

Merged
merged 47 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2a68a96
refactor
xsalonx Apr 9, 2024
9b9438b
typos
xsalonx Apr 9, 2024
ab3a3c6
add testS
xsalonx Apr 9, 2024
ac5ed21
fix
xsalonx Apr 9, 2024
b2e184a
fix
xsalonx Apr 9, 2024
f00bf33
test
xsalonx Apr 9, 2024
77b33f1
refactor'
xsalonx Apr 9, 2024
d2dfc44
fix
xsalonx Apr 9, 2024
c6c840b
refactor
xsalonx Apr 9, 2024
2f1ac2b
refactor
xsalonx Apr 9, 2024
9c31108
add controller
xsalonx Apr 9, 2024
928c23c
test
xsalonx Apr 9, 2024
d0c1d92
test
xsalonx Apr 9, 2024
4fb4c0a
more rigid
xsalonx Apr 9, 2024
b48780c
mr
xsalonx Apr 9, 2024
f4eda9f
cleanup
xsalonx Apr 9, 2024
798d01e
ceanup
xsalonx Apr 9, 2024
175ca10
Merge branch 'main' into xsalonx/QCF/O2B-1120/creation-API
xsalonx Apr 9, 2024
db600b5
refactor
xsalonx Apr 9, 2024
b831876
refactor
xsalonx Apr 9, 2024
de58d97
cleanup
xsalonx Apr 9, 2024
64c80d8
Merge branch 'main' into xsalonx/QCF/O2B-1120/creation-API
xsalonx Apr 9, 2024
7e92023
ref
xsalonx Apr 9, 2024
43bc001
ch err
xsalonx Apr 9, 2024
8d8634c
test
xsalonx Apr 9, 2024
51fd126
use default timestamps
xsalonx Apr 9, 2024
c0d74e4
refactor
xsalonx Apr 10, 2024
fc8b944
refactor
xsalonx Apr 10, 2024
a391574
fix
xsalonx Apr 10, 2024
8454884
docs
xsalonx Apr 10, 2024
0a1740e
handle missing timestamps
xsalonx Apr 10, 2024
d02762f
typo
xsalonx Apr 10, 2024
c62dd0a
dates
xsalonx Apr 10, 2024
8236f4b
test
xsalonx Apr 10, 2024
3b38f1e
test
xsalonx Apr 10, 2024
1977563
test
xsalonx Apr 10, 2024
815cd9f
Merge branch 'main' into xsalonx/QCF/O2B-1120/creation-API
xsalonx Apr 10, 2024
7ac0685
Merge branch 'main' into xsalonx/QCF/O2B-1120/creation-API
xsalonx Apr 10, 2024
3f7d2c5
cleanup
xsalonx Apr 10, 2024
f667bff
rename
xsalonx Apr 10, 2024
cfda539
date
xsalonx Apr 10, 2024
be88776
syntax
xsalonx Apr 10, 2024
7a9e7b3
rename
xsalonx Apr 10, 2024
e2bd491
correct
xsalonx Apr 10, 2024
5e9885e
correct
xsalonx Apr 10, 2024
789e6e3
typo
xsalonx Apr 10, 2024
1d0795e
typo
xsalonx Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading