UNPKG

@universis/candidates

Version:

Universis api server plugin for study program candidates, internship selection etc

213 lines (208 loc) 11.2 kB
import fs from "fs"; import path from "path"; import {Readable} from "stream"; import {Workbook} from 'exceljs'; const XlsxContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; /** * * @param {PostXlsWithConfigOptions} options * @returns {function(*, *, *): *} */ function xlsPostParserWithConfig(options) { return function (req, res, next) { let opts = Object.assign({ name: 'file', schema: 'schema' }, options); // get file key name let name = options?.name ?? opts.name; let schema = options?.schema ?? opts.schema; if (req?.files && req?.files[name] && Array.isArray(req?.files[name]) && req?.files[schema] && Array.isArray(req?.files[schema])) { const files = req?.files[name][0] const config = req?.files[schema][0]; let configStream; try { configStream = JSON.parse(fs.readFileSync(path.resolve(config.destination, config.filename))); } catch (err) { // throw error for invalid json file return next(err); } // get header row, use fixed value if not defined in config const headerRow = configStream.headerRow || 3; // get columns const columns = configStream.columns; // find max merged column depth, use fixed value if not defined in config let maxLevel = 0; columns.forEach(column => { if (column?.isMerged) { if (column?.isMerged?.value) { const level = column.isMerged.level; if (level > maxLevel) { maxLevel = level; } } } }); if (files.mimetype === XlsxContentType || ((/\.(xlsx)$/i).test(files.originalname) && files.mimetype === 'application/octet-stream')) { let stream; if (files.buffer instanceof Buffer) { stream = new Readable(); stream.push(files.buffer); stream.push(null); } else { try { stream = fs.createReadStream(path.resolve(files.destination, files.filename)); } catch (err) { return next(err); } } let body = [], headers = []; // read from a file let workbook = new Workbook(); return workbook.xlsx.read(stream) .then(() => { // read workbook let sheet = workbook.getWorksheet(1); sheet.eachRow((row, rowNumber) => { if (rowNumber >= headerRow) { if (rowNumber === headerRow + maxLevel) { headers = row.values; // replace whitespace in headers with a single space headers = headers.map(x => x.replace(/\s/g, " ").trim()); } else { let res = {}; row.values.forEach((v, k) => { if (k > 0) { const columnInConfig = columns.find(column => column?.property?.title.trim() === headers[k]); // if property is virtual (not used by model), return if (columnInConfig?.virtual) { return; } // if it is needed but is not mapped to a model attribute, throw error if (!columnInConfig?.property?.modelAttribute) { return next(new Error(`Invalid configuration. Model attribute is not defined for column ${headers[k]}`)); } if (!columnInConfig?.dataMappings) { if (typeof v === 'object' && v?.text && v?.hyperlink) { v = v.text; } // attributes may be like "property/childProperty for nested objects. // this makes sure that the correct object is created by splitting the // model attribute to property and childProperty and then appends the // the childProperties to the parent object. if (columnInConfig?.property?.modelAttribute.includes('/')) { if (!res[columnInConfig?.property?.modelAttribute.split('/')[0]]) res[columnInConfig?.property?.modelAttribute.split('/')[0]] = {}; if (typeof v === 'string') { v = v.trim(); if (v.length === 0) { // convert empty string to null v = null; } } res[columnInConfig?.property?.modelAttribute.split('/')[0]][columnInConfig?.property?.modelAttribute.split('/')[1]] = v; } else { if (typeof v === 'string') { v = v.trim(); if (v.length === 0) { // convert empty string to null v = null; } } res[columnInConfig?.property?.modelAttribute] = v; } } else { // get data mappings let mappings = columnInConfig.dataMappings; // find from value const value = mappings.find(x => x.from == v) // if it is not defined, throw error if (!value) { return next(new Error(`Invalid configuration. Incorrect or missing data mappings for column ${headers[k]}`)); } if (res[columnInConfig?.property?.modelAttribute]) { // append to object if it already exists res[columnInConfig?.property?.modelAttribute] = {...value.to, ...res[columnInConfig?.property?.modelAttribute]} } else { res[columnInConfig?.property?.modelAttribute] = value.to } // append extra attributes if (configStream?.extraAttributes) { configStream.extraAttributes.forEach(x => { res[x.modelAttribute] = x.defaultValue; }) } } } }); body.push(res); } } }); req.body = body; return next(); }).catch(err => { return next(err); }); } return next(); } else { return next(new Error("Missing candidate students xlsx file or its json schema.")); } } } /** * Allows data conversion to xls, independent from res. Similar to xlsParser function * @param {*} data The data to be converted * @returns Xlsx buffer */ function toXlsx(data) { return new Promise((resolve, reject) => { if (Array.isArray(data)) { const workbook = new Workbook(); const sheet = workbook.addWorksheet('Sheet1'); let rows = data.map(row => { let res = {}; Object.keys(row).forEach(key => { // if attribute is an object if (typeof row[key]==='object' && row[key] !== null) { // if attribute has property name if (row[key].hasOwnProperty('name')) { // return this name res[key] = row[key]['name']; } else { // otherwise return object res[key] = row[key]; } } else { // set property value res[key] = row[key]; } }); return res; }); // add columns if (rows.length > 0) { sheet.columns = Object.keys(rows[0]).map(key => { return { header: key, key: key }; }); } rows.forEach(row => { sheet.addRow(row); }); // write to a new buffer return workbook.xlsx.writeBuffer().then((buffer) => { return resolve(buffer); }).catch(err => { return reject(err); }); } else { return reject('Unprocessable entity'); } }); } module.exports.xlsPostParserWithConfig = xlsPostParserWithConfig; module.exports.XlsxContentType = XlsxContentType; module.exports.toXlsx = toXlsx;