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-719] Add range based filtering for run number #1810

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 32 additions & 1 deletion lib/domain/dtos/filters/RunFilterDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,39 @@
description: Joi.string(),
});

/**
* Validates run numbers ranges to not exceed 100 runs
*
* @param {*} value The value to validate
* @param {*} helpers The helpers object
* @returns {Object} The value if validation passes
*/
const validateRange = (value, helpers) => {
const MAX_RANGE_SIZE = 100;

const runNumbers = value.split(',').map((runNumber) => runNumber.trim());

for (const runNumber of runNumbers) {
if (runNumber.includes('-')) {
const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10));
if (Number.isNaN(start) || Number.isNaN(end) || start > end) {
return helpers.error('any.invalid', { message: `Invalid range: ${runNumber}` });

Check warning on line 48 in lib/domain/dtos/filters/RunFilterDto.js

View check run for this annotation

Codecov / codecov/patch

lib/domain/dtos/filters/RunFilterDto.js#L48

Added line #L48 was not covered by tests
}
const rangeSize = end - start + 1;

if (rangeSize > MAX_RANGE_SIZE) {
return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumber}` });
}
}
}

return value;
};

exports.RunFilterDto = Joi.object({
runNumbers: Joi.string().trim(),
runNumbers: Joi.string().trim().custom(validateRange).messages({
'any.invalid': '{{#message}}',
}),
calibrationStatuses: Joi.array().items(...RUN_CALIBRATION_STATUS),
definitions: CustomJoi.stringArray().items(Joi.string().uppercase().trim().valid(...RUN_DEFINITIONS)),
eorReason: EorReasonFilterDto,
Expand Down
35 changes: 26 additions & 9 deletions lib/usecases/run/GetAllRunsUseCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,34 @@ class GetAllRunsUseCase {
} = filter;

if (runNumbers) {
const runNumberList = runNumbers.split(SEARCH_ITEMS_SEPARATOR)
.map((runNumber) => parseInt(runNumber, 10))
.filter((runNumber) => !Number.isNaN(runNumber));
const runNumberCriteria = runNumbers.split(SEARCH_ITEMS_SEPARATOR)
.map((runNumbers) => runNumbers.trim())
.filter(Boolean);

if (runNumberList.length) {
if (runNumberList.length > 1) {
// Multiple run numbers.
filteringQueryBuilder.where('runNumber').oneOf(...runNumberList);
const runNumberSet = new Set();

runNumberCriteria.forEach((runNumber) => {
if (runNumber.includes('-')) {
const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10));
if (!Number.isNaN(start) && !Number.isNaN(end)) {
for (let i = start; i <= end; i++) {
runNumberSet.add(i);
}
}
} else {
const parsedRunNumber = parseInt(runNumber, 10);
if (!Number.isNaN(parsedRunNumber)) {
runNumberSet.add(parsedRunNumber);
}
}
});

const finalRunNumberList = Array.from(runNumberSet);
if (finalRunNumberList.length) {
if (finalRunNumberList.length > 1) {
filteringQueryBuilder.where('runNumber').oneOf(...finalRunNumberList);
} else {
// One run number.
const [runNumber] = runNumberList;
const [runNumber] = finalRunNumberList;
filteringQueryBuilder.where('runNumber').substring(`${runNumber}`);
}
}
Expand Down
19 changes: 19 additions & 0 deletions test/api/runs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,25 @@ module.exports = () => {
expect(runs).to.lengthOf(2);
});

it('should successfully filter on run number range', async () => {
const response = await request(server).get('/api/runs?filter[runNumbers]=1-5,8,12,20-30');

expect(response.status).to.equal(200);
const { data: runs } = response.body;
expect(runs).to.lengthOf(18);
});

it('should return 400 if range exceeds maximum of 100', async () => {
const runNumberRange = '1-108';
const MAX_RANGE_SIZE = 100;
const response = await request(server).get(`/api/runs?filter[runNumbers]=${runNumberRange}`);

expect(response.status).to.equal(400);
const { errors: [error] } = response.body;
expect(error.title).to.equal('Invalid Attribute');
expect(error.detail).to.equal(`Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumberRange}`);
});

it('should return 400 if the calibration status filter is invalid', async () => {
{
const response = await request(server).get('/api/runs?filter[calibrationStatuses]=invalid');
Expand Down
8 changes: 8 additions & 0 deletions test/lib/usecases/run/GetAllRunsUseCase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ module.exports = () => {
expect(runs[1].runNumber).to.equal(17);
});

it('should return an array, only containing runs with specified run number and ranges', async () => {
getAllRunsDto.query = { filter: { runNumbers: '1-5,8,12,20-30' } };
const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto);

expect(runs).to.be.an('array');
expect(runs).to.have.lengthOf(18);
});

it('should return runs sorted by runNumber', async () => {
{
const { runs } = await new GetAllRunsUseCase().execute({ query: { sort: { runNumber: 'ASC' } } });
Expand Down
Loading