File

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

Properties

data
data: any[]
Type : any[]
inValidMessages
inValidMessages: any[]
Type : any[]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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 ""