UNPKG

jsharmony

Version:

Rapid Application Development (RAD) Platform for Node.js Database Application Development

922 lines (849 loc) 40.7 kB
/* Copyright 2017 apHarmony This file is part of jsHarmony. jsHarmony is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. jsHarmony is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this package. If not, see <http://www.gnu.org/licenses/>. */ var _ = require('lodash'); var fs = require('fs'); var tmp = require('tmp'); var async = require('async'); var HelperFS = require('./lib/HelperFS.js'); var Helper = require('./lib/Helper.js'); var ejs = require('ejs'); var ejsext = require('./lib/ejsext.js'); var moment = require('moment'); var querystring = require('querystring'); var _HEADER_ZOOM = 0.75; var _BROWSER_RECYCLE_COUNT = 50; function AppSrvRpt(appsrv) { this.AppSrv = appsrv; this.browser = null; this.browserreqcount = 0; this.browserqueue = null; this.InitReportQueue(); process.addListener('exit', function (code) { if (this.browser != null) { this.browser.close(); this.browser = null; } }); } AppSrvRpt.prototype.InitReportQueue = function () { var _this = this; this.browserqueue = async.queue(function (task, done) { _this.genReport(task.req, task.res, task.modelid, task.params, task.data, done); }, 1); }; AppSrvRpt.prototype.queueReport = function (req, res, fullmodelid, Q, P, params, onComplete) { if(!params) params = {}; var thisapp = this.AppSrv; var jsh = thisapp.jsh; var _this = this; var model = jsh.getModel(req, fullmodelid); var db = params.db; var dbcontext = params.dbcontext; var errorHandler = function(num, txt, stats){ return Helper.GenError(req, res, num, txt, { stats: stats }); }; if(params.errorHandler) errorHandler = params.errorHandler; if(req){ if (!Helper.hasModelAction(req, model, 'B')) { return errorHandler(-11, jsh._tP('Invalid Model Access for @fullmodelid', { fullmodelid })); } db = db || jsh.getModelDB(req, fullmodelid); dbcontext = dbcontext || jsh.getDBContext(req, model, db) || 'report'; } else if(!db) throw new Error('Either req or db is required.'); //Validate Parameters var fieldlist = thisapp.getFieldNames(req, model.fields, 'B'); _.map(fieldlist, function (field) { if (!(field in Q)) Q[field] = ''; }); if (!thisapp.ParamCheck('Q', Q, _.map(fieldlist, function (field) { return '&' + field; }).concat(_.map((req?_.keys(req.jshsite.datalock):[]), function(field) { return '|' + field; })))) { return errorHandler(-4, 'Invalid Parameters'); } if (!thisapp.ParamCheck('P', P, [])) { return errorHandler(-4, 'Invalid Parameters'); } if(req && !params.fromBatch) jsh.Log.info('REPORT: ' + req.originalUrl + ' ' + (req.user_id || '') + ' ' + (req.user_name || '')); var sql_ptypes = []; var sql_params = {}; var verrors = {}; var fields = thisapp.getFieldsByName(model.fields, fieldlist); //if (fields.length == 0) return onComplete(null, null); //Commented to enable reports with no parameters _.each(fields, function (field) { var fname = field.name; if (fname in Q) { var dbtype = thisapp.getDBType(field); sql_ptypes.push(dbtype); sql_params[fname] = thisapp.DeformatParam(field, Q[fname], verrors); } else return errorHandler(-4, 'Missing parameter ' + fname); }); verrors = _.merge(verrors, model.xvalidate.Validate('B', sql_params)); if (!_.isEmpty(verrors)) { return errorHandler(-2, verrors[''].join('\n')); } if(model.batch && !params.fromBatch) return _this.batchReport(req, res, db, dbcontext, model, sql_ptypes, sql_params, verrors, errorHandler, Q, params, onComplete); var dbtasks = {}; try{ this.parseReportSQLData(req, db, dbcontext, model, sql_ptypes, sql_params, verrors, dbtasks, model.reportdata); } catch(err){ jsh.Log.error(err); return errorHandler(-99999, err.toString()); } db.ExecTasks(dbtasks, function (err, dbdata, stats) { if (err) { if(jsh.Config.debug_params.report_debug) jsh.Log.debug(err); if(err.sql) jsh.Log.error('Error running SQL in report '+fullmodelid+' :: '+err.sql); return thisapp.AppDBError(req, res, err, stats, errorHandler); } if (dbdata == null) dbdata = {}; _this.MergeReportData(dbdata, model.reportdata, null); if(params.output=='html'){ return onComplete(null,_this.genReportContent(req, res, fullmodelid, sql_params, dbdata)); } else{ _this.browserqueue.push({ req: req, res: res, modelid: fullmodelid, params: sql_params, data: dbdata }, onComplete); } }); }; AppSrvRpt.prototype.batchReport = function (req, res, db, dbcontext, model, sql_ptypes, sql_params, verrors, errorHandler, Q, params, onComplete) { var thisapp = this.AppSrv; var jsh = thisapp.jsh; if(!model.batch || !model.batch.sql) return errorHandler(-6, 'Batch reports require model.batch.sql'); if(model.format != 'pdf') return errorHandler(-6, 'Batch reports requires model.format "pdf"'); //Parameters should already be validated //Add DataLock parameters to SQL var datalockqueries = []; if(req){ thisapp.getDataLockSQL(req, model, model.fields, sql_ptypes, sql_params, verrors, function (datalockquery) { datalockqueries.push(datalockquery); }); } var sql = db.sql.runReportBatch(jsh, model, datalockqueries); if(!req && (sql.indexOf('%%%DATALOCKS%%%')>=0)) throw new Error('Cannot use %%%DATALOCKS%%% in automated reports'); var dbtasks = {}; dbtasks['batchqueue'] = function (callback) { db.Recordset(dbcontext, sql, sql_ptypes, sql_params, function (err, rslt, stats) { if ((err == null) && (rslt == null)) err = Helper.NewError('Record not found', -1); if (err != null) { err.model = model; err.sql = sql; } if (stats) stats.model = model; callback(err, rslt, stats); }); }; db.ExecTasks(dbtasks, function (err, rslt, stats) { if (err) { return thisapp.AppDBError(req, res, err, stats, errorHandler); } if (rslt == null) rslt = {}; if(params.output=='html'){ //Generate Batch HTML var rptrslt = []; async.eachSeries(rslt.batchqueue, function(batchparams, cb){ batchparams = _.extend({}, Q, batchparams); thisapp.rptsrv.queueReport(req, res, model.id, batchparams, {}, _.extend({}, params, { db: db, dbcontext: dbcontext, errorHandler: errorHandler, fromBatch: true}), function (err, rptcontent) { if(err) return cb(err); rptrslt.push(rptcontent); return cb(); }); }, function(err){ if(err) return errorHandler(-99999, err); return onComplete(null, rptrslt); }); } else { //Generate Batch PDF var report_folder = jsh.Config.datadir + 'temp/report/'; HelperFS.createFolderIfNotExists(report_folder, function (err) { if (err) throw err; HelperFS.clearFiles(report_folder, jsh.Config.public_temp_expiration, -1, function () { tmp.file({ dir: report_folder }, function (batchtmperr, batchtmppath, batchtmpfd) { if (batchtmperr) return errorHandler(-99999, batchtmperr); var batchtmppdfpath = batchtmppath + '.pdf'; var pdfFiles = []; var batchdbdata = []; async.eachSeries(rslt.batchqueue, function(batchparams, cb){ batchparams = _.extend({}, Q, batchparams); thisapp.rptsrv.queueReport(req, res, model.id, batchparams, {}, _.extend({}, params, { db: db, dbcontext: dbcontext, errorHandler: errorHandler, fromBatch: true}), function (err, tmppath, dispose, dbdata) { if(err) return cb(err); batchdbdata.push(dbdata); /* Report Done */ HelperFS.getFileStats(req, res, tmppath, function (err, stat) { if (err){ dispose(); return cb('Report file not found'); } pdfFiles.push(tmppath); jsh.Extensions.report.getPdfMerge(function(err, pdfMerge){ if(err) return cb(err); pdfMerge(pdfFiles,{ output: batchtmppdfpath }).then(function(){ pdfFiles = [batchtmppdfpath]; dispose(); return cb(null); }); }); }); }); }, function(err){ // *** Be sure to call dispose independently of the callback, because dispose may fail, but it should not be treated as a critical failure var dispose = function(disposedone){ fs.close(batchtmpfd, function () { fs.unlink(batchtmppath, function (err) { if(disposedone) disposedone(); }); }); }; if(err) return errorHandler(-99999, err); return onComplete(null, batchtmppdfpath, dispose, batchdbdata); }); }); }); }); } }); }; AppSrvRpt.prototype.parseReportSQLData = function (req, db, dbcontext, model, sql_ptypes, sql_params, verrors, dbtasks, rdata) { var thisapp = this.AppSrv; var jsh = thisapp.jsh; var _this = this; if(req){ db = db || jsh.getModelDB(req, model.id); dbcontext = dbcontext || jsh.getDBContext(req, model, db) || 'report'; } else if(!db) throw new Error('Either req or db is required.'); _.each(rdata, function (dparams, dname) { if (!('sql' in dparams)) throw new Error(dname + ' missing sql'); var datalockqueries = []; var skipdatalock = true; if(req){ //Add DataLock parameters to SQL thisapp.getDataLockSQL(req, model, model.fields, sql_ptypes, sql_params, verrors, function (datalockquery) { datalockqueries.push(datalockquery); }, dparams.nodatalock); skipdatalock = false; if ('nodatalock' in dparams) { skipdatalock = true; for (var datalockid in req.jshsite.datalock) { if (Helper.arrayIndexOf(dparams.nodatalock,datalockid,{caseInsensitive:jsh.Config.system_settings.case_insensitive_datalocks}) < 0) skipdatalock = false; } } } var sql = db.sql.parseReportSQLData(jsh, dname, dparams, skipdatalock, datalockqueries); if(!req && (sql.indexOf('%%%DATALOCKS%%%')>=0)) throw new Error('Cannot use %%%DATALOCKS%%% in automated reports'); dbtasks[dname] = function (callback) { db.Recordset(dbcontext, sql, sql_ptypes, sql_params, function (err, rslt, stats) { if ((err == null) && (rslt == null)) err = Helper.NewError('Record not found', -1); if (err != null) { err.model = model; err.sql = sql; } if (stats) stats.model = model; callback(err, rslt, stats); }); }; if ('children' in dparams) _this.parseReportSQLData(req, db, dbcontext, model, sql_ptypes, sql_params, verrors, dbtasks, dparams.children); }); }; AppSrvRpt.prototype.MergeReportData = function (data, tree, parent) { var _this = this; _.each(tree, function (leaf, name) { if ('children' in leaf) _this.MergeReportData(data, leaf.children, name); //Post-order traversal if (parent == null) return; if (!(parent in data)) throw new Error('No parent result set found for ' + name); if (!(name in data)) throw new Error('No result set found for ' + name); var pdata = data[parent]; var cdata = data[name]; var bindings = leaf.bindings; for (var i = 0; i < pdata.length; i++) { var prow = pdata[i]; var pskip = false; _.each(bindings, function (bparent, bchild) { if (!(bparent in prow)) throw new Error('Parent result set ' + parent + ' missing binding ' + bparent); if (prow[bparent] === null) pskip = true; }); prow[name] = []; if (pskip) continue; for (var j = 0; j < cdata.length; j++) { var crow = cdata[j]; var bmatch = true; _.each(bindings, function (bparent, bchild) { if (!(bchild in crow)) throw new Error('Child result set ' + name + ' missing binding ' + bchild); if (crow[bchild] === null) bmatch = false; else if (prow[bparent] !== crow[bchild]) bmatch = false; }); if (bmatch) { prow[name].push(crow); cdata.splice(j, 1); j--; } } } delete data[name]; }); //Parse tree bottom-up // If leaf has no parent, return // For each parent row // Create array of CHILDNAME // Add all records matching binding into CHILDNAME // Delete leaf }; AppSrvRpt.prototype.genReportContent = function(req, res, fullmodelid, params, data){ var rslt = { header: '', body: '', footer: '' }; var _this = this; var jsh = _this.AppSrv.jsh; var model = jsh.getModel(req, fullmodelid); if(!model) throw new Error('Model '+fullmodelid+' not found'); if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report'); var ejsbody_header = Helper.ParseMultiLine(model.pageheader||''); var ejsbody_footer = Helper.ParseMultiLine(model.pagefooter||''); var ejsbody = Helper.ParseMultiLine(model.reportbody||''); if (!ejsbody && ((typeof model.reportbody == 'undefined') || (model.reportbody === null))){ ejsbody = 'REPORT BODY NOT FOUND'; } if (jsh.Config.debug_params.report_debug) { ejsbody = ejsbody.replace(/{{(.*?)}}/g, '<%=ejsext.null_log(jsh.Log,$1,\'$1\')%>'); ejsbody_header = ejsbody_header.replace(/{{(.*?)}}/g, '<%=ejsext.null_log(jsh.Log,$1,\'$1\')%>'); ejsbody_footer = ejsbody_footer.replace(/{{(.*?)}}/g, '<%=ejsext.null_log(jsh.Log,$1,\'$1\')%>'); } else { ejsbody = ejsbody.replace(/{{/g, '<%=ejsext.null_blank('); ejsbody = ejsbody.replace(/}}/g, ')%>'); ejsbody_header = ejsbody_header.replace(/{{/g, '<%=ejsext.null_blank('); ejsbody_header = ejsbody_header.replace(/}}/g, ')%>'); ejsbody_footer = ejsbody_footer.replace(/{{/g, '<%=ejsext.null_blank('); ejsbody_footer = ejsbody_footer.replace(/}}/g, ')%>'); } rslt.body = ejs.render(ejsbody, { model: model, moment: moment, _this: _this, jsh: jsh, ejsext: ejsext, data: data, params: params, _: _, filename: model.path }); if(ejsbody_header){ rslt.header = _this.RenderEJS(ejsbody_header, { model: model, moment: moment, _this: _this, jsh: jsh, ejsext: ejsext, data: data, params: params, _: _, pageNum: "<span class='pageNumber'></span>", numPages: "<span class='totalPages'></span>" }); } if(ejsbody_footer){ rslt.footer = _this.RenderEJS(ejsbody_footer, { model: model, moment: moment, _this: _this, jsh: jsh, ejsext: ejsext, data: data, params: params, _: _, pageNum: "<span class='pageNumber'></span>", numPages: "<span class='totalPages'></span>" }); } return rslt; }; AppSrvRpt.prototype.genReportPdf = function (req, res, fullmodelid, params, data, tmppath, callback) { var _this = this; var jsh = _this.AppSrv.jsh; var model = jsh.getModel(req, fullmodelid); if(!model) throw new Error('Model '+fullmodelid+' not found'); if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report'); _this.getBrowser(function (err, browser) { var page = null; function genReportError(err, source){ var rpterr = Helper.NewError('Error occurred during report generation'+(source ? ' - ' + source: '')+' (' + err.toString() + ')', -99999); if (page != null){ return page.close() .then(function(){ return callback(rpterr, null); }) .catch(function (err) { jsh.Log.error(err); return callback(rpterr, null); }); } else return callback(rpterr, null); } if(err) return genReportError(err, 'getBrowser'); try { browser.newPage().then(function (_page) { var tmppdfpath = tmppath + '.pdf'; var tmphtmlpath = tmppath + '.html'; page = _page; var pagesettings = { format: 'Letter', landscape: false, printBrackground: true, margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm', } }; var rptcontent = { header: '', body: '', footer: '' }; var contentWidth = 0; var contentHeight = 0; var font_css = ''; var font_render = []; var cancelRender = false; async.waterfall([ //model.onrender function(rpt_cb){ if(!model.onrender) return rpt_cb(); model.onrender({ page: page, content: rptcontent, data: data, params: params }, function(err, _cancelRender){ cancelRender = _cancelRender; return rpt_cb(err); }, require, jsh, fullmodelid); }, //Initialize report function(rpt_cb){ if(cancelRender) return rpt_cb(); rptcontent = _this.genReportContent(req, res, fullmodelid, params, data); if ('pagesettings' in model){ if(model.pagesettings.width && model.pagesettings.height) delete pagesettings.format; pagesettings = _.extend(pagesettings, model.pagesettings); } pagesettings.path = tmppdfpath; var dpi = 96; var default_header = '1cm';//Math.floor(0.4 * dpi) + 'px'; var headerheight = 0; var footerheight = 0; if(rptcontent.header){ pagesettings.displayHeaderFooter = true; pagesettings.headerTemplate = rptcontent.header; headerheight = default_header; if ('headerheight' in model) headerheight = model.headerheight; } if(rptcontent.footer){ pagesettings.displayHeaderFooter = true; pagesettings.footerTemplate = rptcontent.footer; footerheight = default_header; if ('footerheight' in model) footerheight = model.footerheight; } if(pagesettings.displayHeaderFooter) pagesettings.footerTemplate = pagesettings.footerTemplate || ' '; //page.set('viewportSize',{width:700,height:800},function(){ //Calculate page width var marginLeft = 0; var marginRight = 0; var marginTop = 0; var marginBottom = 0; var pageWidth = 1; //px var pageHeight = 1; //px var headerHeightPx = 0; var footerHeightPx = 0; if(pagesettings.margin){ var basemargin = pagesettings.margin; if(!basemargin) basemargin = 0; if(_.isString(basemargin)) basemargin = parseUnitsPx(basemargin,dpi); if(_.isNumber(basemargin)){ marginLeft = basemargin; marginRight = basemargin; marginTop = basemargin; marginBottom = basemargin; } else { if(basemargin.left) marginLeft = parseUnitsPx(basemargin.left,dpi); if(basemargin.right) marginRight = parseUnitsPx(basemargin.right,dpi); if(basemargin.top) marginTop = parseUnitsPx(basemargin.top,dpi); if(basemargin.bottom) marginBottom = parseUnitsPx(basemargin.bottom,dpi); } } if(pagesettings.format){ var fmt = pagesettings.format.toLowerCase(); var w = 1; var h = 1; //Width and height in millimeters if(fmt=='a0'){ w = 841; h=1189; } if(fmt=='a1'){ w = 594; h=841; } if(fmt=='a2'){ w = 420; h=594; } if(fmt=='a3'){ w = 297; h=420; } else if(fmt=='a4'){ w=210; h=297; } else if(fmt=='a5'){ w=148; h=210; } else if(fmt=='a6'){ w=105; h=148; } else if(fmt=='a7'){ w=74; h=105; } else if(fmt=='legal'){ w=215.9; h=355.6; } else if(fmt=='letter'){ w=215.9; h=279.4; } else if(fmt=='tabloid'){ w=279.4; h=431.8; } else if(fmt=='ledger'){ w=279.4; h=431.8; } else return jsh.Log.error('Invalid report format: '+pagesettings.format); if(pagesettings.landscape){ var _w = w; w = h; h = _w; } //Width and height in pixels pageWidth = (w / (25.4)) * dpi; pageHeight = (h / (25.4)) * dpi; } if(pagesettings.width) pageWidth = parseUnitsPx(pagesettings.width,dpi); if(pagesettings.height) pageHeight = parseUnitsPx(pagesettings.height,dpi); if(headerheight) headerHeightPx = parseUnitsPx(headerheight,dpi); if(footerheight) footerHeightPx = parseUnitsPx(footerheight,dpi); contentWidth = pageWidth - marginLeft - marginRight; contentHeight = pageHeight - marginTop - marginBottom - headerHeightPx - footerHeightPx; if (jsh.Config.debug_params.report_debug) { jsh.Log.debug('Calculated Page Size: '+contentHeight + 'x'+contentWidth); } pagesettings.margin = { top: (marginTop+headerHeightPx)+'px', right: marginRight+'px', bottom: (marginBottom+footerHeightPx)+'px', left: marginLeft+'px', }; if(pagesettings.headerTemplate){ pagesettings.headerTemplate = '<style type="text/css">#header{ padding:'+Math.round(marginTop*_HEADER_ZOOM)+'px '+Math.round(marginRight*_HEADER_ZOOM)+'px '+Math.round(marginBottom*_HEADER_ZOOM)+'px '+Math.round(marginLeft*_HEADER_ZOOM)+'px; -webkit-print-color-adjust: exact; }</style><div style="position:absolute;width:'+(contentWidth)+'px;font-size:12px;transform: scale('+_HEADER_ZOOM+'); transform-origin: top left;">'+pagesettings.headerTemplate+'</div>'; } if(pagesettings.footerTemplate){ pagesettings.footerTemplate = '<style type="text/css">#footer{ padding:'+Math.round(marginTop*_HEADER_ZOOM)+'px '+Math.round(marginRight*_HEADER_ZOOM)+'px '+Math.round(marginBottom*_HEADER_ZOOM)+'px '+Math.round(marginLeft*_HEADER_ZOOM)+'px; -webkit-print-color-adjust: exact; }</style><div style="position:absolute;width:'+(contentWidth)+'px;font-size:12px;transform: scale('+_HEADER_ZOOM+'); transform-origin: bottom left;">'+pagesettings.footerTemplate+'</div>'; } return rpt_cb(); }, //Load fonts function(rpt_cb){ if(cancelRender) return rpt_cb(); var report_fonts = [].concat(jsh.Config.default_report_fonts||[]).concat(model.fonts||[]); jsh.loadFonts(report_fonts, function(err, _font_css){ if(err) return jsh.Log.error(err); font_css = _font_css; for(var i=0;i<report_fonts.length;i++){ var font = report_fonts[i]; var font_str = ''; if(font['font-family']) font_str += "font-family:'"+Helper.escapeCSS(font['font-family'].toString())+"';"; if(font['font-style']) font_str += 'font-style:'+font['font-style'].toString()+';'; if(font['font-weight']) font_str += 'font-weight:'+font['font-weight'].toString()+';'; if(font_str) font_render.push(font_str); } if(font_css){ if(pagesettings.headerTemplate) pagesettings.headerTemplate = '<style type="text/css">'+font_css+'</style>' + pagesettings.headerTemplate; if(pagesettings.footerTemplate) pagesettings.footerTemplate = '<style type="text/css">'+font_css+'</style>' + pagesettings.footerTemplate; } return rpt_cb(); }); }, //model.onrendered function(rpt_cb){ if(!model.onrendered) return rpt_cb(); model.onrendered({ page: page, content: rptcontent, data: data, params: params }, function(err){ if(err) return genReportError(err, 'onrendered'); return rpt_cb(); }, require, jsh, fullmodelid); }, //Render function(rpt_cb){ if(cancelRender) return rpt_cb(); //Sets styles and returns document width var onPageLoad = function(font_render,font_css){ if(!document||!document.body) return 0; //Load CSS in header var head = document.getElementsByTagName('head'); if(head.length) head = head[0]; if(head){ var css = document.createElement('style'); css.type='text/css'; css.innerHTML = font_css; head.appendChild(css); } //Add fonts to body (otherwise using them in the page header / footer will cause a Page Crash) if(font_render) for(var i=0;i<font_render.length;i++){ var fontElement = document.createElement('div'); fontElement.innerHTML = '&nbsp;'; fontElement.setAttribute('style', font_render[i]+'visibility:hidden;position:absolute;top:0px;left:0px;'); document.body.appendChild(fontElement); } document.body.style['-webkit-print-color-adjust'] = 'exact'; return document.body.clientWidth; }; fs.writeFile(tmphtmlpath, rptcontent.body||'','utf8',function(err){ if(err) return jsh.Log.error(err); page.goto('file://'+tmphtmlpath, { timeout: jsh.Config.report_timeout, waitUntil: 'networkidle0' }) .then(function(){ page.evaluate(onPageLoad, font_render, font_css).then(function(documentWidth){ if(documentWidth && !pagesettings.scale){ var scale = contentWidth * 0.998 / documentWidth; if(scale < 0.1) scale = 0.1; if(scale > 1) scale = 1; pagesettings.scale = scale; } return rpt_cb(); }).catch(function (err) { genReportError(err, 'onPageLoad'); }); }).catch(function (err) { genReportError(err, 'page.goto'); }); }); }, //Save to PDF function(rpt_cb){ setTimeout(function(){ page.pdf(pagesettings).then(function () { page.close().then(function () { page = null; return rpt_cb(); }).catch(function (err) { jsh.Log.error(err); return rpt_cb(); }); }).catch(function (err) { genReportError(err, 'page.pdf'); }); }, (jsh.Config.debug_params.report_interactive ? 500000 : 0)); } ], function(err){ if(err) return genReportError(err); return callback(null, tmppdfpath); }); }).catch(function (err) { genReportError(err, 'browser.newPage'); }); } catch (err) { genReportError(err); } }); //, { dnodeOpts: { weak: false } } }; AppSrvRpt.prototype.genReportXlsx = function (req, res, fullmodelid, params, data, tmppath, callback) { var _this = this; var jsh = _this.AppSrv.jsh; var model = jsh.getModel(req, fullmodelid); if(!model) throw new Error('Model '+fullmodelid+' not found'); if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report'); function genReportError(err, source){ var rpterr = Helper.NewError('Error occurred during report generation'+(source ? ' - ' + source: '')+' (' + err.toString() + ')', -99999); jsh.Log.error(rpterr); return callback(rpterr, null); } var rsltpath = tmppath + '.xlsx'; jsh.Extensions.report.getExcelJS(function(err, excelJS){ if(err) return genReportError(err); try{ //Create workbook var cancelRender = false; var workbook = new excelJS.Workbook(); async.waterfall([ //model.onrender function(rpt_cb){ if(!model.onrender) return rpt_cb(); model.onrender({ workbook: workbook, data: data, params: params }, function(err, _cancelRender){ cancelRender = _cancelRender; return rpt_cb(err); }, require, jsh, fullmodelid); }, //Generate worksheets function(rpt_cb){ if(cancelRender) return rpt_cb(); var sheetNames = []; if(data) sheetNames = _.keys(data).reverse(); async.eachSeries(sheetNames, function(rsName, rs_cb){ var rsData = data[rsName]; try{ //Create worksheet var worksheet = workbook.addWorksheet(rsName); //Add Data if(rsData && rsData.length){ var cols = _.keys(rsData[0]); worksheet.addRow(cols); _.each(rsData, function(row){ worksheet.addRow(_.values(row)); }); if(cols.length){ //AutoFilter worksheet.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: cols.length }, }; } } //AutoSize _.each(worksheet.columns, function(col){ var maxlen = 10; col.eachCell({ includeEmpty: true }, function(cell){ var len = 0; if(_.isDate(cell.value)) len = 15; else len = (cell.value||'').toString().length + 3; if(len > 100) len = 100; if(len > maxlen) maxlen = len; }); col.width = maxlen; }); } catch(ex){ return genReportError(ex, 'Sheet '+rsName); } return rs_cb(); }, rpt_cb); }, //model.onrendered function(rpt_cb){ if(!model.onrendered) return rpt_cb(); model.onrendered({ workbook: workbook, data: data, params: params }, rpt_cb, require, jsh, fullmodelid); }, //Save to disk function(rpt_cb){ workbook.xlsx.writeFile(rsltpath) .then(function(){ return rpt_cb(); }) .catch(function(err){ return rpt_cb(err); }); }, ], function(err){ if(err) return genReportError(err); return callback(null, rsltpath); }); } catch(ex){ return genReportError(ex); } }); }; AppSrvRpt.prototype.genReport = function (req, res, fullmodelid, params, data, done) { var _this = this; var jsh = _this.AppSrv.jsh; var report_folder = jsh.Config.datadir + 'temp/report/'; var model = jsh.getModel(req, fullmodelid); if(!model) throw new Error('Model '+fullmodelid+' not found'); if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report'); HelperFS.createFolderIfNotExists(report_folder, function (err) { if (err) throw err; HelperFS.clearFiles(report_folder, jsh.Config.public_temp_expiration, -1, function () { tmp.file({ dir: report_folder }, function (tmperr, tmppath, tmpfd) { if (tmperr) throw tmperr; var reportFunc = (model.format == 'xlsx' ? _this.genReportXlsx : _this.genReportPdf); reportFunc.call(_this, req, res, fullmodelid, params, data, tmppath, function(err, rsltpath){ if(err) return done(err, null); var dispose = function (disposedone) { fs.close(tmpfd, function () { fs.unlink(rsltpath, function (err) { fs.unlink(tmppath, function (err) { if (disposedone) disposedone(); }); }); }); }; return done(null, rsltpath, dispose, data); }); }); }); }); }; function parseUnitsPx(val,dpi){ //mm,cm,in,px or no units = px if(!val) return 0; val = val.toLowerCase(); //Get value in px if(val.indexOf('mm') >= 0){ val = Helper.ReplaceAll(val,'mm','').trim(); val = parseFloat(val) * dpi / 25.4; } else if(val.indexOf('cm') >= 0){ val = Helper.ReplaceAll(val,'cm','').trim(); val = parseFloat(val) * dpi / 2.54; } else if(val.indexOf('in') >= 0){ val = Helper.ReplaceAll(val,'in','').trim(); val = parseFloat(val) * dpi; } else if(val.indexOf('px') >= 0){ val = Helper.ReplaceAll(val,'px','').trim(); val = parseFloat(val); } if(isNaN(val)) return 0; return Math.floor(val); } AppSrvRpt.prototype.RenderEJS = function (ejssrc, ejsdata) { return ejs.render(ejssrc, _.extend(ejsdata, { _: _, ejsext: ejsext })); }; AppSrvRpt.prototype.getBrowser = function (callback) { var _this = this; var jsh = _this.AppSrv.jsh; if (_this.browser) { //Recycle browser after _BROWSER_RECYCLE_COUNT uses _this.browserreqcount++; if (_this.browserreqcount >= _BROWSER_RECYCLE_COUNT) { jsh.Log.info('Recycling Report Renderer'); var oldbrowser = _this.browser; _this.browser = null; return oldbrowser.close() .then(function(){ return _this.getBrowser(callback); }) .catch(function(err){ var errmsg = 'Cound not exit report renderer: '+err.toString(); jsh.Log.error(errmsg); return callback(new Error(errmsg)); }); } else return callback(null, _this.browser); } jsh.Log.info('Launching Report Renderer'); jsh.Extensions.report.getPuppeteer(function(err, puppeteer){ if(err){ jsh.Log.error(err.toString()); return callback(new Error(err.toString())); } var launchParams = { ignoreHTTPSErrors: true }; if(jsh.Config.debug_params.report_interactive) launchParams.headless = false; puppeteer.launch(launchParams) .then(function(rslt){ _this.browser = rslt; _this.browser.on('disconnected', function(){ _this.browser = null; }); _this.browserreqcount = 0; return callback(null, _this.browser); }) .catch(function(err){ jsh.Log.error(err); return callback(err); }); }); }; AppSrvRpt.prototype.runReportJob = function (req, res, fullmodelid, Q, P, onComplete) { var thisapp = this.AppSrv; var jsh = thisapp.jsh; var model = jsh.getModel(req, fullmodelid); if(!model) throw new Error('Model '+fullmodelid+' not found'); if (!Helper.hasModelAction(req, model, 'B')) { Helper.GenError(req, res, -11, jsh._tP('Invalid Model Access for @fullmodelid', { fullmodelid })); return; } if (!('jobqueue' in model)) throw new Error(fullmodelid + ' job queue not enabled'); if (!thisapp.JobProc) throw new Error('Job Processor not configured'); if (model.layout !== 'report') throw new Error('Model '+fullmodelid+' is not a report'); //Validate Parameters var fieldlist = thisapp.getFieldNames(req, model.fields, 'B'); _.map(fieldlist, function (field) { if (!(field in Q)) Q[field] = ''; }); var Qfields = _.map(fieldlist, function (field) { return '&' + field; }).concat(_.map(_.keys(req.jshsite.datalock), function(field) { return '|' + field; })); Qfields.push('|_test'); if (!thisapp.ParamCheck('Q', Q, Qfields)) { Helper.GenError(req, res, -4, 'Invalid Parameters'); return; } if (!thisapp.ParamCheck('P', P, [])) { Helper.GenError(req, res, -4, 'Invalid Parameters'); return; } var sql_ptypes = []; var sql_params = {}; var verrors = {}; var db = jsh.getModelDB(req, fullmodelid); var dbcontext = jsh.getDBContext(req, model, db); var fields = thisapp.getFieldsByName(model.fields, fieldlist); if (fields.length == 0) return onComplete(null, {}); _.each(fields, function (field) { var fname = field.name; if (fname in Q) { var dbtype = thisapp.getDBType(field); sql_ptypes.push(dbtype); sql_params[fname] = thisapp.DeformatParam(field, Q[fname], verrors); } else throw new Error('Missing parameter ' + fname); }); verrors = _.merge(verrors, model.xvalidate.Validate('B', sql_params)); if (!_.isEmpty(verrors)) { Helper.GenError(req, res, -2, verrors[''].join('\n')); return; } if (!('sql' in model.jobqueue)) throw new Error(fullmodelid + ' missing job queue sql'); //Add DataLock parameters to SQL var datalockqueries = []; thisapp.getDataLockSQL(req, model, model.fields, sql_ptypes, sql_params, verrors, function (datalockquery) { datalockqueries.push(datalockquery); }); var sql = db.sql.runReportJob(jsh, model, datalockqueries); var dbtasks = {}; dbtasks['jobqueue'] = function (callback) { db.Recordset(dbcontext, sql, sql_ptypes, sql_params, function (err, rslt, stats) { if ((err == null) && (rslt == null)) err = Helper.NewError('Record not found', -1); if (err != null) { err.model = model; err.sql = sql; } if (stats) stats.model = model; callback(err, rslt, stats); }); }; db.ExecTasks(dbtasks, function (err, rslt, stats) { if (err != null) { thisapp.AppDBError(req, res, err, stats); return; } if (rslt == null) rslt = {}; if (('_test' in Q) && (Q._test == 1)) { for (let i = 0; i < rslt.jobqueue.length; i++) { var reporturl = req.baseurl + '_d/_report/' + model.id + '/?'; let jrow = rslt.jobqueue[i]; let rparams = {}; let verrors = {}; //Add each parameter to url _.each(fields, function (field) { var fname = field.name; if (fname in jrow) rparams[fname] = thisapp.DeformatParam(field, jrow[fname], verrors); else rparams[fname] = sql_params[fname]; if (_.isDate(rparams[fname])) rparams[fname] = rparams[fname].toISOString(); }); verrors = _.merge(verrors, model.xvalidate.Validate('B', rparams)); if (!_.isEmpty(verrors)) { Helper.GenError(req, res, -99999, 'Error during job queue: ' + verrors[''].join('\n') + ' ' + JSON.stringify(rparams)); return; } reporturl += querystring.stringify(rparams); rslt.jobqueue[i] = _.merge({ 'Run Report': '<a href="' + reporturl + '" target="_blank">Run Report</a>' }, jrow); } res.send(Helper.renderTable(rslt.jobqueue)); return; } else { var jobtasks = {}; for (let i = 0; i < rslt.jobqueue.length; i++) { let jrow = rslt.jobqueue[i]; let rparams = {}; let verrors = {}; //Add each parameter to url _.each(fields, function (field) { var fname = field.name; if (fname in jrow) rparams[fname] = thisapp.DeformatParam(field, jrow[fname], verrors); else rparams[fname] = sql_params[fname]; if (_.isDate(rparams[fname])) rparams[fname] = rparams[fname].toISOString(); }); verrors = _.merge(verrors, model.xvalidate.Validate('B', rparams)); if (!_.isEmpty(verrors)) { Helper.GenError(req, res, -99999, 'Error during job queue: ' + verrors[''].join('\n') + ' ' + JSON.stringify(rparams)); return; } if(!thisapp.JobProc.AddDBJob(req, res, jobtasks, i, jrow, model.id, rparams)) return; } thisapp.JobProc.db.ExecTransTasks(jobtasks, function (err, rslt, stats) { if (err != null) { thisapp.AppDBError(req, res, err, stats); return; } else rslt = { '_success': _.size(jobtasks) }; rslt['_stats'] = Helper.FormatStats(req, stats); res.type('json'); res.send(JSON.stringify(rslt)); }); } }); }; module.exports = AppSrvRpt;