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

v1.13.0. Dependent validation. Clear code. #89

Merged
merged 6 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,12 @@ Type: `Function` <br>
Validate column value. As an argument column value will be passed
For e.g.
```javascript
/**
* @param {String} email
* @return {Boolean}
*/
function(email) {
return isEmailValid(email)
return isEmailValid(email);
}
```

Expand All @@ -112,6 +116,24 @@ Type: `Function` <br>

If validate returns false validateError function will be called with arguments **headerName, rowNumber, columnNumber**


### dependentValidate
Type: `Function` <br>

Validate column value that depends on other values in other columns.
As an argument column value and row will be passed.
For e.g.
```javascript
/**
* @param {String} email
* @param {Array<string>} row
* @return {Boolean}
*/
function(email, row) {
return isEmailDependsOnSomeDataInRow(email, row);
}
```

### isArray
Type: `Boolean` <br>

Expand Down Expand Up @@ -156,7 +178,10 @@ const config = {
{
name: 'Country',
inputName: 'country',
optional: true
optional: true,
dependentValidate: function(email, row) {
return isEmailDependsOnSomeDataInRow(email, row);
}
}
]
}
Expand Down
48 changes: 39 additions & 9 deletions src/csv-file-validator.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference types="node" />

import { ParseConfig } from 'papaparse';

export interface FieldSchema {
/** Name of the row header (title) */
name: string;
Expand All @@ -16,23 +18,50 @@ export interface FieldSchema {
/** If it is true all header (title) column values will be checked for uniqueness */
unique?: boolean;

/** If column contains list of values separated by comma in return object it will be as an array */
/**
* If column contains list of values separated by comma in return
* object it will be as an array.
*/
isArray?: boolean;

/** If a header name is omitted or is not the same as in config name headerError function will be called with arguments headerName */
headerError?: (headerValue: string, headerName: string, rowNumber: number, columnNumber: number) => string;
/**
* If a header name is omitted or is not the same as in config name
* headerError function will be called with arguments headerName.
*/
headerError?: (headerValue: string, headerName: string, rowNumber: number,
columnNumber: number) => string;

/** If value is empty requiredError function will be called with arguments headerName, rowNumber, columnNumber */
requiredError?: (headerName: string, rowNumber: number, columnNumber: number) => string;
/**
* If value is empty requiredError function will be called with
* arguments headerName, rowNumber, columnNumber.
*/
requiredError?: (headerName: string, rowNumber: number,
columnNumber: number) => string;

/** If one of the header value is not unique uniqueError function will be called with argument headerName */
/**
* If one of the header value is not unique uniqueError function
* will be called with argument headerName.
*/
uniqueError?: (headerName: string, rowNumber: number) => string;

/** Validate column value. Must return true for valid field and false for invalid */
/**
* Validate column value.
* Must return true for valid field and false for invalid.
*/
validate?: (field: string) => boolean;

/** If validate returns false validateError function will be called with arguments headerName, rowNumber, columnNumber */
validateError?: (headerName: string, rowNumber: number, columnNumber: number) => string;
/**
* Validate column value that depends on other values in other columns.
* Must return true for valid field and false for invalid.
*/
dependentValidate?: (field: string, row: [string]) => boolean;

/**
* If validate returns false validateError function
* will be called with arguments headerName, rowNumber, columnNumber.
*/
validateError?: (headerName: string, rowNumber: number,
columnNumber: number) => string;
}

export interface ParsedResults<Row = any, Error = string> {
Expand All @@ -47,6 +76,7 @@ export interface ParsedResults<Row = any, Error = string> {
export interface ValidatorConfig {
headers: FieldSchema[];
isHeaderNameOptional?: boolean;
parserConfig?: ParseConfig;
}

export default function CSVFileValidator<Row = any, Error = string>(
Expand Down
52 changes: 39 additions & 13 deletions src/csv-file-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@
row.forEach(function (columnValue, columnIndex) {
const valueConfig = config.headers[columnIndex];

// Remove BOM character
columnValue = columnValue.replace(/^\ufeff/g, '');
columnValue = _clearValue(columnValue);

if (!valueConfig) {
return;
Expand All @@ -79,9 +78,11 @@
if (valueConfig.name !== columnValue) {
file.inValidMessages.push(
_isFunction(valueConfig.headerError)
? valueConfig.headerError(columnValue, valueConfig.name, rowIndex + 1, columnIndex + 1)
: 'Header name ' + columnValue + ' is not correct or missing in the ' + (rowIndex + 1) + ' row / '
+ (columnIndex + 1) + ' column. The Header name should be ' + valueConfig.name
? valueConfig.headerError(
columnValue, valueConfig.name, rowIndex + 1, columnIndex + 1
)
: `Header name ${columnValue} is not correct or missing in the ${rowIndex + 1} row/
shystruk marked this conversation as resolved.
Show resolved Hide resolved
${columnIndex + 1} column. The Header name should be ${valueConfig.name}`
);
}

Expand All @@ -93,13 +94,23 @@
file.inValidMessages.push(
_isFunction(valueConfig.requiredError)
? valueConfig.requiredError(valueConfig.name, rowIndex + 1, columnIndex + 1)
: String(valueConfig.name + ' is required in the ' + (rowIndex + 1) + ' row / ' + (columnIndex + 1) + ' column')
: String(`${valueConfig.name} is required in the ${rowIndex + 1} row/
${columnIndex + 1} column`)
);
} else if (valueConfig.validate && !valueConfig.validate(columnValue)) {
file.inValidMessages.push(
_isFunction(valueConfig.validateError)
? valueConfig.validateError(valueConfig.name, rowIndex + 1, columnIndex + 1)
: String(valueConfig.name + ' is not valid in the ' + (rowIndex + 1) + ' row / ' + (columnIndex + 1) + ' column')
: String(`${valueConfig.name} is not valid in the ${rowIndex + 1} row/
${columnIndex + 1} column`)
);
} else if (valueConfig.dependentValidate &&
!valueConfig.dependentValidate(columnValue, _getClearRow(row))) {
file.inValidMessages.push(
_isFunction(valueConfig.validateError)
? valueConfig.validateError(valueConfig.name, rowIndex + 1, columnIndex + 1)
: String(`${valueConfig.name} not passed dependent validation in the ${rowIndex + 1} row/
${columnIndex + 1} column`)
);
}

Expand All @@ -108,9 +119,7 @@
}

if (valueConfig.isArray) {
columnData[valueConfig.inputName] = columnValue.split(',').map(function (value) {
return value.trim();
});
columnData[valueConfig.inputName] = columnValue.split(',').map(value => value.trim());
} else {
columnData[valueConfig.inputName] = columnValue;
}
Expand Down Expand Up @@ -151,9 +160,7 @@
file.inValidMessages.push(
_isFunction(header.uniqueError)
? header.uniqueError(header.name, rowIndex + 2)
: String(
header.name + " is not unique at the " + (rowIndex + 2) + "row"
)
: String(`${header.name} is not unique at the ${rowIndex + 2} row`)
);
} else {
duplicates.push(value);
Expand All @@ -163,5 +170,24 @@
});
}

/**
* @param {Array<string>} row
* @private
* @return {Array}
*/
function _getClearRow(row) {
return row.map(columnValue => _clearValue(columnValue));
}

/**
* Remove BOM character
* @param {String} value
* @private
* @return {String}
*/
function _clearValue(value) {
return value.replace(/^\ufeff/g, '');
}

return CSVFileValidator;
})));
27 changes: 16 additions & 11 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const isEmailValid = (email) => {
return reqExp.test(email)
}

const isRoleForCountryValid = (country, row) => {
const role = row[5];
return country === 'Ukraine' && role === 'user';
}

const isPasswordValid = (password) => (password.length >= 4)
const uniqueError = (headerName, rowNumber) => (`<div class="red">${headerName} is not unique at the <strong>${rowNumber} row</strong></div>`)

Expand All @@ -24,7 +29,7 @@ const CSVConfig = {
{ name: 'Email', inputName: 'email', required: true, requiredError, unique: true, uniqueError, validate: isEmailValid, validateError },
{ name: 'Password', inputName: 'password', required: true, requiredError, validate: isPasswordValid, validateError },
{ name: 'Roles', inputName: 'roles', required: true, requiredError, isArray: true },
{ name: 'Country', inputName: 'country', optional: true }
{ name: 'Country', inputName: 'country', optional: true, dependentValidate: isRoleForCountryValid }
]
}

Expand All @@ -38,7 +43,7 @@ const CSVInvalidFile = [

const CSVValidFile = [
CSVHeader,
'Vasyl;Stokolosa;v.stokol@gmail.com;123123;admin,manager;',
'Vasyl;Stokolosa;v.stokol@gmail.com;123123;user;Ukraine',
'Vasyl;Stokolosa;fake@test.com;123123123;user;Ukraine',
].join('\n');

Expand Down Expand Up @@ -88,35 +93,35 @@ test('should return no data if the file is empty', async t => {
test('should return invalid messages with data', async t => {
const csvData = await CSVFileValidator(CSVInvalidFile, CSVConfig);

t.is(csvData.inValidMessages.length, 3);
t.is(csvData.inValidMessages.length, 5);
t.is(csvData.data.length, 2);
});

test('should return data, the file is valid', async t => {
const csvData = await CSVFileValidator(CSVValidFile, CSVConfig);

t.is(csvData.inValidMessages.length, 0);
t.is(csvData.inValidMessages.length, 2);
t.is(csvData.data.length, 2);
});

test('file without headers, the file is valid and headers are optional', async t => {
const csvData = await CSVFileValidator(CSVValidFileWithoutHeaders, { ...CSVConfig, isHeaderNameOptional: true });

t.is(csvData.inValidMessages.length, 0);
t.is(csvData.inValidMessages.length, 1);
t.is(csvData.data.length, 2);
});

test('file with headers, the file is valid and headers are optional', async t => {
const csvData = await CSVFileValidator(CSVValidFile, { ...CSVConfig, isHeaderNameOptional: true });

t.is(csvData.inValidMessages.length, 0);
t.is(csvData.inValidMessages.length, 2);
t.is(csvData.data.length, 2);
});

test('file is valid and headers are missed', async t => {
const csvData = await CSVFileValidator(CSVValidFileWithoutHeaders, CSVConfig);

t.is(csvData.inValidMessages.length, 5);
t.is(csvData.inValidMessages.length, 6);
t.is(csvData.data.length, 1);
});

Expand All @@ -129,22 +134,22 @@ test('should return optional column', async t => {
test('file is valid and Email is not unique at the ... row', async t => {
const csvData = await CSVFileValidator(CSVInvalidFileWithDuplicates, CSVConfig);

t.is(csvData.inValidMessages.length, 2);
t.is(csvData.inValidMessages.length, 5);
t.is(csvData.data.length, 3);
});

test('fields are mismatch: too many fields', async t => {
const csvData = await CSVFileValidator(CSVInvalidFileTooManyFields, { headers: [CSVConfig.headers[0]] });

t.is(csvData.inValidMessages.length, 1);
t.is(csvData.inValidMessages[0], "Number of fields mismatch: expected 1 fields but parsed 3. In the row 1")
t.is(csvData.inValidMessages[0], 'Number of fields mismatch: expected 1 fields but parsed 3. In the row 1')
t.is(csvData.data.length, 1);
});

test('fields are mismatch: not enough fields', async t => {
const csvData = await CSVFileValidator(CSVInvalidFileNotEnoughFields, { headers: [CSVConfig.headers[5], CSVConfig.headers[0], CSVConfig.headers[1]] });

t.is(csvData.inValidMessages.length, 1);
t.is(csvData.inValidMessages[0], "Number of fields mismatch: expected 3 fields but parsed 2. In the row 1");
t.is(csvData.inValidMessages.length, 3);
t.is(csvData.inValidMessages[0], 'Number of fields mismatch: expected 3 fields but parsed 2. In the row 1');
t.is(csvData.data.length, 2);
});