|
| 1 | +import * as XLSX from 'xlsx'; |
| 2 | +import { cwd } from 'node:process'; |
| 3 | +import * as xlsxPopulate from 'xlsx-populate'; |
| 4 | +import { CONSTANTS } from '@shared/constants'; |
| 5 | +import { ParseConfig, parse } from 'papaparse'; |
| 6 | +import { ColumnDelimiterEnum, ColumnTypesEnum, Defaults, FileEncodingsEnum } from '@impler/shared'; |
| 7 | +import { EmptyFileException } from '@shared/exceptions/empty-file.exception'; |
| 8 | +import { InvalidFileException } from '@shared/exceptions/invalid-file.exception'; |
| 9 | +import { IExcelFileHeading } from '@shared/types/file.types'; |
| 10 | + |
| 11 | +export class ExcelFileService { |
| 12 | + async convertToCsv(file: Express.Multer.File, sheetName?: string): Promise<string> { |
| 13 | + return new Promise(async (resolve, reject) => { |
| 14 | + try { |
| 15 | + const wb = XLSX.read(file.buffer); |
| 16 | + const ws = sheetName && wb.SheetNames.includes(sheetName) ? wb.Sheets[sheetName] : wb.Sheets[wb.SheetNames[0]]; |
| 17 | + resolve( |
| 18 | + XLSX.utils.sheet_to_csv(ws, { |
| 19 | + blankrows: false, |
| 20 | + skipHidden: true, |
| 21 | + forceQuotes: true, |
| 22 | + // rawNumbers: true, // was converting 12:12:12 to 1.3945645673 |
| 23 | + }) |
| 24 | + ); |
| 25 | + } catch (error) { |
| 26 | + reject(error); |
| 27 | + } |
| 28 | + }); |
| 29 | + } |
| 30 | + formatName(name: string): string { |
| 31 | + return ( |
| 32 | + CONSTANTS.EXCEL_DATA_SHEET_STARTER + |
| 33 | + name |
| 34 | + .replace(/[^a-zA-Z0-9]/g, '') |
| 35 | + .toLowerCase() |
| 36 | + .slice(0, 25) // exceljs don't allow heading more than 30 characters |
| 37 | + ); |
| 38 | + } |
| 39 | + addSelectSheet(wb: any, heading: IExcelFileHeading): string { |
| 40 | + const name = this.formatName(heading.key); |
| 41 | + const addedSheet = wb.addSheet(name); |
| 42 | + addedSheet.cell('A1').value(heading.key); |
| 43 | + heading.selectValues.forEach((value, index) => addedSheet.cell(`A${index + 2}`).value(value)); |
| 44 | + |
| 45 | + return name; |
| 46 | + } |
| 47 | + |
| 48 | + getExcelColumnNameFromIndex(columnNumber: number) { |
| 49 | + // To store result (Excel column name) |
| 50 | + const columnName = []; |
| 51 | + |
| 52 | + while (columnNumber > 0) { |
| 53 | + // Find remainder |
| 54 | + const rem = columnNumber % 26; |
| 55 | + |
| 56 | + /* |
| 57 | + * If remainder is 0, then a |
| 58 | + * 'Z' must be there in output |
| 59 | + */ |
| 60 | + if (rem == 0) { |
| 61 | + columnName.push('Z'); |
| 62 | + columnNumber = Math.floor(columnNumber / 26) - 1; |
| 63 | + } // If remainder is non-zero |
| 64 | + else { |
| 65 | + columnName.push(String.fromCharCode(rem - 1 + 'A'.charCodeAt(0))); |
| 66 | + columnNumber = Math.floor(columnNumber / 26); |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + return columnName.reverse().join(''); |
| 71 | + } |
| 72 | + async getExcelFileForHeadings(headings: IExcelFileHeading[], data?: string): Promise<Buffer> { |
| 73 | + const currentDir = cwd(); |
| 74 | + const isMultiSelect = headings.some( |
| 75 | + (heading) => heading.type === ColumnTypesEnum.SELECT && heading.allowMultiSelect |
| 76 | + ); |
| 77 | + const workbook = await xlsxPopulate.fromFileAsync( |
| 78 | + `${currentDir}/src/config/${isMultiSelect ? 'Excel Multi Select Template.xlsm' : 'Excel Template.xlsx'}` |
| 79 | + ); |
| 80 | + const worksheet = workbook.sheet('Data'); |
| 81 | + const multiSelectHeadings = {}; |
| 82 | + |
| 83 | + headings.forEach((heading, index) => { |
| 84 | + const columnName = this.getExcelColumnNameFromIndex(index + 1); |
| 85 | + const columnHeadingCellName = columnName + '1'; |
| 86 | + if (heading.type === ColumnTypesEnum.SELECT && heading.allowMultiSelect) { |
| 87 | + worksheet |
| 88 | + .cell(columnHeadingCellName) |
| 89 | + .value(heading.key + '#MULTI' + '#' + (heading.delimiter || ColumnDelimiterEnum.COMMA)); |
| 90 | + multiSelectHeadings[heading.key] = heading.delimiter || ColumnDelimiterEnum.COMMA; |
| 91 | + } else worksheet.cell(columnHeadingCellName).value(heading.key); |
| 92 | + worksheet.column(columnName).style('numberFormat', '@'); |
| 93 | + }); |
| 94 | + |
| 95 | + const frozenColumns = headings.filter((heading) => heading.isFrozen).length; |
| 96 | + if (frozenColumns) worksheet.freezePanes(frozenColumns, 1); // freeze panes (freeze first n column and first row) |
| 97 | + else worksheet.freezePanes(0, 1); // freeze 0 column and first row |
| 98 | + |
| 99 | + headings.forEach((heading, index) => { |
| 100 | + if (heading.type === ColumnTypesEnum.SELECT) { |
| 101 | + const keyName = this.addSelectSheet(workbook, heading); |
| 102 | + const columnName = this.getExcelColumnNameFromIndex(index + 1); |
| 103 | + worksheet.range(`${columnName}2:${columnName}9999`).dataValidation({ |
| 104 | + type: 'list', |
| 105 | + allowBlank: !heading.isRequired, |
| 106 | + formula1: `${keyName}!$A$2:$A$9999`, |
| 107 | + ...(!heading.allowMultiSelect |
| 108 | + ? { |
| 109 | + showErrorMessage: true, |
| 110 | + error: 'Please select from the list', |
| 111 | + errorTitle: 'Invalid Value', |
| 112 | + } |
| 113 | + : {}), |
| 114 | + }); |
| 115 | + } |
| 116 | + }); |
| 117 | + const headingNames = headings.map((heading) => heading.key); |
| 118 | + const endColumnPosition = this.getExcelColumnNameFromIndex(headings.length + 1); |
| 119 | + |
| 120 | + let parsedData = []; |
| 121 | + try { |
| 122 | + if (data) parsedData = JSON.parse(data); |
| 123 | + } catch (error) {} |
| 124 | + if (Array.isArray(parsedData) && parsedData.length > 0) { |
| 125 | + const rows: string[][] = parsedData.reduce<string[][]>((acc: string[][], rowItem: Record<string, any>) => { |
| 126 | + acc.push( |
| 127 | + headingNames.map((headingKey) => |
| 128 | + multiSelectHeadings[headingKey] && Array.isArray(rowItem[headingKey]) |
| 129 | + ? rowItem[headingKey].join(multiSelectHeadings[headingKey]) |
| 130 | + : rowItem[headingKey] |
| 131 | + ) |
| 132 | + ); |
| 133 | + |
| 134 | + return acc; |
| 135 | + }, []); |
| 136 | + const rangeKey = `A2:${endColumnPosition}${rows.length + 1}`; |
| 137 | + const range = workbook.sheet(0).range(rangeKey); |
| 138 | + range.value(rows); |
| 139 | + } |
| 140 | + const buffer = await workbook.outputAsync(); |
| 141 | + |
| 142 | + return buffer as Promise<Buffer>; |
| 143 | + } |
| 144 | + getExcelSheets(file: Express.Multer.File): Promise<string[]> { |
| 145 | + return new Promise(async (resolve, reject) => { |
| 146 | + try { |
| 147 | + const wb = XLSX.read(file.buffer); |
| 148 | + resolve(wb.SheetNames); |
| 149 | + } catch (error) { |
| 150 | + reject(error); |
| 151 | + } |
| 152 | + }); |
| 153 | + } |
| 154 | + getExcelRowsColumnsCount(file: Express.Multer.File, sheetName?: string): Promise<{ rows: number; columns: number }> { |
| 155 | + return new Promise(async (resolve, reject) => { |
| 156 | + try { |
| 157 | + const wb = XLSX.read(file.buffer); |
| 158 | + const ws = sheetName && wb.SheetNames.includes(sheetName) ? wb.Sheets[sheetName] : wb.Sheets[wb.SheetNames[0]]; |
| 159 | + const range = ws['!ref']; |
| 160 | + const regex = /([A-Z]+)(\d+):([A-Z]+)(\d+)/; |
| 161 | + const match = range.match(regex); |
| 162 | + |
| 163 | + if (!match) reject(new InvalidFileException()); |
| 164 | + |
| 165 | + const [, startCol, startRow, endCol, endRow] = match; |
| 166 | + |
| 167 | + function columnToNumber(col: string) { |
| 168 | + let num = 0; |
| 169 | + for (let i = 0; i < col.length; i++) { |
| 170 | + num = num * 26 + (col.charCodeAt(i) - 64); |
| 171 | + } |
| 172 | + |
| 173 | + return num; |
| 174 | + } |
| 175 | + |
| 176 | + const columns = columnToNumber(endCol) - columnToNumber(startCol) + 1; |
| 177 | + const rows = parseInt(endRow) - parseInt(startRow) + 1; |
| 178 | + |
| 179 | + resolve({ |
| 180 | + columns, |
| 181 | + rows, |
| 182 | + }); |
| 183 | + } catch (error) { |
| 184 | + reject(error); |
| 185 | + } |
| 186 | + }); |
| 187 | + } |
| 188 | +} |
| 189 | + |
| 190 | +export class CSVFileService2 { |
| 191 | + getCSVMetaInfo(file: string | Express.Multer.File, options?: ParseConfig) { |
| 192 | + return new Promise<{ rows: number; columns: number }>((resolve, reject) => { |
| 193 | + let fileContent = ''; |
| 194 | + if (typeof file === 'string') { |
| 195 | + fileContent = file; |
| 196 | + } else { |
| 197 | + fileContent = file.buffer.toString(FileEncodingsEnum.CSV); |
| 198 | + } |
| 199 | + let rows = 0; |
| 200 | + let columns = 0; |
| 201 | + |
| 202 | + parse(fileContent, { |
| 203 | + ...(options || {}), |
| 204 | + dynamicTyping: false, |
| 205 | + skipEmptyLines: true, |
| 206 | + step: function (results) { |
| 207 | + rows++; |
| 208 | + if (Array.isArray(results.data)) { |
| 209 | + columns = results.data.length; |
| 210 | + } |
| 211 | + }, |
| 212 | + complete: function () { |
| 213 | + resolve({ rows, columns }); |
| 214 | + }, |
| 215 | + error: (error) => { |
| 216 | + if (error.message.includes('Parse Error')) { |
| 217 | + reject(new InvalidFileException()); |
| 218 | + } else { |
| 219 | + reject(error); |
| 220 | + } |
| 221 | + }, |
| 222 | + }); |
| 223 | + }); |
| 224 | + } |
| 225 | + |
| 226 | + getFileHeaders(file: string | Express.Multer.File, options?: ParseConfig): Promise<string[]> { |
| 227 | + return new Promise((resolve, reject) => { |
| 228 | + let fileContent = ''; |
| 229 | + if (typeof file === 'string') { |
| 230 | + fileContent = file; |
| 231 | + } else { |
| 232 | + fileContent = file.buffer.toString(FileEncodingsEnum.CSV); |
| 233 | + } |
| 234 | + let headings: string[]; |
| 235 | + let recordIndex = -1; |
| 236 | + parse(fileContent, { |
| 237 | + ...(options || {}), |
| 238 | + preview: 2, |
| 239 | + step: (results) => { |
| 240 | + recordIndex++; |
| 241 | + if (recordIndex === Defaults.ZERO) { |
| 242 | + if (Array.isArray(results.data) && results.data.length > Defaults.ZERO) headings = results.data as string[]; |
| 243 | + else reject(new EmptyFileException()); |
| 244 | + } else resolve(headings); |
| 245 | + }, |
| 246 | + error: (error) => { |
| 247 | + if (error.message.includes('Parse Error')) { |
| 248 | + reject(new InvalidFileException()); |
| 249 | + } else { |
| 250 | + reject(error); |
| 251 | + } |
| 252 | + }, |
| 253 | + complete: () => { |
| 254 | + if (recordIndex !== Defaults.ONE) { |
| 255 | + reject(new EmptyFileException()); |
| 256 | + } |
| 257 | + }, |
| 258 | + }); |
| 259 | + }); |
| 260 | + } |
| 261 | +} |
0 commit comments