File

src/app/modules/sourcing/components/bulk-upload/csv-helper-util.ts

Constructor

constructor(config: any, allowedDynamicColumns: any)
Parameters :
  • config : Object

Methods

Private handleError
handleError(object: any, key: any, message: any, data: any)
Parameters :
  • object : Object
  • key : String
  • message : String
  • data : Array
Returns: void
Private prepareDataAndValidateFile
prepareDataAndValidateFile()
Returns: void
Public validate
validate(csvFile: any)
Parameters :
  • csvFile : File
Returns: void

Properties

Private allowedDynamicColumns
allowedDynamicColumns: any
Private config
config: any
Private csvData
csvData: any
Private csvFile
csvFile: any
Private response
response: CSVFileValidatorResponse
import * as _ from 'lodash-es';
import Papa from 'papaparse';

export interface CSVFileValidatorResponse {
    inValidMessages: any[],
    data: any[]
}

export default class CSVFileValidator {
    private csvFile = null;
    private csvData = null;
    private config = null;
    private allowedDynamicColumns = null;
    private response: CSVFileValidatorResponse;

    /**
     * @param {Object} config
     */
    constructor(config, allowedDynamicColumns) {

        this.config = config;
        this.allowedDynamicColumns = allowedDynamicColumns;
    }

    /**
     * @param {Object} object
     * @param {String} key
     * @param {String} message
     * @param {Array} data
     * @private
     */
    private handleError(object, key, message, data) {
        if (_.isFunction(_.get(object, key))) {
            object[key](...data);
        } else {
            this.response.inValidMessages.push(message);
        }
        return this.response;
    }

    /**
     * @private
     */
    private prepareDataAndValidateFile() {
        const expectedColumns = this.config.headers.length;
        const foundColumns = this.csvData[1].length;

        // Check if extra columns are present other than specified
        if (foundColumns > expectedColumns) {
            const invalidColumns = _.map(_.range(expectedColumns, foundColumns), (number) => this.csvData[1][number] || `Column ${number}`)
            return this.handleError(this.config, 'extraHeaderError', `Invalid data found in columns: ${invalidColumns.join(',')}`, [invalidColumns, expectedColumns, foundColumns]);
        }

        // Two row for headers and descriptions and one empty blank row at last
        const actualRows = this.csvData.length - 3;

        // Empty rows or file validation
        if (actualRows === 0) {
            return this.handleError(this.config, 'noRowsError', `Empty rows found in the file`, []);
        }

        // Minimum rows validation
        const minRows = _.get(this.config, 'minRows', 0);
        if (minRows > 0 && (minRows > actualRows)) {
            return this.handleError(this.config, 'minRowsError', `Expected min ${minRows} rows but found ${actualRows} rows in the file`, [minRows, actualRows]);
        }

        // Maximum rows validation
        const maxRows = _.get(this.config, 'maxRows', 0);
        if (maxRows > 0 && (maxRows < actualRows)) {
            return this.handleError(this.config, 'maxRowsError', `Expected max ${maxRows} rows but found ${actualRows} rows in the file`, [maxRows, actualRows]);
        }

        // Required headers validation
        const headers = this.config.headers;
        const csvHeaders = this.csvData[1]; // get the header row
        const headerNames = headers.map(row => {
           row.name = _.get(row, 'name', '').trim();
           return row.name;
        });

        // Missing headers
        let difference = headerNames
            .filter(x => !csvHeaders.includes(x))
            .concat(csvHeaders.filter(x => !headerNames.includes(x)));

        if (difference.length > 0) {
            difference.map((column) => {
                const valueConfig = headers.find(row => row.name === column);
                if (valueConfig) {
                    return this.handleError(valueConfig, 'headerError', `${column} header is missing`, [column]);
                }
            });
        }

        const uniqueValues = {};

        // Iterate over each row in csv file
        this.csvData.forEach((row, rowIndex) => {
            // First row is description and second row is headers so skip it
            if (rowIndex === 0 || rowIndex === 1) return;

            // No more rows in the file
            if ((row.length < headers.length)) {
                return ;
            }

            const rowData = {};
            let hasError = false;

            // Iterate over each column (header) in a row
            headers.forEach((valueConfig, columnIndex) => {
                // If header is not present
                if (!valueConfig) {
                    return;
                }

                // Get the column (header) value
                let columnValue = (row[columnIndex] || '').trim();

                // Default validation
                if (valueConfig.isDefault && !columnValue) {
                    columnValue = valueConfig.default;
                }

                const maxLength = _.get(valueConfig, 'maxLength', -1);

                // Max length validation
                if (typeof(columnValue) === 'string' && maxLength > -1) {
                    if (columnValue.length > maxLength) {
                        this.handleError(valueConfig, 'maxLengthError', `${valueConfig.name} contains more than ${maxLength} characters at row: ${rowIndex + 1}`, [valueConfig.name, rowIndex + 1, columnIndex + 1, maxLength, columnValue.length]);
                        hasError = true;
                        return;
                    }
                }


                //  Required column value validation
                if (valueConfig.required && !columnValue.length) {
                    this.handleError(valueConfig, 'requiredError', `${valueConfig.name} is required in the (${rowIndex + 1}) row / (${columnIndex + 1}) column`, [valueConfig.name, rowIndex + 1, columnIndex + 1]);
                    hasError = true;
                    return;
                }

                // Custom column (header) validation
                if (valueConfig.validate && !valueConfig.validate(columnValue)) {
                    this.handleError(valueConfig, 'validateError', `${valueConfig.name} is not valid in the (${rowIndex + 1}) row / (${columnIndex + 1}) column`, [valueConfig.name, rowIndex + 1, columnIndex + 1]);
                    hasError = true;
                    return;
                }

                // Unique validation
                if (valueConfig.unique) {
                    const inputName = _.get(valueConfig, 'inputName');
                    uniqueValues[inputName] = _.get(uniqueValues, `${inputName}`, []);

                    // If value not present in array
                    if (!uniqueValues[inputName].includes(columnValue)) {
                        uniqueValues[inputName].push(columnValue);
                    } else {
                        this.handleError(valueConfig, 'uniqueError', `${valueConfig.name} has duplicate value in the (${rowIndex + 1}) row / (${columnIndex + 1}) column`, [valueConfig.name, rowIndex + 1, columnIndex + 1, columnValue]);
                        hasError = true;
                        return;
                    }
                }

                // Optional validation
                if (valueConfig.optional) {
                    rowData[valueConfig.inputName] = columnValue;
                }

                // Url validation
                if (valueConfig.isUrl) {
                    const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
                    '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
                    '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
                    '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
                    '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
                    '(\\#[-a-z\\d_]*)?$','i'); // fragment locator
                    const isUrl = !!urlPattern.test(columnValue);
                    if (!isUrl) {
                        this.handleError(valueConfig, 'urlError', `${valueConfig.name} has invalid url at (${rowIndex + 1}) row / (${columnIndex + 1}) column`, [valueConfig.name, rowIndex + 1, columnIndex + 1, columnValue]);
                        hasError = true;
                        return;
                    }
                }
                // Array validation
                if (valueConfig.isArray) {
                    rowData[valueConfig.inputName] = columnValue.split(',')
                        .map((value) => value.trim());
                } else {
                    rowData[valueConfig.inputName] = columnValue;
                }

                const inValues = _.get(valueConfig, 'in', []);

                // In values validation
                if (!_.isEmpty(inValues) && !valueConfig.isDefault) {
                    const lowerValues = inValues.map((v) => _.toLower(v));
                    if (!lowerValues.includes(_.toLower(columnValue))) {
                        this.handleError(valueConfig, 'inError', `${valueConfig.name} has invalid value at row: ${rowIndex + 1}`, [valueConfig.name, rowIndex + 1, columnIndex + 1, valueConfig.in, columnValue]);
                        hasError = true;
                        return;
                    }
                }
            });

            if (hasError) {
                return;
            }

            // Custom row validation
            if (_.isFunction(this.config.validateRow)) {
                this.config.validateRow(rowData, rowIndex + 1);
            }

            // Push the rowData
            this.response.data.push(rowData);
        });

        // Return response
        return this.response;
    }

    /**
     * @param {File} csvFile
     * @private
     */
    public validate(csvFile) {
        this.csvFile = csvFile;
        this.response = {
            inValidMessages: [],
            data: []
        };

        return new Promise((resolve, reject) => {
            Papa.parse(this.csvFile, {
                complete: (results) => {
                    this.csvData = results.data;
                    const dynamicHeaders = !_.isEmpty(this.allowedDynamicColumns) ? // 10
                    [...this.config.headers, ..._.filter(this.allowedDynamicColumns, columns => {
                        return _.includes(this.csvData[1], columns.name);
                    })] : [...this.config.headers];
                    this.config.headers = _.uniqBy(dynamicHeaders, 'inputName');
                    resolve(this.prepareDataAndValidateFile());
                },
                error: (error, file) => {
                    reject({ error: error, file: file });
                }
            });
        });
    }
}

results matching ""

    No results matching ""