From 87c2522f146d362f6be9e0b61a938ecd0f0f0698 Mon Sep 17 00:00:00 2001 From: Andrew Barker Date: Thu, 14 Oct 2021 11:28:05 +0100 Subject: [PATCH 1/4] Add ParserConfig type in .d.ts --- src/csv-file-validator.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/csv-file-validator.d.ts b/src/csv-file-validator.d.ts index ce291b9..d75424b 100644 --- a/src/csv-file-validator.d.ts +++ b/src/csv-file-validator.d.ts @@ -1,5 +1,7 @@ /// +import { ParseConfig } from "papaparse"; + export interface FieldSchema { /** Name of the row header (title) */ name: string; @@ -47,6 +49,7 @@ export interface ParsedResults { export interface ValidatorConfig { headers: FieldSchema[]; isHeaderNameOptional?: boolean; + parserConfig?: ParseConfig; } export default function CSVFileValidator( From 5511dae2934a8aaa05ef8c84380f01b10396eda4 Mon Sep 17 00:00:00 2001 From: Sheldon Frith Date: Thu, 28 Oct 2021 17:58:06 -0500 Subject: [PATCH 2/4] Added a new type of validator function that allows users to validate columns dependent on other columns --- src/csv-file-validator.d.ts | 3 +++ src/csv-file-validator.js | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/csv-file-validator.d.ts b/src/csv-file-validator.d.ts index ce291b9..635a84c 100644 --- a/src/csv-file-validator.d.ts +++ b/src/csv-file-validator.d.ts @@ -31,6 +31,9 @@ export interface FieldSchema { /** Validate column value. Must return true for valid field and false for invalid */ validate?: (field: string) => boolean; + /** 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: {[column:string]: string}) => boolean; + /** If validate returns false validateError function will be called with arguments headerName, rowNumber, columnNumber */ validateError?: (headerName: string, rowNumber: number, columnNumber: number) => string; } diff --git a/src/csv-file-validator.js b/src/csv-file-validator.js index 8a39193..34b29d7 100644 --- a/src/csv-file-validator.js +++ b/src/csv-file-validator.js @@ -50,7 +50,7 @@ csvData.forEach(function (row, rowIndex) { const columnData = {}; - + const rowFormattedForUser = formatCsvRowForPassingToUser(row) // fields are mismatch if (rowIndex !== 0 && row.length !== config.headers.length) { file.inValidMessages.push( @@ -101,6 +101,12 @@ ? valueConfig.validateError(valueConfig.name, rowIndex + 1, columnIndex + 1) : String(valueConfig.name + ' is not valid in the ' + (rowIndex + 1) + ' row / ' + (columnIndex + 1) + ' column') ); + } else if (valueConfig.dependentValidate && !valueConfig.dependentValidate(columnValue, rowFormattedForUser)){ + 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') + ) } if (valueConfig.optional) { @@ -126,6 +132,15 @@ return file; } + /** + * + * @param {Array} row + * @returns + */ + function formatCsvRowForPassingToUser(row){ + return row.map((columnValue) => columnValue.replace(/^\ufeff/g, '')) + } + /** * @param {Object} file * @param {Object} config From 6ea234277df4f4a7f8f0dfda0710c13ec566e7cc Mon Sep 17 00:00:00 2001 From: shystruk Date: Mon, 29 Nov 2021 15:41:38 -0800 Subject: [PATCH 3/4] dependent validation, clear code --- README.md | 29 +++++++++++++++-- src/csv-file-validator.d.ts | 54 ++++++++++++++++++++++--------- src/csv-file-validator.js | 63 ++++++++++++++++++++++--------------- test.js | 27 +++++++++------- 4 files changed, 119 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 9b8e29b..a211259 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,12 @@ Type: `Function`
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); } ``` @@ -112,6 +116,24 @@ Type: `Function`
If validate returns false validateError function will be called with arguments **headerName, rowNumber, columnNumber** + +### dependentValidate +Type: `Function`
+ +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} row + * @return {Boolean} + */ +function(email, row) { + return isEmailDependsOnSomeDataInRow(email, row); +} +``` + ### isArray Type: `Boolean`
@@ -156,7 +178,10 @@ const config = { { name: 'Country', inputName: 'country', - optional: true + optional: true, + dependentValidate: function(email, row) { + return isEmailDependsOnSomeDataInRow(email, row); + } } ] } diff --git a/src/csv-file-validator.d.ts b/src/csv-file-validator.d.ts index 2ecc28e..6b71c92 100644 --- a/src/csv-file-validator.d.ts +++ b/src/csv-file-validator.d.ts @@ -1,6 +1,6 @@ /// -import { ParseConfig } from "papaparse"; +import { ParseConfig } from 'papaparse'; export interface FieldSchema { /** Name of the row header (title) */ @@ -18,26 +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 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 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 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; - /** 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: {[column:string]: 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 { diff --git a/src/csv-file-validator.js b/src/csv-file-validator.js index 34b29d7..3099ffa 100644 --- a/src/csv-file-validator.js +++ b/src/csv-file-validator.js @@ -50,7 +50,7 @@ csvData.forEach(function (row, rowIndex) { const columnData = {}; - const rowFormattedForUser = formatCsvRowForPassingToUser(row) + // fields are mismatch if (rowIndex !== 0 && row.length !== config.headers.length) { file.inValidMessages.push( @@ -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; @@ -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/ + ${columnIndex + 1} column. The Header name should be ${valueConfig.name}` ); } @@ -93,20 +94,24 @@ 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, rowFormattedForUser)){ + } 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 + ' is not valid in the ' + (rowIndex + 1) + ' row / ' + (columnIndex + 1) + ' column') - ) + : String(`${valueConfig.name} not passed dependent validation in the ${rowIndex + 1} row/ + ${columnIndex + 1} column`) + ); } if (valueConfig.optional) { @@ -114,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; } @@ -132,15 +135,6 @@ return file; } - /** - * - * @param {Array} row - * @returns - */ - function formatCsvRowForPassingToUser(row){ - return row.map((columnValue) => columnValue.replace(/^\ufeff/g, '')) - } - /** * @param {Object} file * @param {Object} config @@ -166,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); @@ -178,5 +170,24 @@ }); } + /** + * @param {Array} 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; }))); diff --git a/test.js b/test.js index 54abc65..72ac00e 100644 --- a/test.js +++ b/test.js @@ -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) => (`
${headerName} is not unique at the ${rowNumber} row
`) @@ -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 } ] } @@ -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'); @@ -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); }); @@ -129,7 +134,7 @@ 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); }); @@ -137,14 +142,14 @@ 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); }); From b0e494a047cd188591d93a0192e09407d8872c3e Mon Sep 17 00:00:00 2001 From: shystruk Date: Mon, 29 Nov 2021 15:42:14 -0800 Subject: [PATCH 4/4] 1.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8478145..6c4d4bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "csv-file-validator", - "version": "1.12.0", + "version": "1.13.0", "description": "Validation of CSV file against user defined schema (returns back object with data and invalid messages)", "main": "./src/csv-file-validator.js", "types": "./src/csv-file-validator.d.ts",