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

BHBC-2183: Re-enable/fix required column value validator #931

Merged
merged 7 commits into from
Feb 1, 2023
Merged
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
22 changes: 22 additions & 0 deletions api/src/json-schema/validation-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export const submissionValidationSchema = {
title: 'Column Validation',
description: 'The validators that can be applied against a column within a file/sheet.',
anyOf: [
{
$ref: '#/$defs/column_required_validator'
},
{
$ref: '#/$defs/column_format_validator'
},
Expand Down Expand Up @@ -331,6 +334,25 @@ export const submissionValidationSchema = {
},
additionalProperties: false
},
column_required_validator: {
description: 'Validates that this column value is not empty',
type: 'object',
properties: {
column_required_validator: {
type: 'object',
properties: {
name: {
type: 'string'
},
description: {
type: 'string'
}
},
additionalProperties: false
}
},
additionalProperties: false
},
column_format_validator: {
description: 'Validates that this column value matches a regex',
type: 'object',
Expand Down
56 changes: 27 additions & 29 deletions api/src/utils/media/csv/validation/csv-row-validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import {
} from './csv-row-validator';

describe('getRequiredFieldsValidator', () => {
it('adds no errors when required fields are not provided', () => {
const requiredFieldsByHeader: string[] = [];
it('adds no errors when required fields are populated', () => {
const requiredColumnsConfig = {
columnName: 'Header1'
};

const validator = getRequiredFieldsValidator(requiredFieldsByHeader);
const validator = getRequiredFieldsValidator(requiredColumnsConfig);

const xlsxWorkSheet = xlsx.utils.aoa_to_sheet([
['Header1', 'Header2'],
Expand All @@ -32,8 +34,11 @@ describe('getRequiredFieldsValidator', () => {
});

it('adds no errors when header does not exist', () => {
const requiredFieldsByHeader: string[] = ['Header1', 'Header2']; // fields for these headers are required
const validator = getRequiredFieldsValidator(requiredFieldsByHeader);
const requiredColumnsConfig = {
columnName: 'Header1'
};

const validator = getRequiredFieldsValidator(requiredColumnsConfig);

const xlsxWorkSheet = xlsx.utils.aoa_to_sheet([[], [5]]);

Expand All @@ -44,37 +49,28 @@ describe('getRequiredFieldsValidator', () => {
expect(csvWorkSheet.csvValidation.rowErrors).to.eql([]);
});

it('adds errors for every field if required fields are provided and there are zero data rows in the worksheet', () => {
const requiredFieldsByHeader: string[] = ['Header1', 'Header2']; // fields for these headers are required
it('adds no errors if there are zero rows in the worksheet', () => {
const requiredColumnsConfig = {
columnName: 'Header1'
};

const validator = getRequiredFieldsValidator(requiredFieldsByHeader);
const validator = getRequiredFieldsValidator(requiredColumnsConfig);

const xlsxWorkSheet = xlsx.utils.aoa_to_sheet([['Header1', 'Header2']]); // no data rows

const csvWorkSheet = new CSVWorksheet('Sheet1', xlsxWorkSheet);

validator(csvWorkSheet);

expect(csvWorkSheet.csvValidation.rowErrors).to.eql([
{
col: 'Header1',
errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_REQUIRED_FIELD,
message: 'Missing required value for column',
row: 2
},
{
col: 'Header2',
errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_REQUIRED_FIELD,
message: 'Missing required value for column',
row: 2
}
]);
expect(csvWorkSheet.csvValidation.rowErrors).to.eql([]);
});

it('adds errors for required fields that are empty', () => {
const requiredFieldsByHeader: string[] = ['Header1', 'Header2']; // fields for these headers are required
const requiredColumnsConfig = {
columnName: 'Header1'
};

const validator = getRequiredFieldsValidator(requiredFieldsByHeader);
const validator = getRequiredFieldsValidator(requiredColumnsConfig);

const xlsxWorkSheet = xlsx.utils.aoa_to_sheet([
['Header1', 'Header2', 'Header3'],
Expand All @@ -89,20 +85,22 @@ describe('getRequiredFieldsValidator', () => {
{
col: 'Header1',
errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_REQUIRED_FIELD,
message: 'Missing required value for column',
message: 'Value is required and cannot be empty',
row: 2
}
]);
});

it('adds no errors if there are no invalid required fields', () => {
const requiredFieldsByHeader: string[] = ['Header1', 'Header2']; // fields for these headers are required
it('adds no errors if there are no empty required fields', () => {
const requiredColumnsConfig = {
columnName: 'Header1'
};

const validator = getRequiredFieldsValidator(requiredFieldsByHeader);
const validator = getRequiredFieldsValidator(requiredColumnsConfig);

const xlsxWorkSheet = xlsx.utils.aoa_to_sheet([
['Header1', 'Header2', 'Header3'],
['header2Data', 'Header2Data', ''] // valid fields
['header1Data', 'Header2Data', ''] // valid fields
]);

const csvWorkSheet = new CSVWorksheet('Sheet1', xlsxWorkSheet);
Expand Down
65 changes: 26 additions & 39 deletions api/src/utils/media/csv/validation/csv-row-validator.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,45 @@
import { SUBMISSION_MESSAGE_TYPE } from '../../../../constants/status';
import { CSVValidator } from '../csv-file';

export type RequiredFieldsValidatorConfig = {
columnName: string;
};

/**
* TODO needs updating to use new config style, etc.
* For a specified column, adds an error for each row whose column value is null, undefined or empty.
*
* @param {RequiredFieldsValidatorConfig} [config]
* @return {*} {CSVValidator}
*/
export const getRequiredFieldsValidator = (requiredFieldsByHeader?: string[]): CSVValidator => {
export const getRequiredFieldsValidator = (config?: RequiredFieldsValidatorConfig): CSVValidator => {
return (csvWorksheet) => {
if (!requiredFieldsByHeader?.length) {
if (!config) {
return csvWorksheet;
}

const rows = csvWorksheet.getRows();

// If there are no rows, then add errors for all cells in the first data row based on the array of required headers
if (!rows?.length) {
csvWorksheet.csvValidation.addRowErrors(
requiredFieldsByHeader.map((requiredFieldByHeader) => {
return {
errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_REQUIRED_FIELD,
message: `Missing required value for column`,
col: requiredFieldByHeader,
row: 2
};
})
);

return csvWorksheet;
}

const headersLowerCase = csvWorksheet.getHeadersLowerCase();

// If there are rows, then check each cell in each row against the list of required headers, adding errors as needed
rows.forEach((row, rowIndex) => {
for (const requiredFieldByHeader of requiredFieldsByHeader) {
const columnIndex = headersLowerCase.indexOf(requiredFieldByHeader.toLowerCase());
const columnIndex = headersLowerCase.indexOf(config.columnName.toLowerCase());

//if column does not exist, return csvWorksheet
if (columnIndex < 0) {
return csvWorksheet;
}
// if column does not exist, return
if (columnIndex < 0) {
return csvWorksheet;
}

const rowValueForColumn = row[columnIndex];
const rowValueForColumn = row[columnIndex];

// Add an error if the cell value is empty
if (rowValueForColumn === undefined || rowValueForColumn === null || rowValueForColumn === '') {
csvWorksheet.csvValidation.addRowErrors([
{
errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_REQUIRED_FIELD,
message: `Missing required value for column`,
col: requiredFieldByHeader,
row: rowIndex + 2
}
]);
}
if (rowValueForColumn == undefined || rowValueForColumn === null || rowValueForColumn === '') {
// cell is empty when it is required, add an error for this cell
csvWorksheet.csvValidation.addRowErrors([
{
errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_REQUIRED_FIELD,
message: `Value is required and cannot be empty`,
col: config.columnName,
row: rowIndex + 2
}
]);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const sampleValidationSchema = {
},
{
column_range_validator: {}
},
{
column_required_validator: {}
}
]
}
Expand Down Expand Up @@ -126,11 +129,12 @@ describe('ValidationSchemaParser', () => {

const validators = validationSchemaParser.getAllColumnValidations('testFile1');

expect(validators.length).to.equal(3);
expect(validators.length).to.equal(4);

expect(typeof validators[0]).to.equal('function');
expect(typeof validators[1]).to.equal('function');
expect(typeof validators[2]).to.equal('function');
expect(typeof validators[3]).to.equal('function');
});
});

Expand Down
2 changes: 1 addition & 1 deletion api/src/utils/media/validation/validation-schema-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const ValidationRulesRegistry = {
generator: getValidHeadersValidator
},
{
name: '',
name: 'column_required_validator',
generator: getRequiredFieldsValidator
},
{
Expand Down