UNPKG

jsharmony

Version:

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

393 lines (366 loc) 18.3 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 Helper = require('./lib/Helper.js'); var HelperFS = require('./lib/HelperFS.js'); var _ = require('lodash'); var async = require('async'); var csv = require('csv'); module.exports = exports = {}; exports.getModelRecordset = function (req, res, fullmodelid, Q, P, rowlimit, options) { var _this = this; if (!options) options = {}; var model = this.jsh.getModel(req, fullmodelid); if (!Helper.hasModelAction(req, model, 'B')) { Helper.GenError(req, res, -11, _this._tP('Invalid Model Access for @fullmodelid', { fullmodelid })); return; } var fieldlist = this.getFieldNames(req, model.fields, 'B'); var filelist = this.getFileFieldNames(req, model.fields, 'B'); var keylist = this.getKeyNames(model.fields); var allfieldslist = _.union(keylist, fieldlist); var availablesortfieldslist = this.getFieldNames(req, model.fields, 'BFK'); var searchlist = this.getFieldNames(req, model.fields, 'FKS'); searchlist = _.union(this.getFieldNames(req, model.fields, 'B', function(field){ if(field.disable_search){ return false; } return true; }), searchlist); searchlist = _.union(keylist, searchlist); var encryptedfields = this.getFields(req, model.fields, '*', function(field){ return field.type=='encascii'; }); if (encryptedfields.length > 0) throw new Error('Encrypted fields not supported on GRID (field.type=encascii)'); encryptedfields = this.getEncryptedFields(req, model.fields, 'S'); //Encrypted fields can be used for search if ((encryptedfields.length > 0) && !(req.secure) && (!_this.jsh.Config.system_settings.allow_insecure_http_encryption)) { Helper.GenError(req, res, -51, 'Encrypted / hash fields require HTTPS connection'); return; } var db = _this.jsh.getModelDB(req, fullmodelid); var dbcontext = _this.jsh.getDBContext(req, model, db); if ('d' in Q) P = JSON.parse(Q.d); if (!_this.ParamCheck('Q', Q, ['|rowstart', '|rowcount', '|sort', '|search', '|searchjson', '|d', '|meta', '|getcount'])) { Helper.GenError(req, res, -4, 'Invalid Parameters'); return; } if (!_this.ParamCheck('P', P, _.map(_.union(searchlist, ['_action']), function (search) { return '|' + search; }))) { Helper.GenError(req, res, -4, 'Invalid Parameters'); return; } var getcount = ((('rowcount' in Q) && (Q.rowcount == -1)) || Q.getcount); var is_insert = (('_action' in P) && (P['_action'] == 'insert')); var is_browse = (('_action' in P) && (P['_action'] == 'browse')); delete P['_action']; //getQueryVars //Add query vars to Default, etc. var sql_ptypes = []; var sql_params = {}; var verrors = {}; var searchfields = this.getFieldsByName(model.fields, searchlist); var allfields = this.getFieldsByName(model.fields, allfieldslist); var sql_searchfields = []; _.each(searchfields, function (field) { if (field.name in P) { sql_searchfields.push(field); } }); var sortfields = []; var searchparams = []; var datalockqueries = []; //Merge sort parameters with parameters from querystring var dsort = new Array(); if (('sort' in Q) && (Q['sort'] != '')) dsort = JSON.parse(Q['sort']); else if ('sort' in model) dsort = model['sort']; //Check against allfieldslist var unsortedkeys = keylist.slice(); _.each(dsort, function (val) { if (!_.isString(val)) throw new Error('Invalid sort string'); if (val.length < 2) throw new Error('Invalid sort string'); var sortfield = val.substring(1); var lovtxtfield = ''; if(sortfield.indexOf('__'+_this.jsh.map.code_txt+'__')==0){ lovtxtfield = sortfield; sortfield = sortfield.substr(_this.jsh.map.code_txt.length + 4); } var sortdir = val[0]; if (sortdir == 'v') sortdir = 'desc'; else if (sortdir == '^') sortdir = 'asc'; else throw new Error('Invalid sort string'); if (!_.includes(availablesortfieldslist, sortfield)) throw new Error('Invalid sort field ' + sortfield); let field = _this.getFieldByName(model.fields, sortfield); var sortfieldname = sortfield; if(lovtxtfield && field.lov && !field.lov.showcode) sortfieldname = lovtxtfield; sortfields.push({ 'field': sortfieldname, 'dir': sortdir, 'sql': (field.sqlsort || '') }); if (_.includes(unsortedkeys, sortfield)) unsortedkeys = _.without(unsortedkeys, sortfield); }); if (unsortedkeys.length > 0) _.each(unsortedkeys, function (keyname) { sortfields.push({ 'field': keyname, 'dir': 'asc', 'sql': '' }); }); //Set search parameters if (('searchjson' in Q) && (Q.searchjson != '')) { var search_items = []; try{ search_items = JSON.parse(Q.searchjson); } catch(ex){ Helper.GenError(req, res, -4, 'Invalid Parameters'); return; } var search_join = 'and'; if (_.isArray(search_items) && (search_items.length > 0)) { for (let i = 0; i < search_items.length; i++) { var search_column = search_items[i].Column; var search_value = search_items[i].Value; var search_comparison = 'contains'; if ('Comparison' in search_items[i]) search_comparison = search_items[i].Comparison; if ('Join' in search_items[i]) search_join = search_items[i].Join; if (typeof search_column == 'undefined') continue; if (typeof search_value == 'undefined') continue; if (!(_.includes(['>', '<', '>=', '<=', '=', '<>', 'contains', 'notcontains', 'beginswith', 'endswith', 'null', 'notnull', 'soundslike'], search_comparison))) continue; if ((search_join != 'and') && (search_join != 'or')) continue; search_value = search_value.toString().trim(); if ((search_value == '') && (search_comparison != 'null') && (search_comparison != 'notnull')) continue; search_column = search_column.toString(); if (search_column != 'ALL') { var found_col = false; _.each(searchlist, function (searchlistcol) { if (search_column == searchlistcol) { search_column = searchlistcol; found_col = true; } }); if (!found_col) throw new Error('Invalid search field ' + search_column); } //Process search item if (search_column == 'ALL') { if (searchlist.length == 0) continue; var searchall = []; var searchlistfields = this.getFieldsByName(model.fields, searchlist); _.each(searchlistfields, function (field) { if(field.disable_search_all) return; if(!('disable_search_all' in field) && field.disable_search) return; var searchtermsql = _this.addSearchTerm(req, model, field, i, search_value, search_comparison, sql_ptypes, sql_params, verrors, { search_all: true }); if (searchtermsql) { if (searchall.length) searchall.push('or'); searchall.push(searchtermsql); } }); if (searchall.length) { if (searchparams.length) searchparams.push(search_join); searchparams.push(searchall); } } else { let field = this.getFieldByName(model.fields, search_column); var searchtermsql = this.addSearchTerm(req, model, field, i, search_value, search_comparison, sql_ptypes, sql_params, verrors); if (searchtermsql) { if (searchparams.length) searchparams.push(search_join); searchparams.push(searchtermsql); } } } } } if (model.grid_require_search && !searchparams.length) searchparams.push('1=0'); //Apply rowstart var rowstart = 0; if ('rowstart' in Q) rowstart = parseInt(Q['rowstart']); if (rowstart <= 0) rowstart = 0; //Apply rowcount if (typeof rowlimit == 'undefined') { if ('rowlimit' in model) rowlimit = model.rowlimit; else rowlimit = _this.jsh.Config.default_rowlimit; } var rowcount = rowlimit; if ('rowcount' in Q) { rowcount = parseInt(Q['rowcount']); if (rowcount <= 0) rowcount = rowlimit; if (rowcount > rowlimit) rowcount = rowlimit; } var keys = searchfields; for (let i = 0; i < keys.length; i++) { let field = keys[i]; var fname = field.name; if ((fname in P) && !(fname in sql_params)) { var dbtype = _this.getDBType(field); sql_ptypes.push(dbtype); sql_params[fname] = _this.DeformatParam(field, P[fname], verrors); } } for (let i = 0; i < model.fields.length; i++){ let field = model.fields[i]; if(field.lov && field.lov.sqlselect && field.lov.sqlselect_params){ for(var j = 0; j < field.lov.sqlselect_params.length; j++){ if(!(field.lov.sqlselect_params[j] in P)){ Helper.GenError(req, res, -99999, model.id + ' > ' + field.name + ': Missing lov.sqlselect parameter @'+field.lov.sqlselect_params[j] + ' - grid parameters must be passed in the querystring or bindings. The lov.sqlselect is inserted into the grid select statement; if this is a per-record lookup, use the expression LOVSQLTABLE.FIELD = MODELTABLE.FIELD'); return; } } } } //Add DataLock parameters to SQL this.getDataLockSQL(req, model, model.fields, sql_ptypes, sql_params, verrors, function (datalockquery) { datalockqueries.push(datalockquery); }, null, fullmodelid); verrors = _.merge(verrors, model.xvalidate.Validate('BFK', sql_params, undefined, undefined, undefined, { ignoreUndefined: true })); if (!_.isEmpty(verrors)) { Helper.GenError(req, res, -2, verrors[''].join('\n')); return; } var dbsql = db.sql.getModelRecordset(_this.jsh, model, sql_searchfields, allfields, sortfields, searchparams, datalockqueries, rowstart, rowcount + 1); //Add dynamic parameters from query string var dbtasks = {}; var dbtaskname = fullmodelid; if (!is_insert) { dbtasks[dbtaskname] = function (dbtans, callback) { db.Recordset(dbcontext, dbsql.sql, sql_ptypes, sql_params, dbtans, function (err, rslt, stats) { if (err != null) { err.model = model; err.sql = dbsql.sql; } else { if (stats) stats.model = model; if ((rslt != null) && (rslt.length > rowcount)) { rslt.pop(); rslt.push({ '_eof': false }); } else rslt.push({ '_eof': true }); //Verify files exist on disk if (filelist.length > 0) { if (keylist.length != 1) throw new Error('File download requires one key in the model'); //For each row async.eachOfLimit(rslt, 4, function(row, rownum, rowcallback){ if('_eof' in row) return rowcallback(); var filerslt = {}; var keyval = row[keylist[0]]; if(!keyval) throw new Error('Cannot process file fields in row #'+rownum+'. Key field not found in database select results.'); //For each file field async.each(filelist, function (file, filecallback) { var filefield = _this.getFieldByName(model.fields, file); var fpath = _this.jsh.Config.datadir + filefield.controlparams.data_folder + '/' + (filefield.controlparams.data_file_prefix||file) + '_' + keyval; if (filefield.controlparams._data_file_has_extension) fpath += '%%%EXT%%%'; HelperFS.getExtFileName(fpath, function(err, filename){ if(err){ filerslt[file] = false; return filecallback(null); } HelperFS.exists(filename, function (exists) { filerslt[file] = exists; return filecallback(null); }); }); }, function(err){ if(err) return rowcallback(err); _.merge(row, filerslt); return rowcallback(); }); }, function (err) { if (err != null){ _this.jsh.Log.error(err); return callback(Helper.NewError('Error performing file operation', -99999)); } callback(null, rslt, stats); }); return; } } callback(err, rslt, stats); }); }; } else { dbtasks[fullmodelid] = function (dbtrans, callback) { var rslt = []; callback(null, rslt); }; } if (getcount) { dbtasks['_count_' + dbtaskname] = function (dbtrans, callback) { db.Row(dbcontext, dbsql.rowcount_sql, sql_ptypes, sql_params, dbtrans, function (err, rslt, stats) { if ((err == null) && (rslt == null)) err = Helper.NewError('Count not found', -14); if (err != null) { err.model = model; err.sql = dbsql.rowcount_sql; } if (stats) stats.model = model; callback(err, rslt, stats); }); }; } if (Q.meta) { if(_this.addDefaultTasks(req, res, model, P, dbtasks)===false) return; if(_this.addLOVTasks(req, res, model, P, dbtasks, { action: (is_browse?'B':model.actions) })===false) return; if(_this.addBreadcrumbTasks(req, res, model, P, dbtasks, 'B')===false) return; if(_this.addTitleTasks(req, res, model, P, dbtasks, (is_browse?'B':'U'))===false) return; } return dbtasks; }; exports.exportCSV = function (req, res, dbtasks, fullmodelid, options) { var _this = this; var jsh = _this.jsh; if (!jsh.hasModel(req, fullmodelid)) throw new Error('Model not found'); options = _.extend({ /* columns: '["column1","column2"]' */ }, options); var model = jsh.getModel(req, fullmodelid); if(model.disable_csv_export) return Helper.GenError(req, res, -9, 'CSV Export of this data not supported'); var db = _this.jsh.getModelDB(req, fullmodelid); if(options.columns){ if(_.isString(options.columns)){ try { options.columns = JSON.parse(options.columns); } catch (e) { return Helper.GenError(req, res, -4, 'Invalid Parameters: ' + e.toString()); } } if(!_.isArray(options.columns)) return Helper.GenError(req, res, -4, 'Invalid Parameters'); } //Get list of columns to display var exportColumns = _this.getFieldNames(req, model.fields, 'B', function(field){ if(!('caption' in field)) return false; if(field.control=='hidden') return false; if(options.columns && !_.includes(options.columns, field.name)) return false; return true; }); //Execute SQL dbtasks = _.reduce(dbtasks, function (rslt, dbtask, key) { rslt[key] = async.apply(dbtask, undefined); return rslt; }, {}); db.ExecTasks(dbtasks, function (err, rslt, stats) { if (err != null) { _this.AppDBError(req, res, err, stats); return; } if (!(fullmodelid in rslt)) throw new Error('DB result missing model.'); var eof = false; if (_.isArray(rslt[fullmodelid]) && (_.isObject(rslt[fullmodelid][rslt[fullmodelid].length - 1])) && ('_eof' in rslt[fullmodelid][rslt[fullmodelid].length - 1])) { var rslt_eof = rslt[fullmodelid].pop(); eof = rslt_eof._eof; } //Add header if (rslt[fullmodelid].length > 0) { var header = {}; var frow = _.extend({}, rslt[fullmodelid][0]); for (let fcol in frow) { //Ignore columns that should not be exported if(!_.includes(exportColumns, fcol)){ delete frow[fcol]; continue; } var field = _this.getFieldByName(model.fields, fcol); if (field && ('caption' in field)) { header[fcol] = field.caption_ext || field.caption; } else if (fcol.indexOf('__' + jsh.map.code_txt + '__') == 0) { field = _this.getFieldByName(model.fields, fcol.substr(('__' + jsh.map.code_txt + '__').length)); if (field && ('caption' in field)) { header[fcol] = (field.caption_ext || field.caption || '') + ' Desc'; } else header[fcol] = fcol; } else header[fcol] = fcol; } rslt[fullmodelid].unshift(header); var dataidx = 1; //If data was truncated, add notification row if (!eof) { var eofrow = {}; for (let fcol in frow) { eofrow[fcol] = ''; } for (let fcol in frow) { eofrow[fcol] = 'Data exceeded limit of ' + _this.jsh.Config.export_rowlimit + ' rows, data has been truncated.'; break; } rslt[fullmodelid].unshift(eofrow); dataidx++; } //Process data for (var i = dataidx; i < rslt[fullmodelid].length; i++) { var crow = rslt[fullmodelid][i]; for (let ccol in crow) { if(!ccol) continue; //Overwrite code_val with code_txt if(Helper.beginsWith(ccol, '__'+jsh.map.code_txt+'__')){ var lovval = crow[ccol]; if(lovval !== null) crow[ccol.substr(jsh.map.code_txt.length+4)] = crow[ccol]; } //Replace Dates with ISO String if (_.isDate(crow[ccol])) { crow[ccol] = crow[ccol].toISOString();//.replace('T', ' ').replace('Z', ''); } } for (let ccol in crow) { if(!_.includes(exportColumns, ccol)){ delete crow[ccol]; continue; } } } } //No results found res.writeHead(200, { 'Content-Type': 'text/csv', //'Content-Length': stat.size, 'Content-Disposition': 'attachment; filename=' + encodeURIComponent(fullmodelid + '.csv') }); csv.stringify(rslt[fullmodelid], { quotedString: true }).pipe(res); }); }; return module.exports;