@universis/candidates
Version:
Universis api server plugin for study program candidates, internship selection etc
380 lines (360 loc) • 16.2 kB
JavaScript
;var _fs = _interopRequireDefault(require("fs"));
var _exceljs = require("exceljs");
var _data = require("@themost/data");
var _lodash = require("lodash");
var _common = require("@themost/common");
var _moment = _interopRequireDefault(require("moment"));function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {try {var info = gen[key](arg);var value = info.value;} catch (error) {reject(error);return;}if (info.done) {resolve(value);} else {Promise.resolve(value).then(_next, _throw);}}function _asyncToGenerator(fn) {return function () {var self = this,args = arguments;return new Promise(function (resolve, reject) {var gen = fn.apply(self, args);function _next(value) {asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);}function _throw(err) {asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);}_next(undefined);});};}
const XlsxContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
// use this value as a fallback if it hasn't been configured in the schema nor under settings/universis/candidates
// since this is already widely used
const DEFAULT_HEADER_ROW = 3;
let dataConfiguration;
function xlsPostParserWithConfig(context, opts = {}) {
return /*#__PURE__*/function () {var _ref = _asyncToGenerator(function* (req, _, next) {
try {
_common.Args.check(!!context, 'The application context cannot be empty');
const options = Object.assign(
{
fileProperty: 'file',
schemaProperty: 'schema' },
opts);
// get source file definition
const sourceFileDefinition = req && req.files && req.files[options.fileProperty];
_common.Args.check(!!sourceFileDefinition, 'The source xls(x) file cannot be empty at this context');
// validate source file content type
_common.Args.check(
sourceFileDefinition.contentType === XlsxContentType ||
/\.(xlsx)$/i.test(sourceFileDefinition.contentFileName) && sourceFileDefinition.contentType === 'application/octet-stream',
'The source file should be a valid xls(x) file');
// get config file definition
const schemaDefinition = req.files[options.schemaProperty];
_common.Args.check(!!schemaDefinition, 'The schema (configuration) file cannot be empty at this context');
// read config
const schema = JSON.parse(_fs.default.readFileSync(schemaDefinition.path));
// validate basic required attributes
_common.Args.check(
schema && Array.isArray(schema.columns) && typeof schema.model === 'string',
`The required "model" and/or "columns" properties are missing from the configuration file`);
// get data configuration strategy
const configuration = context.getApplication().getConfiguration();
dataConfiguration = configuration.getStrategy(_data.DataConfigurationStrategy);
_common.Args.check(!!dataConfiguration, 'The DataConfigurationStrategy may not be empty in this context');
// get target model definition
const model = (0, _lodash.cloneDeep)(dataConfiguration.getModelDefinition(schema.model));
_common.Args.check(!!model, `The target model ${schema.model} cannot be found in the current application context`);
// validate non-virtual every model attribute in the schema
for (const column of schema.columns) {
// if column is virtual, continue
if (column.virtual) {
continue;
}
// get model attribute
const modelAttribute = column.property && column.property.modelAttribute;
// and validate it
_common.Args.check(
!!modelAttribute,
`Missing "modelAtrribute" property for the non-virtual column ${column.property && column.property.title || 'empty'}`);
_common.Args.check(
attributeIsValid(modelAttribute, model),
`The modelAttribute ${modelAttribute} of the column ${column.property.title || 'empty'} is invalid in the current context of the ${model.name} entity`);
}
// validate every extra attribute also
for (const extraAttribute of schema.extraAttributes || []) {
// get model attribute
const modelAttribute = extraAttribute.modelAttribute;
// and validate it
_common.Args.check(!!modelAttribute, `The modelAttribute property is missing from the ${extraAttribute.title || 'empty'} extraAttribute`);
_common.Args.check(
attributeIsValid(modelAttribute, model),
`The modelAttribute ${modelAttribute} of the extra attribute ${extraAttribute.title || 'empty'} is invalid in the current context of the ${model.name} entity`);
_common.Args.check(
Object.prototype.hasOwnProperty.call(extraAttribute, 'defaultValue'),
`The defaultValue property is missing for the ${
extraAttribute.title || 'empty'
} extraAttribute, where modelAttribute is ${modelAttribute}`);
}
// read source file
const workbook = new _exceljs.Workbook();
yield workbook.xlsx.read(sourceFileDefinition);
// get and validate header row
const headerRow =
Number(schema.headerRow) || configuration.getSourceAt('settings/universis/candidates/headerRow') || DEFAULT_HEADER_ROW;
_common.Args.check(typeof headerRow === 'number', 'The header row is invalid or cannot be determined');
const body = [];
// enumerate worksheets
workbook.worksheets.forEach(sheet => {
let headers = [],
headersToSchemaColumns = new Map();
// enumerate rows
sheet.eachRow((row, rowNumber) => {
// do nothing until the headerRow
if (rowNumber < headerRow) {
return;
}
if (rowNumber === headerRow) {
// get headers
headers = row.values;
// validate that all headers are configured
const missingHeaders = [];
headers.forEach((header, index) => {
// find related column
const schemaColumn = schema.columns.find(column => {
return column.property && normalizeValue(column.property.title) === normalizeValue(header);
});
if (!schemaColumn) {
missingHeaders.push(header);
} else {
// add it to the map
headersToSchemaColumns.set(index, schemaColumn);
}
});
_common.Args.check(
missingHeaders.length === 0,
`Based on the specified header row ${headerRow}, the header(s) "${missingHeaders.join(
', ')
}" is/are missing from the configuration file. All headers must be configured (even if they are virtual)`);
return;
}
let rowData = {};
// enumerate row values
row.values.forEach((value, index) => {
// note: header indexes in exceljs are >= 1
if (index <= 0) {
return;
}
// get the schema column by header
const schemaColumn = headersToSchemaColumns.get(index);
// if the column is virtual, do nothing
if (schemaColumn.virtual) {
return;
}
// get modelAttribute
const modelAttribute = schemaColumn.property.modelAttribute;
// flatten the value
value = flattenValue(value);
// start handling the data
if (!Array.isArray(schemaColumn.dataMappings)) {
// check dateFormat
if (typeof schemaColumn.property.dateFormat === 'string' && typeof value === 'string') {
const dateFormat = schemaColumn.property.dateFormat;
// try to convert date strictly
const converted = (0, _moment.default)(value, dateFormat, true);
_common.Args.check(converted.isValid(), `The date ${value} cannot be strictly converted to the format ${dateFormat}`);
value = converted.toDate();
}
// if no dataMappings are set, just format the value
const formattedValue = formatValue(modelAttribute, value);
// and merge it with the existing row data
(0, _lodash.merge)(rowData, formattedValue);
} else {
// find source value
const mapping = schemaColumn.dataMappings.find(mapping => {
return normalizeValue(mapping.from) == normalizeValue(value);
});
// and validate it
_common.Args.check(
!!mapping,
`The source ("from") value ${value || 'empty'} is missing from the dataMappings of the column ${
schemaColumn.property.title
}`);
// validate target value
_common.Args.check(
Object.prototype.hasOwnProperty.call(mapping, 'to'),
`The target ("to") value is missing from the dataMappings of the column ${schemaColumn.property.title}, where "from" is ${mapping.from}`);
// format the value
const formattedValue = formatValue(modelAttribute, mapping.to);
// and merge it with the existing row data
(0, _lodash.merge)(rowData, formattedValue);
}
});
// enumerate extra attributes
if (Array.isArray(schema.extraAttributes)) {
schema.extraAttributes.forEach(attribute => {
// get model attribute
const modelAttribute = attribute.modelAttribute;
// format the value
const formattedValue = formatValue(modelAttribute, attribute.defaultValue);
// and merge it with the row data
(0, _lodash.merge)(rowData, formattedValue);
});
}
// push the row data
body.push(rowData);
});
});
// assign body to request
req.body = body;
// and exit
return next();
} catch (err) {
_common.TraceUtils.error(err);
return next(err);
}
});return function (_x, _x2, _x3) {return _ref.apply(this, arguments);};}();
}
function normalizeValue(value) {
if (typeof value !== 'string') {
return value;
}
return value.
normalize('NFKC').
replace(/[\p{Cc}\p{Cf}\u200B-\u200D\uFEFF]/gu, '').
replace(/\s+/g, ' ').
trim();
}
function populateFieldsFromInheritance(model) {
// validate model
if (model == null || dataConfiguration == null) {
return model;
}
// ensure model fields
model.fields = model.fields || [];
if (model.inherits) {
// get inherted model
let nextIheritedModel = (0, _lodash.cloneDeep)(dataConfiguration.getModelDefinition(model.inherits));
do {
// transfer non existing fields
nextIheritedModel.fields = nextIheritedModel.fields || [];
nextIheritedModel.fields.forEach(field => {
const exists = model.fields.find(baseField => {
return baseField.name === field.name;
});
if (!exists) {
model.fields.unshift(field);
}
});
// fetch next model, if any
nextIheritedModel = (0, _lodash.cloneDeep)(dataConfiguration.getModelDefinition(nextIheritedModel.inherits));
} while (nextIheritedModel);
}
if (model.implements) {
// get implemented model
let nextImplementedModel = (0, _lodash.cloneDeep)(dataConfiguration.getModelDefinition(model.implements));
do {
// ensure fields
nextImplementedModel.fields = nextImplementedModel.fields || [];
// transfer non existing fields
nextImplementedModel.fields.forEach(field => {
const exists = model.fields.find(baseField => {
return baseField.name === field.name;
});
if (!exists) {
model.fields.unshift(field);
}
});
// get next model, if any
nextImplementedModel = (0, _lodash.cloneDeep)(dataConfiguration.getModelDefinition(nextImplementedModel.implements));
} while (nextImplementedModel);
}
return model;
}
function attributeIsValid(attribute, model) {
try {
_common.Args.check(!!model && typeof attribute === 'string');
// populate model fields
populateFieldsFromInheritance(model);
// initiate current model as the parent model
let currentModel = model;
// split attribute into a stack
const attributesStack = attribute.split('/');
// while the stack is not empty
while (attributesStack.length > 0) {
// validate current model (important for 2nd+ iterations)
if (currentModel == null) {
return false;
}
// shift an element
const fieldName = attributesStack.shift();
// find field in the current model
const field = currentModel.fields.find(field => {
return field.name === fieldName;
});
// if it's not found, exit
if (field == null) {
return false;
}
// refresh current model and continue with the stack
currentModel = (0, _lodash.cloneDeep)(dataConfiguration.getModelDefinition(field.type));
populateFieldsFromInheritance(currentModel);
}
return true;
} catch (err) {
_common.TraceUtils.error(err);
return false;
}
}
function flattenValue(value) {
if (value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'text')) {
value = value.text;
}
if (value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'result')) {
value = value.result;
}
if (value !== null && typeof value === 'object' && Array.isArray(value.richText)) {
value = value.richText.map(richText => richText.text).join('');
}
if (typeof value === 'string' && value.trim().length === 0) {
return null;
}
return value;
}
function formatValue(path, value, delimiter = '/') {
const parts = path.split(delimiter).filter(Boolean);
return parts.reduceRight((acc, key) => ({ [key]: acc }), value);
}
/**
* 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 _exceljs.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 (Object.prototype.hasOwnProperty.call(row[key], '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;
//# sourceMappingURL=xls.js.map