@universis/candidates
Version:
Universis api server plugin for study program candidates, internship selection etc
213 lines (208 loc) • 11.2 kB
JavaScript
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;