-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathQcFlagService.js
More file actions
751 lines (671 loc) · 31.3 KB
/
Copy pathQcFlagService.js
File metadata and controls
751 lines (671 loc) · 31.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
/**
* @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: {
QcFlagRepository,
DataPassQcFlagRepository,
DataPassRepository,
SimulationPassQcFlagRepository,
RunRepository,
QcFlagVerificationRepository,
QcFlagEffectivePeriodRepository,
},
} = require('../../../database/index.js');
const { dataSource } = require('../../../database/DataSource.js');
const { qcFlagAdapter, dataPassQcFlagAdapter, simulationPassQcFlagAdapter } = require('../../../database/adapters/index.js');
const { BadParameterError } = require('../../errors/BadParameterError.js');
const { NotFoundError } = require('../../errors/NotFoundError.js');
const { getUserOrFail } = require('../user/getUserOrFail.js');
const { ConflictError } = require('../../errors/ConflictError.js');
const { getQcDetectorOrFail } = require('../detector/getQcDetectorOrFail.js');
const { Op } = require('sequelize');
const { BkpRoles } = require('../../../domain/enums/BkpRoles.js');
const { getOneDataPassOrFail } = require('../dataPasses/getOneDataPassOrFail.js');
const { getOneSimulationPassOrFail } = require('../simulationPasses/getOneSimulationPassOrFail.js');
const { AccessDeniedError } = require('../../errors/AccessDeniedError.js');
const { LogManager } = require('@aliceo2/web-ui');
const { DetectorType } = require('../../../domain/enums/DetectorTypes.js');
/**
* @typedef UserWithRoles
* @property {number} [userId]
* @property {number} [externalUserId]
* @property {string[]} roles
*/
/**
* Return expert's role for given detector
* @param {string} detectorName name of detector
* @return {string} role
*/
const getRoleForDetector = (detectorName) => `det-${detectorName.toLowerCase()}`;
/**
* Check whether given user's roles suffice to manage QC flags for given detector
* @param {string[]} userRoles roles of user
* @param {string} detectorName name of detector
* @return {void}
* @throws {AccessDeniedError} when roles doesn't suffice
*/
const validateUserDetectorAccess = (userRoles, detectorName) => {
if (!userRoles.includes(BkpRoles.ADMIN) && !userRoles.includes(getRoleForDetector(detectorName))) {
throw new AccessDeniedError(`You have no permission to manage flags for ${detectorName} detector`);
}
};
/**
* Quality control flags service
*/
class QcFlagService {
/**
* Constructor
*/
constructor() {
this._logger = LogManager.getLogger('QC_FLAG_SERVICE');
}
/**
* Find a Quality Control Flag by its id
* @param {number} id identifier of Quality Control Flag
* @return {Promies<QcFlag>} a Quality Control Flag
*/
async getById(id) {
if (!id) {
throw new BadParameterError('Can not find without Quality Control Flag id');
}
const queryBuilder = this.prepareQueryBuilder()
.where('id').is(id);
const qcFlag = await QcFlagRepository.findOne(queryBuilder);
return qcFlag ? qcFlagAdapter.toEntity(qcFlag) : null;
}
/**
* Find a Quality Control Flag by its id
* @param {number} id id of Quality Control Flag
* @throws {NotFoundError} in case there is no Quality Control Flag with given id
* @return {Promise<QcFlag>} a Quality Control Flag
*/
async getOneOrFail(id) {
const qcFlag = await this.getById(id);
if (!qcFlag) {
throw new NotFoundError(`Quality Control Flag with this id (${id}) could not be found`);
}
return qcFlag;
}
/**
* Create new instance of quality control flags,
* asynchronous for data/simulation pass or synchronous
*
* @param {Partial<QcFlag>[]} qcFlags flags to create
* @param {object} scope scope of the QC flags to create
* @param {number} scope.runNumber associated run's number
* @param {DplDetectorIdentifier} scope.detectorIdentifier associated dplDetector's identifier
* @param {DataPassIdentifier} [scope.dataPassIdentifier] associated data pass id
* @param {SimulationPassIdentifier} [scope.simulationPassIdentifier] associated simulation pass id
* @param {object} relations relations of the QC flag
* @param {UserWithRoles} relations.user identifier with roles of the user creating the QC flag
* @return {Promise<QcFlag[]>} resolves with the created QC flags
* @throws {BadParameterError, NotFoundError}
*/
async create(qcFlags, scope, relations) {
const {
runNumber,
dataPassIdentifier,
simulationPassIdentifier,
detectorIdentifier,
} = scope;
const { user: { userId, externalUserId, roles: userRoles = [] } } = relations;
if (dataPassIdentifier && simulationPassIdentifier) {
throw new BadParameterError('Cannot create QC flag for data pass and simulation pass simultaneously');
}
return dataSource.transaction(async () => {
const user = await getUserOrFail({ userId, externalUserId });
const detector = await getQcDetectorOrFail(detectorIdentifier);
const dataPass = dataPassIdentifier ? await getOneDataPassOrFail(dataPassIdentifier) : null;
const simulationPass = simulationPassIdentifier ? await getOneSimulationPassOrFail(simulationPassIdentifier) : null;
validateUserDetectorAccess(userRoles, detector.name);
if (dataPass?.isFrozen) {
throw new Error('Cannot create QC flag when datapass is frozen');
}
const targetRun = await this._getQcTargetRun(
runNumber,
detector,
{ dataPassId: dataPass?.id, simulationPassId: simulationPass?.id },
);
const createdFlags = [];
for (const qcFlag of qcFlags) {
const { comment, flagTypeId, origin } = qcFlag;
// Each flag should be treated separately and the for loop should not fail on the first error
try {
const { from, to } = this._prepareQcFlagPeriod({ from: qcFlag.from, to: qcFlag.to }, targetRun);
// Insert
const newInstance = await QcFlagRepository.insert({
from,
to,
comment,
origin,
createdById: user.id,
flagTypeId,
runNumber,
detectorId: detector.id,
});
if (dataPass) {
await newInstance.addDataPass(dataPass);
} else if (simulationPass) {
await newInstance.addSimulationPass(simulationPass);
}
/** @var {SequelizeQcFlag} createdFlag */
const createdFlag = await QcFlagRepository.findOne({
where: { id: newInstance.id },
include: [
{ association: 'dataPasses' },
{ association: 'simulationPasses' },
{ association: 'createdBy' },
],
});
// Update effective periods
const effectivePeriodsToBeUpdated = await QcFlagEffectivePeriodRepository.findOverlappingPeriodsCreatedBeforeLimit(
{ from, to },
createdFlag.createdAt,
{ dataPassId: dataPass?.id, simulationPassId: simulationPass?.id, runNumber, detectorId: detector.id },
);
await this._removeEffectivePeriodsAndPeriodIntersection(createdFlag, effectivePeriodsToBeUpdated);
await QcFlagEffectivePeriodRepository.insert({
flagId: newInstance.id,
from: newInstance.from,
to: newInstance.to,
});
createdFlags.push(qcFlagAdapter.toEntity(createdFlag));
} catch (error) {
this._logger.warnMessage(`Failed to create QC flag with properties: ${JSON.stringify(qcFlag)}. Error: ${error}`);
}
}
return createdFlags;
});
}
/**
* Delete single instance of QC flag
* @param {number} id QC flag id
* @return {Promise<QcFlag>} promise
*/
async delete(id) {
return dataSource.transaction(async () => {
const qcFlag = await QcFlagRepository.findOne({
where: { id, deleted: false },
include: [
{ association: 'dataPasses' },
{ association: 'simulationPasses' },
{ association: 'verifications' },
{ association: 'createdBy' },
],
});
if (!qcFlag) {
throw new NotFoundError(`Not-deleted Quality Control Flag with this id (${id}) could not be found`);
}
if (qcFlag.verifications?.length > 0) {
throw new ConflictError('Cannot delete QC flag which is verified');
}
if (qcFlag.dataPasses[0]?.isFrozen) {
throw new Error('Cannot delete QC flag when datapass is frozen');
}
const dataPassId = qcFlag.dataPasses[0]?.id;
const simulationPassId = qcFlag.simulationPasses[0]?.id;
const {
after: flagsCreatedAfterRemovedFlag,
before: flagsCreatedBeforeRemovedFlag,
} = await QcFlagRepository.findFlagsCreatedAfterAndBeforeGivenOne(id);
await QcFlagEffectivePeriodRepository.removeAll({ where: { flagId: id } });
await QcFlagRepository.update(qcFlag, { deleted: true });
// Remove all effective periods of flags created before the deleted one
await QcFlagEffectivePeriodRepository.removeAll({
where: { flagId: { [Op.in]: flagsCreatedBeforeRemovedFlag.map(({ id }) => id) } },
});
while (flagsCreatedBeforeRemovedFlag.length) {
const flagWhichEffectivePeriodsAreToBeRecomputed = flagsCreatedBeforeRemovedFlag.shift();
await QcFlagEffectivePeriodRepository.insert({
flagId: flagWhichEffectivePeriodsAreToBeRecomputed.id,
from: flagWhichEffectivePeriodsAreToBeRecomputed.from,
to: flagWhichEffectivePeriodsAreToBeRecomputed.to,
});
for (const potentiallyOverlappingFlag of [...flagsCreatedBeforeRemovedFlag, ...flagsCreatedAfterRemovedFlag]) {
const { id, from, to, createdAt, runNumber, detectorId } = potentiallyOverlappingFlag;
const overlappingEffectivePeriods = (await QcFlagEffectivePeriodRepository.findOverlappingPeriodsCreatedBeforeLimit(
{ from, to },
createdAt,
{ dataPassId, simulationPassId, runNumber, detectorId },
)).filter(({ flagId }) => flagId !== id);
await this._removeEffectivePeriodsAndPeriodIntersection({ from, to }, overlappingEffectivePeriods);
}
}
{
const { id, from, to, origin, createdById, runNumber, dplDetectorId, flagTypeId, createdAt } = qcFlag;
const qcFlagPropertiesToLog = {
id,
from,
to,
origin,
createdById,
runNumber,
dplDetectorId,
flagTypeId,
dataPassId,
simulationPassId,
createdAt,
};
this._logger.infoMessage(`Deleted QC flag with properties: ${JSON.stringify(qcFlagPropertiesToLog)}`);
}
return qcFlagAdapter.toEntity(qcFlag);
});
}
/**
* Delete all the QC flags related to a given data pass
*
* @param {number} dataPassId the id of the data pass to which QC flag should relate
* @return {Promise<number>} resolves once all the QC has been deleted returning the amount of flags deleted
*/
async deleteAllForDataPass(dataPassId) {
return dataSource.transaction(async () => {
const dataPass = await DataPassRepository.findOne({ where: { id: dataPassId } });
if (!dataPass) {
throw new NotFoundError(`No datapass found with id ${dataPassId}`);
}
// Sequelize destroy requires a where and can't work only using association
const effectivePeriodIds = (await QcFlagEffectivePeriodRepository.findAll({
attributes: ['id'],
include: {
association: 'flag',
attributes: [],
required: true,
include: {
association: 'dataPasses',
attributes: [],
required: true,
where: {
id: dataPassId,
},
},
},
raw: true,
})).map(({ id }) => id);
await QcFlagEffectivePeriodRepository.removeAll({ where: { id: { [Op.in]: effectivePeriodIds } } });
// Sequelize update requires a where and can't work only using association
const qcFlagIds = (await QcFlagRepository.findAll({
attributes: ['id'],
include: {
association: 'dataPasses',
attributes: [],
required: true,
where: {
id: dataPassId,
},
},
raw: true,
})).map(({ id }) => id);
await QcFlagRepository.updateAll(
{ deleted: true },
{ where: { id: qcFlagIds } },
);
return qcFlagIds.length;
});
}
/**
* Create verification of QC flag
* @param {Partial<QcFlagVerification>} qcFlagVerification flag verification
* @param {object} relations QC Flag entity relations
* @param {UserWithRoles} relations.user user identifier with roles
* @return {Promise<QcFlag>} promise
* @throws {NotFoundError|AccessDeniedError}
*/
async verifyFlag({ flagId, comment }, relations) {
return dataSource.transaction(async () => {
const qcFlag = await this.getOneOrFail(flagId);
if (qcFlag.deleted) {
throw new Error(`QC flag ${flagId} is already discarded and cannot be verified`);
}
const { user: { userId, externalUserId, roles: userRoles = [] } } = relations;
const user = await getUserOrFail({ userId, externalUserId });
const detector = await getQcDetectorOrFail({ detectorId: qcFlag.dplDetectorId });
validateUserDetectorAccess(userRoles, detector.name);
await QcFlagVerificationRepository.insert({
flagId,
comment,
createdById: user.id,
});
return await this.getOneOrFail(flagId);
});
}
/**
* Return a paginated list of QC flags related to a given data pass, run and dpl detector
*
* @param {object} criteria the QC flag criteria
* @param {number} criteria.dataPassId the id of the data pass to which QC flag should relate
* @param {number} criteria.runNumber the run number of the run to which QC flag should relate
* @param {number} criteria.detectorId the id of the DPL detector to which QC flag should release
* @param {object} [pagination] the pagination to apply
* @param {number} [pagination.offset] amount of items to skip
* @param {number} [pagination.limit] amount of items to fetch
* @return {Promise<{count, rows: DataPassQcFlag[]}>} paginated list of data pass QC flags
*/
async getAllPerDataPassAndRunAndDetector({ dataPassId, runNumber, detectorId }, pagination) {
const { limit, offset } = pagination || {};
const queryBuilder = dataSource.createQueryBuilder()
.where('dataPassId').is(dataPassId)
.include({
association: 'qcFlag',
include: [
{ association: 'flagType' },
{ association: 'createdBy' },
{ association: 'verifications', include: [{ association: 'createdBy' }] },
{ association: 'effectivePeriods' },
],
where: {
runNumber,
detectorId,
},
required: true,
})
.set('subQuery', false)
.orderBy('id', 'DESC', 'qcFlag');
if (limit) {
queryBuilder.limit(limit);
}
if (offset) {
queryBuilder.offset(offset);
}
// The findAndCountAll function is not working properly with required include and distinct (count only on data pass id)
const [rows, count] = await Promise.all([
DataPassQcFlagRepository.findAll(queryBuilder),
DataPassQcFlagRepository.count(queryBuilder),
]);
return {
count,
rows: rows.map(dataPassQcFlagAdapter.toEntity),
};
}
/**
* Return a paginated list of QC flags related to a given simulation pass, run and dpl detector
*
* @param {object} criteria the QC flag criteria
* @param {number} criteria.simulationPassId the id of the simulation pass to which QC flag should relate
* @param {number} criteria.runNumber the run number of the run to which QC flag should relate
* @param {number} criteria.detectorId the id of the DPL detector to which QC flag should release
* @param {object} [pagination] the pagination to apply
* @param {number} [pagination.offset] amount of items to skip
* @param {number} [pagination.limit] amount of items to fetch
* @return {Promise<{count, rows: SimulationPassQcFlag[]}>} paginated list of simulation pass QC flags
*/
async getAllPerSimulationPassAndRunAndDetector({ simulationPassId, runNumber, detectorId }, pagination) {
const { limit, offset } = pagination || {};
const queryBuilder = dataSource.createQueryBuilder()
.where('simulationPassId').is(simulationPassId)
.include({
association: 'qcFlag',
include: [
{ association: 'flagType' },
{ association: 'createdBy' },
{ association: 'verifications', include: [{ association: 'createdBy' }] },
{ association: 'effectivePeriods' },
],
where: {
runNumber,
detectorId,
},
required: true,
})
.set('subQuery', false)
.orderBy('id', 'DESC', 'qcFlag');
if (limit) {
queryBuilder.limit(limit);
}
if (offset) {
queryBuilder.offset(offset);
}
// The findAndCountAll function is not working properly with required include and distinct (count only on simulation pass id)
const [rows, count] = await Promise.all([
SimulationPassQcFlagRepository.findAll(queryBuilder),
SimulationPassQcFlagRepository.count(queryBuilder),
]);
return {
count: count,
rows: rows.map(simulationPassQcFlagAdapter.toEntity),
};
}
/**
* Return a paginated list of synchronous QC flags related to a run and detector
*
* @param {object} criteria the QC flag criteria
* @param {number} criteria.runNumber the run number of the run to which QC flag should relate
* @param {number} criteria.detectorId the id of the detector to which QC flag should relate
* @param {object} [pagination] the pagination to apply
* @param {number} [pagination.offset] amount of items to skip
* @param {number} [pagination.limit] amount of items to fetch
* @return {Promise<{count, rows: SynchronousQcFlag[]}>} paginated list of synchronous QC flags
*/
async getAllSynchronousPerRunAndDetector({ runNumber, detectorId }, pagination) {
const { limit, offset } = pagination || {};
const queryParameters = {
subQuery: false,
where: {
runNumber,
detectorId,
'$dataPasses.id$': null,
'$simulationPasses.id$': null,
},
include: [
{ association: 'flagType' },
{ association: 'createdBy' },
{ association: 'verifications', include: [{ association: 'createdBy' }] },
{ association: 'dataPasses', required: false },
{ association: 'simulationPasses', required: false },
],
order: [['createdAt', 'DESC'], ['id', 'DESC']],
limit,
offset,
};
// The findAndCountAll function is not working properly with required include and distinct (count only on simulation pass id)
const [rows, count] = await Promise.all([
QcFlagRepository.findAll(queryParameters),
QcFlagRepository.count(queryParameters),
]);
return {
count,
rows: rows.map(qcFlagAdapter.toEntity),
};
}
/**
* Prepare query builder with common includes for fetching data
* @return {QueryBuilder} common fetch-data query builder
*/
prepareQueryBuilder() {
return dataSource.createQueryBuilder()
.include({ association: 'flagType' })
.include({ association: 'createdBy' })
.include({ association: 'verifications', include: [{ association: 'createdBy' }] })
.orderBy('createdAt', 'DESC', 'verifications');
}
/**
* Validate QC flag timestamps
* If null timestamp was provided, given timestamp is replaced by run's startTime or endTime
* @param {Partial<Period>} timestamps QC flag timestamps
* @param {Run} targetRun run which for QC flag is to be set
* @return {{from: (number|null), to: (number|null)}} prepared timestamps
* @throws {BadParameterError}
*/
_prepareQcFlagPeriod(timestamps, targetRun) {
const qcTimeStart = targetRun.qcTimeStart?.getTime();
const qcTimeEnd = targetRun.qcTimeEnd?.getTime();
const { from, to } = timestamps;
if (from && to && from >= to) {
throw new BadParameterError('Parameter "to" timestamp must be greater than "from" timestamp');
}
const isFromOutOfRange = from && (qcTimeStart && from < qcTimeStart || qcTimeEnd && qcTimeEnd <= from);
const isToOutOfRange = to && (qcTimeStart && to <= qcTimeStart || qcTimeEnd && qcTimeEnd < to);
if (isFromOutOfRange || isToOutOfRange) {
throw new BadParameterError(`Given QC flag period (${from}, ${to}) is out of run (${qcTimeStart}, ${qcTimeEnd}) period`);
}
return { from, to };
}
/**
* Remove a time segment from a list of QC flags effective periods
*
* @param {Partial<Period>} eraseWindow time segment that should be removed from given periods
* @param {SequelizeQcFlagEffectivePeriod[]} periods effective periods from which time window should be erased
* @return {Promise<void>} resolve once all periods are updated
*/
async _removeEffectivePeriodsAndPeriodIntersection(eraseWindow, periods) {
/*
* Example of what is named `before` and `after` an erase window:
*
* ------------------------------[ erase window ]----------------------------> time
* [ period BEFORE erase window ] [ period AFTER erase window ]
*
* When erasing time segment from the following QC flag (using the erase window from above):
* ---------------------[ QC flag period ]--------------------
*
* Resultant effective sub-periods of the QC flag will be:
* ---------------------[ ]-------------------------------------------- => sub-period BEFORE erase window
* ----------------------------------------------[ ]-------------------- => sub-period AFTER erase window
*
* Analogously, for the the following flag would have no sub-period after:
* ----------------[ QC flag period ]----------------------------------------
* ----------------[ ]-------------------------------------------- => sub-period BEFORE erase window
*
* And the following flag would have no sub-period before
* ---------------------------------------[ QC flag period ]-----------------
* ----------------------------------------------[ ]----------------- => sub-period AFTER erase window
*/
/**
* Return the sub-period left before the erase window
*
* @param {Partial<Period>} period the period from which a time segment is to be removed
* @param {Partial<Period>} eraseWindow the erase window
* @return {Partial<Period>|null} the resulting sub-period, if it exists
*/
const getPeriodBeforeEraseWindow = (period, eraseWindow) => {
if (
eraseWindow.from === null
|| period.from !== null && period.from.getTime() >= eraseWindow.from // Period started after erase window from
) {
return null;
}
return {
from: period.from?.getTime(),
// If period.to is null, simply goes to eraseWindow.from
to: Math.min(period.to?.getTime() ?? eraseWindow.from, eraseWindow.from),
};
};
/**
* Return the sub-period left after the erase window
*
* @param {Partial<Period>} period the period from which a time segment is to be removed
* @param {Partial<Period>} eraseWindow the erase window
* @return {Partial<Period>|null} the resulting sub-period, if it exists
*/
const getPeriodAfterEraseWindow = (period, eraseWindow) => {
if (
eraseWindow.to === null
|| period.to !== null && period.to.getTime() <= eraseWindow.to
) {
return null;
}
return {
from: Math.max(period.from?.getTime() ?? eraseWindow.to, eraseWindow.to), // If period.from is null, start from eraseWindow.to
to: period.to?.getTime(),
};
};
for (const period of periods) {
// Consider what effective period is left before the erase window
const periodBeforeEraseWindow = getPeriodBeforeEraseWindow(period, eraseWindow);
// Consider what effective period is left after the erase window
const periodAfterEraseWindow = getPeriodAfterEraseWindow(period, eraseWindow);
/*
* Four cases:
* - Empty sub-periods before and after => delete the whole effective period
* - Empty sub-period before and non-empty after => crop `from`
* - Empty sub-period after and non-empty before => crop `to`
* - two non-empty sub-periods => split the effective period in two,
* actually keeping the existing one as `before` and creating `after`
*/
if (periodBeforeEraseWindow === null && periodAfterEraseWindow === null) { // Old flag is fully covered by new one
await QcFlagEffectivePeriodRepository.removeOne({ where: { id: period.id } });
} else if (periodBeforeEraseWindow === null) {
await QcFlagEffectivePeriodRepository.update(period, { from: periodAfterEraseWindow.from });
} else if (periodAfterEraseWindow === null) {
await QcFlagEffectivePeriodRepository.update(period, { to: periodBeforeEraseWindow.to });
} else {
await QcFlagEffectivePeriodRepository.update(period, { to: periodBeforeEraseWindow.to }); // Reuse the existing one
await QcFlagEffectivePeriodRepository.insert({
flagId: period.flagId,
from: periodAfterEraseWindow.from,
to: periodAfterEraseWindow.to,
});
}
}
}
/**
* Find a run with a given run number and including a given detector.
* Throw if none exists
* There is exception for `GLO` detector, no run is linked with it.
*
* @param {number} runNumber the run number of the run to fetch
* @param {Detector} detector detector that the run must include if it is PHYSICAL
* @param {number} [monalisaProduction] MonALISA production, if not specified run will be fetched for synchronous QC flags,
* for asynchronous otherwise
* @param {number} [monalisaProduction.dataPassId] the id of the data pass to which run must be linked to
* @param {number} [monalisaProduction.simulationPassId] the id of the simulation pass to which the run must be linked to
* @return {Promise<Run>} the found run
* @private
*/
async _getQcTargetRun(runNumber, detector, { dataPassId, simulationPassId }) {
const runInclude = [
{
association: 'detectors',
where: { id: detector.id },
through: { attributes: [] },
attributes: [],
required: detector.type === DetectorType.PHYSICAL,
},
];
if (dataPassId) {
runInclude.push({
association: 'dataPass',
where: { id: dataPassId },
through: { attributes: [] },
attributes: ['id'],
required: true,
});
}
if (simulationPassId) {
runInclude.push({
association: 'simulationPasses',
where: { id: simulationPassId },
through: { attributes: [] },
attributes: ['id'],
required: true,
});
}
const run = await RunRepository.findOne({
subQuery: false,
attributes: ['qcTimeStart', 'qcTimeEnd'],
where: { runNumber },
include: runInclude,
});
if (!run) {
const criteria = [`run with this number (${runNumber})`, `detector with this name (${detector.name})`];
if (dataPassId) {
criteria.push(`data pass with this id (${dataPassId})`);
}
if (simulationPassId) {
criteria.push(`simulation pass with this id (${simulationPassId})`);
}
throw new BadParameterError(`There is not association between ${criteria.join(', ')}`);
}
return run;
}
}
module.exports.QcFlagService = QcFlagService;
module.exports.qcFlagService = new QcFlagService();