UNPKG

jsharmony

Version:

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

1,148 lines (1,086 loc) 158 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 async = require('async'); var fs = require('fs'); var path = require('path'); var crypto = require('crypto'); var Helper = require('./lib/Helper.js'); var HelperFS = require('./lib/HelperFS.js'); var jsHarmonyCodeGen = require('./lib/CodeGen.js'); var jsParser = require('./lib/JSParser.js'); module.exports = exports = {}; /******************* | LOAD MODELS | *******************/ var BASE_CONTROLS = ['label', 'html', 'textbox', 'textzoom', 'dropdown', 'date', 'textarea', 'htmlarea', 'hidden', 'subform', 'html', 'password', 'file_upload', 'file_download', 'button', 'linkbutton', 'tree', 'checkbox','image','tagbox']; var BASE_DATATYPES = ['DATETIME','VARCHAR','CHAR','BOOLEAN','BIGINT','INT','SMALLINT','TINYINT','DECIMAL','FLOAT','DATE','DATETIME','TIME','ENCASCII','HASH','FILE','BINARY']; //Get array of all model folders exports.getModelDirs = function(){ var rslt = []; for(var moduleName in this.Modules){ var module = this.Modules[moduleName]; var modelPath = module.getModelPath(); if(modelPath){ rslt.push({ module: moduleName, path: modelPath, namespace: module.namespace }); } } return rslt; }; exports.SetModels = function (models) { this.Models = models; }; exports.LoadModels = function (modelbasedir, modeldir, prefix, dbtype, moduleName, options) { options = _.extend(options, { isBaseDir: true }); var _this = this; var dbDrivers = this.getDBDrivers(); if (typeof prefix == 'undefined') prefix = ''; if (typeof dbtype == 'undefined') dbtype = ''; if(!fs.existsSync(modelbasedir)){ _this.LogInit_ERROR('Model folder ' + modelbasedir + ' not found'); return; } var fmodels = fs.readdirSync(modelbasedir); for (let i in fmodels) { var fname = fmodels[i]; var fpath = modelbasedir + fname; var fstat = fs.lstatSync(fpath); if(fstat.isDirectory()){ if(options.isBaseDir){ if(fname=='js') continue; if(fname=='sql') continue; if(fname=='public_css') continue; } _this.LoadModels(fpath + '/', modeldir, prefix + fname + '/', dbtype, moduleName, { isBaseDir: false }); } if (fname.indexOf('.json', fname.length - 5) == -1) continue; if (fname == '_canonical.json') continue; var modelname = prefix + fname.replace('.json', ''); if (dbtype && (fname.indexOf('.' + dbtype + '.') < 0)) { var found_other_dbtype = false; _.each(dbDrivers, function (odbtype) { if (fname.indexOf('.' + odbtype + '.') >= 0) found_other_dbtype = true; }); if (found_other_dbtype) continue; } else{ //Model is specific to this database modelname = prefix + fname.replace('.' + dbtype + '.', '.').replace('.json', ''); } _this.LogInit_INFO('Loading ' + modelname); var modelbasename = _this.getBaseModelName(modelname); var model = _this.ParseJSON(fpath, moduleName, 'Model ' + modelname); if (modelbasename == '_controls') { for (var c in model) this.CustomControls[c] = model[c]; } else if (modelbasename == '_config') { continue; } else { if (!('layout' in model) && !('inherits' in model)) { //Parse file as multiple-model file _.each(model, function (submodel, submodelname) { if(_.isString(submodel)){ _this.LogInit_ERROR('Invalid model definition: ' + fpath + '. Each model must have a "layout" or "inherits" property.'); return; } if(submodelname && (submodelname[0]=='/')) submodelname = submodelname.substr(1); else submodelname = prefix + submodelname; _this.LogInit_INFO('Loading sub-model ' + submodelname); _this.AddModel(submodelname, submodel, prefix, fpath, modeldir, moduleName); }); } else this.AddModel(modelname, model, prefix, fpath, modeldir, moduleName); } } }; exports.ParseJSON = function(fname, moduleName, desc, cb, options){ if(!options) options = { fatalError: true }; var _this = this; var fread = null; //Transform var module = _this.Modules[moduleName]; function _transform(txt){ if(!module) return txt; return module.transform.Apply(txt, fname); } if(cb) fread = function(fread_cb){ fs.readFile(fname, 'utf8', fread_cb); }; else fread = function(fread_cb){ return fread_cb(null, fs.readFileSync(fname, 'utf8')); }; return fread(function(err, data){ if(err){ if(cb) return cb(err); throw err; } var fdir = path.dirname(fname); var ftext = fs.readFileSync(fname, 'utf8'); ftext = _transform(Helper.JSONstrip(ftext)); //Parse JSON var rslt = null; try { rslt = jsParser.Parse(ftext, fname, { functions: { '@importstr': function(filename){ var filepath = filename; if(!path.isAbsolute(filepath)) filepath = path.join(fdir, filepath); return _transform(fs.readFileSync(filepath, 'utf8')); }, '@importjson': function(filename){ var filepath = filename; if(!path.isAbsolute(filepath)) filepath = path.join(fdir, filepath); return _this.ParseJSON(filepath, moduleName, desc + ' :: Script ' + filename); }, '@merge': function(){ if(!arguments.length) return null; var accumulator = arguments[0]; for(var i=1;i<arguments.length;i++){ if(_.isArray(accumulator)) accumulator = accumulator.concat(arguments[i]); else if(_.isString(accumulator)) accumulator += arguments[i]; else if(_.isObject(accumulator)) _.extend(accumulator, arguments[i]); else { if(!accumulator) accumulator = arguments[i]; } } return accumulator; }, } }).Tree; } catch (ex2) { _this.Log.console_error('-------------------------------------------'); var errmsg = (options.fatalError ? 'FATAL ' : '') + 'ERROR Parsing ' + desc + ' in ' + fname + '\n'; if('startpos' in ex2){ errmsg += 'Error: Parse error on line ' + ex2.startpos.line + ', char ' + ex2.startpos.char + '\n'; var eline = Helper.getLine(ftext, ex2.startpos.line); if(typeof eline != 'undefined'){ errmsg += eline + '\n'; for(let i=0;i<ex2.startpos.char;i++) errmsg += '-'; errmsg += '^\n'; } errmsg += ex2.message; } else errmsg += ex2.toString(); _this.Log.console_error(errmsg); _this.Log.console(ex2.stack); _this.Log.console_error('-------------------------------------------'); if(options.fatalError) process.exit(8); if(cb) return cb(new Error(errmsg)); throw (new Error(errmsg)); } if(cb) return cb(null, rslt); else return rslt; }); }; exports.MergeFolder = function (dir, moduleName) { var _this = this; var f = {}; if (fs.existsSync(dir)) f = fs.readdirSync(dir); else return ''; var rslt = ''; f.sort(function (a, b) { var abase = a; var bbase = b; var a_lastdot = a.lastIndexOf('.'); var b_lastdot = b.lastIndexOf('.'); if (a_lastdot > 0) abase = a.substr(0, a_lastdot); if (b_lastdot > 0) bbase = b.substr(0, b_lastdot); if (abase == bbase) return a > b; return abase > bbase; }); for (let i in f) { var fname = dir + f[i]; _this.LogInit_INFO('Loading ' + fname); var ftext = fs.readFileSync(fname, 'utf8'); rslt += ftext + '\r\n'; } //Apply Transform var module = _this.Modules[moduleName]; if(module) rslt = module.transform.Apply(rslt, fname); return rslt; }; exports.AddModel = function (modelname, model, prefix, modelpath, modeldir, moduleName) { var _this = this; var module = _this.Modules[moduleName]; function prependPropFile(prop, path){ if (fs.existsSync(path)) { var fcontent = fs.readFileSync(path, 'utf8'); if(module) fcontent = module.transform.Apply(fcontent, path); if (prop in model) fcontent += '\r\n' + model[prop]; model[prop] = fcontent; } } //------------------------- if(model===null){ delete this.Models[modelname]; return; } if(!prefix) prefix = ''; model['id'] = modelname; model['idmd5'] = crypto.createHash('md5').update(_this.Config.frontsalt + model.id).digest('hex'); if('namespace' in model){ _this.LogInit_ERROR(model.id + ': "namespace" attribute should not be set, it is a read-only system parameter'); } model._inherits = []; model._transforms = []; model._referencedby = []; model._parentmodels = { tab: {}, duplicate: {}, subform: {}, popuplov: {}, button: {} }; model._parentbindings = {}; model._childbindings = {}; model._auto = {}; model._sysconfig = { unbound_meta: false }; if(!model.path && modelpath) model.path = modelpath; if(!model.module && modeldir && modeldir.module) model.module = modeldir.module; model.namespace = _this.getNamespace(modelname); model.using = model.using || []; if(!_.isArray(model.using)) model.using = [model.using]; if(prefix != model.namespace) model.using.push(prefix); if(module && (module.namespace != prefix) && (module.namespace != model.namespace)) model.using.push(module.namespace); for(let i=0;i<model.using.length;i++){ //Resolve "using" paths var upath = model.using[i]; upath = _this.getCanonicalNamespace(upath, modeldir.namespace); model.using[i] = upath; } if(!('fields' in model)) model.fields = []; if(_.isString(model.buttons)){ _this.LogInit_ERROR(model.id + ': Invalid value for model.buttons attribute'); delete model.buttons; } _.each(model.buttons, function(button){ button._orig_model = modelname; }); if('css' in model) model.css = Helper.ParseMultiLine(model.css); if('js' in model) model.js = Helper.ParseMultiLine(model.js); //if (modelname in this.Models) throw new Error('Cannot add ' + modelname + '. The model already exists.') var modelbasedir = ''; if(model.path) modelbasedir = path.dirname(model.path) + '/'; if(modelbasedir){ //var modelpathbase = modelpath.substr(0,modelpath.length-5); if(!('source_files_prefix' in model)) model.source_files_prefix = _this.getBaseModelName(model.id); var modelpathbase = modelbasedir + model.source_files_prefix; //Load JS prependPropFile('js',modelpathbase + '.js'); //Load JS Library prependPropFile('jslib',modelpathbase + '.lib.js'); //Load CSS var cssfname = (modelpathbase + '.css'); if(model.layout=='report') cssfname = (modelpathbase + '.form.css'); prependPropFile('css',cssfname); //Load EJS var ejsfname = (modelpathbase + '.ejs'); if(model.layout=='report') ejsfname = (modelpathbase + '.form.ejs'); prependPropFile('ejs',ejsfname); //Load Header EJS var headerfname = (modelpathbase + '.header.ejs'); if(model.layout=='report') headerfname = (modelpathbase + '.form.header.ejs'); prependPropFile('header',headerfname); //Load Report EJS if(model.layout=='report'){ prependPropFile('pageheader',modelpathbase + '.header.ejs'); prependPropFile('pagefooter',modelpathbase + '.footer.ejs'); prependPropFile('reportbody',modelpathbase + '.ejs'); } //Load "onroute" handler prependPropFile('onroute',modelpathbase + '.onroute.js'); } if (!('helpid' in model) && !('inherits' in model)) model.helpid = modelname; if ('onroute' in model) model.onroute = _this.createFunction(model.onroute, ['routetype', 'req', 'res', 'callback', 'require', 'jsh', 'modelid', 'params'], model.id+' model.onroute', model.path); //Check if model inherits self - if so, add to _transforms if((modelname in this.Models) && model.inherits){ var parentModel = _this.getModel(null,model.inherits,model); if(parentModel===this.Models[modelname]){ parentModel._transforms.push(model); return; } } this.Models[modelname] = model; }; exports.ParseModelInheritance = function () { var _this = this; var foundinheritance = true; //Add model groups _.forOwn(this.Models, function (model) { if(!model.groups) model.groups = []; if(!_.isArray(model.groups)) throw new Error(model.id + ': model.groups must be an array'); for(var modelgroup in _this.Config.model_groups){ if(_.includes(_this.Config.model_groups[modelgroup],model.id)) model.groups.push(modelgroup); } //Handle field aliases _.each(model.fields, function(field){ if(!field) return; if('newline' in field){ if('nl' in field) delete field.newline; else { field.nl = field.newline; delete field.newline; } } if(field.value) field.value = Helper.ParseMultiLine(field.value); }); }); while (foundinheritance) { foundinheritance = false; _.forOwn(this.Models, function (model) { if ('inherits' in model) { foundinheritance = true; var curmodelid = _this.resolveModelID(model.id); var parentmodel = _this.getModel(null,model.inherits,model,{ ignore: [curmodelid] }); if (!parentmodel){ if(_this.getModel(null,model.inherits,model)) throw new Error('Model ' + model.id + ' cyclic inheritance.'); throw new Error('Model ' + model.id + ': Parent model ' + model.inherits + ' does not exist.'); } if (parentmodel.id == model.id) throw new Error('Model ' + model.id + ' cyclic inheritance.'); var origparentmodel = parentmodel; var parentinheritance = parentmodel.inherits; if (typeof parentinheritance !== 'undefined') return; parentmodel = JSON.parse(JSON.stringify(parentmodel)); //Deep clone if(origparentmodel.onroute) parentmodel.onroute = origparentmodel.onroute; model._inherits = parentmodel._inherits.concat([model.inherits]); if(!_.includes(model.using, parentmodel.namespace)) model.using.push(parentmodel.namespace); //Add Parent Model Groups model.groups = _.union(parentmodel.groups, model.groups); model.using = _.union(parentmodel.using, model.using); //Merge Models //Extend this to enable delete existing values by making them NULL //Extend this to enable merging arrays, like "button", "fields", "roles" using key, other arrays just overwrite var mergedprops = {}; EntityPropMerge(mergedprops, 'fields', model, parentmodel, function (newval, oldval) { return _this.MergeModelArray(newval, oldval, function(newItem, oldItem, rsltItem){ if ('validate' in newItem) rsltItem.validate = newItem.validate; EntityPropMerge(rsltItem, 'roles', newItem, oldItem, function (newval, oldval) { return _.merge({}, oldval, newval); }); EntityPropMerge(rsltItem, 'controlparams', newItem, oldItem, function (newval, oldval) { return _.extend({}, oldval, newval); }); }); }); if(model.buttons && parentmodel.buttons){ if(_this.Config.system_settings.deprecated.disable_button_inheritance[model.module]){ //If button inheritance is disabled, remove all inherited buttons prior to parsing if(_.isArray(model.buttons)) model.buttons.unshift({ '__REMOVEALL__': true }); } else { //Check if child model has zero-length buttons if(!model.buttons.length){ _this.LogInit_WARNING('Inheriting model ' + model.id + ' has empty buttons array. Avoid empty button arrays in child models'); } } } //Create a clone of parent model instead of object reference if (('fields' in parentmodel) && !('fields' in model)) model.fields = parentmodel.fields.slice(0); EntityPropMerge(mergedprops, 'roles', model, parentmodel, function (newval, oldval) { return newval||oldval; }); EntityPropMerge(mergedprops, 'pagesettings', model, parentmodel, function (newval, oldval) { return _.merge({}, oldval, newval); }); EntityPropMerge(mergedprops, 'tabs', model, parentmodel, function (newval, oldval) { return _this.MergeModelArray(newval, oldval); }); EntityPropMerge(mergedprops, 'buttons', model, parentmodel, function (newval, oldval) { return _this.MergeModelArray(newval, oldval); }); EntityPropMerge(mergedprops, 'reportdata', model, parentmodel, function (newval, oldval) { return _.extend({}, oldval, newval); }); EntityPropMerge(mergedprops, 'ejs', model, parentmodel, function (newval, oldval) { return oldval + '\r\n' + newval; }); EntityPropMerge(mergedprops, 'header', model, parentmodel, function (newval, oldval) { return oldval + '\r\n' + newval; }); EntityPropMerge(mergedprops, 'js', model, parentmodel, function (newval, oldval) { return oldval + '\r\n' + newval; }); EntityPropMerge(mergedprops, 'jslib', model, parentmodel, function (newval, oldval) { return oldval + '\r\n' + newval; }); EntityPropMerge(mergedprops, 'css', model, parentmodel, function (newval, oldval) { return oldval + '\r\n' + newval; }); EntityPropMerge(mergedprops, 'fonts', model, parentmodel, function (newval, oldval) { return (oldval||[]).concat(newval||[]); }); //Merge Everything Else _this.Models[model.id] = _.extend({}, parentmodel, model); //Restore Merged Properties _.each(mergedprops, function (val, key) { _this.Models[model.id][key] = val; }); for (var prop in _this.Models[model.id]) { if (_this.Models[model.id][prop] == '__REMOVEPROPERTY__') { delete _this.Models[model.id][prop]; } } _.each(_this.Models[model.id].fields, function(field){ if(!field) return; for (var prop in field) { if (field[prop] == '__REMOVEPROPERTY__') { delete field[prop]; } } }); delete _this.Models[model.id].inherits; if(model.buttons && parentmodel.buttons){ if(!_this.Config.system_settings.deprecated.disable_button_inheritance[model.module]){ //Check if named buttons are used in both model and parent model if(_.filter(_this.Models[model.id].buttons, function(button){ return !button.name; }).length){ _this.LogInit_WARNING('Model ' + model.id + ' buttons may conflict with parent model '+parentmodel.id+' buttons. Use button.name on all buttons to prevent conflicts in inherited models'); } } } model = _this.Models[model.id]; } //Apply Transforms if(model._transforms){ while(model._transforms.length){ var transform = model._transforms.shift(); _this.ApplyModelTransform(model, transform); } delete _this.Models[model.id]._transforms; } }); } }; exports.ApplyModelTransform = function(model, transform){ var systemProperties = ['inherits','id','idmd5','_inherits','_transforms','_referencedby','_parentmodels','_parentbindings','_childbindings','_auto','_sysconfig','path','module','namespace','source_files_prefix','using','fields']; //Append "using" references _.each(transform.using, function(ref){ if(ref && !_.includes(model.using, ref)) model.using.push(ref); }); _.each(systemProperties, function(key){ if((key=='fields') && transform.fields && transform.fields.length) return; delete transform[key]; }); this.MergeModelTransform(model, transform); this.CleanTransform(model); }; exports.CleanTransform = function(obj){ if(!obj) return; if(_.isString(obj)) return; else if(_.isArray(obj)){ for(var i=0;i<obj.length;i++){ if(obj.__MATCH__){ obj.splice(i,1); i--; } else this.CleanTransform(obj[i]); } } else{ for(var key in obj){ if(obj[key]){ if(obj[key]=='__REMOVEPROPERTY__') delete obj[key]; else if(obj[key].__REMOVE__) delete obj[key]; else if(obj[key].__PREFIX__||obj[key].__SUFFIX__) obj[key] = (obj[key].__PREFIX__||'') + (obj[key].__SUFFIX__||''); else if(key=='__REPLACE__') delete obj[key]; else this.CleanTransform(obj[key]); } } } }; exports.TransformArrayMatch = function(arr, query, transform){ //Search through arr for query if(!query || !arr) return false; var _this = this; var found = false; var queryField = query.split(':'); if(query=='*') queryField = ['*','*']; else if(queryField.length != 2) _this.LogInit_ERROR('Invalid transform __MATCH__ expression: ' + query); for(var i=0;i<arr.length;i++){ var elem = arr[i]; //Check for match var match = false; if(query=='*') match = true; else if(_.isObject(elem)){ if(queryField[0] in elem){ if((queryField[1]=='*')||(queryField[1]===elem[queryField[0]])) match = true; } } if(match){ found = true; if(transform){ if(transform.__REMOVE__){ //Remove item arr.splice(i,1); i--; } else if(transform.__REPLACE__){ //Replace item arr[i] = JSON.parse(JSON.stringify(transform)); delete arr[i].__MATCH__; delete arr[i].__REPLACE__; } else if(!_.isObject(elem)){ arr[i] = JSON.parse(JSON.stringify(transform)); delete arr[i].__MATCH__; } else _this.MergeModelTransform(elem, transform); } } } return found; }; exports.MergeModelTransform = function(base, transform){ if(!base || !transform) return; var _this = this; for(var key in transform){ var bval = base[key]; var tval = transform[key]; if(key=='__MATCH__') continue; else if(tval=='__REMOVEPROPERTY__') delete base[key]; else if(_.isArray(tval)){ if(_.isArray(bval)){ //Array Transform for(var i=0;i<tval.length;i++){ var telem = tval[i]; if(_.isObject(telem)){ if(telem.__MATCH__){ //Apply match _this.TransformArrayMatch(bval, telem.__MATCH__, telem); tval.splice(i,1); i--; continue; } else if((key=='fields')&&telem.name){ //Apply based on name var found = _this.TransformArrayMatch(bval, 'name:'+telem.name, telem); if(found){ tval.splice(i,1); i--; continue; } } } //Add to array bval.push(telem); } } else base[key] = tval; //Re-sort array SortModelArray(base[key]); } else if(_.isObject(tval)){ //Object Transform if(tval['__REMOVE__']) delete base[key]; else if(tval['__REPLACE__']){ delete tval['__REPLACE__']; base[key] = tval; } else if(Helper.isNullUndefined(bval)) base[key] = tval; else if(_.isString(bval)){ //String Transform if(tval.__PREFIX__ || tval.__SUFFIX__){ if(!Helper.isNullUndefined(tval.__PREFIX__)) base[key] = tval.__PREFIX__.toString() + base[key]; if(!Helper.isNullUndefined(tval.__SUFFIX__)) base[key] = base[key] + tval.__SUFFIX__.toString(); } else base[key] = tval; } else if(_.isArray(bval)){ base[key] = tval; } else if(_.isObject(bval)){ _this.MergeModelTransform(bval, tval); } else base[key] = tval; } else base[key] = tval; } }; function EntityPropMerge(mergedprops, prop, model, parent, mergefunc) { if ((prop in model) && (prop in parent)) mergedprops[prop] = mergefunc(model[prop], parent[prop]); } exports.MergeModelArray = function(newval, oldval, eachItem){ var _this = this; if(!newval || _.isString(newval)) return newval; try{ var rslt = newval.slice(0); } catch(ex){ _this.Log.console(ex); _this.Log.console(newval); throw(ex); } var removeAll = false; for(let i=0;i<rslt.length;i++){ if(rslt[i]['__REMOVEALL__']){ for(let j=0; j <= i; j++) rslt[j]['__REMOVE__'] = true; removeAll = true; } } if(!removeAll) _.each(oldval, function (field) { if ((typeof field.name != 'undefined') && (field.name)) { var modelfield = _.find(rslt, function (mfield) { return mfield.name == field.name; }); } if (typeof modelfield !== 'undefined') { rslt.splice(rslt.indexOf(modelfield), 1); if (!('__REMOVE__' in modelfield)) { //oldfield = field, newfield = modelfield var newfield = _.merge({}, field, modelfield); if(eachItem) eachItem(modelfield, field, newfield); rslt.push(newfield); } } else { if (!('__REMOVE__' in field)) { rslt.push(field); } } }); SortModelArray(rslt); for (let i = 0; i < rslt.length; i++) { if ('__REMOVE__' in rslt[i]) { rslt.splice(i, 1); i--; } } return rslt; }; function SortModelArray(arr){ var cnt = 0; do { cnt = 0; for(let i = 0; i < arr.length; i++) { var elem = arr[i]; var newidx = -1; if(_.isObject(elem) && ('__AFTER__' in elem)){ //Get position of new index if(_.isInteger(elem['__AFTER__'])){ newidx = elem['__AFTER__'] + 1; if(newidx > arr.length) newidx = arr.length; //else if(newidx > i) newidx--; /* if moving forward, subtract 1 */ } else if(elem['__AFTER__']=='__START__') newidx = 0; else if(elem['__AFTER__']=='__END__') newidx = arr.length; else if(_.isString(elem['__AFTER__'])){ if(elem['__AFTER__'].indexOf(':')>=0){ var elemQuery = elem['__AFTER__'].split(':'); if(elemQuery.length == 2){ var qField = elemQuery[0]; var qVal = elemQuery[1]; for(let j = 0; j < arr.length; j++){ if(qField in arr[j]){ if((qVal=='*') || (arr[j][qField] === qVal)){ newidx = j + 1; //if(newidx > i) newidx--; /* if moving forward, subtract 1 */ break; } } } } } if(newidx == -1){ for(let j = 0; j < arr.length; j++){ if(arr[j].name == elem['__AFTER__']){ newidx = j + 1; //if(newidx > i) newidx--; /* if moving forward, subtract 1 */ break; } } } } if(newidx >= 0){ cnt++; delete elem['__AFTER__']; if(newidx != i){ arr.splice(i, 1); if(newidx > i) newidx--; arr.splice(newidx, 0, elem); if(newidx > i) i--; } } } } } while(cnt > 0); } exports.LogDeprecated = function(msg) { this.Statistics.Counts.InitDeprecated++; if (this.Config.debug_params.hide_deprecated) return; this.Log.console('**DEPRECATED** ' + msg); }; exports.TestImageExtension = function(cb){ var _this = this; if(!_this.Extensions.dependencies.image || !_this.Extensions.dependencies.image.length) return cb(); if(_this.Config.system_settings.ignore_image_extension) return cb(); var errMsg = 'Image controls were detected in this project: ' + _.uniq(_this.Extensions.dependencies.image).join(', ')+'\n'; errMsg += ' Please add support for a jsHarmony Image Extension:\n'; errMsg += ' jsharmony-image-sharp - Includes bundled DLLs for Node 10+\n'; errMsg += ' jsharmony-image-magick - Requires ImageMagick installation, has GIF resize support\n'; errMsg += ' To add a jsHarmony Image Extension:\n'; errMsg += ' 1. Install a jsHarmony Image Extension:\n'; errMsg += ' npm install jsharmony-image-sharp\n'; errMsg += ' 2. Add the extension to app.config.js / app.config.local.js:\n'; errMsg += " jsh.Extensions.image = require('jsharmony-image-sharp');"; if(!_this.Extensions.image){ return cb(errMsg); } _this.Extensions.image.init(function(err){ return cb(err && errMsg); }); }; exports.TestReportExtension = function(cb){ var _this = this; if(!_this.Extensions.dependencies.report || !_this.Extensions.dependencies.report.length) return cb(); if(_this.Config.system_settings.ignore_report_extension) return cb(); var errMsg = 'Report models were detected in this project: ' + _.uniq(_this.Extensions.dependencies.report).join(', ')+'\n'; errMsg += ' Please add support for a jsHarmony Report Extension:\n'; errMsg += ' 1. Install the jsHarmony Report Extension:\n'; errMsg += ' npm install jsharmony-report\n'; errMsg += ' 2. Add the extension to app.config.js / app.config.local.js:\n'; errMsg += " jsh.Extensions.report = require('jsharmony-report');"; if(!_this.Extensions.report) return cb(errMsg); _this.Extensions.report.init(function(err){ return cb(err && errMsg); }); }; exports.ParseDeprecated = function () { var _this = this; _.forOwn(this.Models, function (model) { //Convert tabs to indexed format, if necessary if(model.tabs){ if(!_.isArray(model.tabs)){ _this.LogDeprecated(model.id + ': Defining tabs as an associative array has been deprecated. Please convert to the indexed array syntax [{ "name": "TABNAME" }]'); var new_tabs = []; for (var tabname in model.tabs) { if(!model.tabs[tabname]) model.tabs[tabname] = { '__REMOVE__': 1 }; if(!model.tabs[tabname].name) model.tabs[tabname].name = tabname; new_tabs.push(model.tabs[tabname]); } model.tabs = new_tabs; } } if (model.duplicate && !_.isString(model.duplicate)){ if('link_text' in model.duplicate){ _this.LogDeprecated(model.id + ': model.duplicate.link_text has been deprecated. Please use model.duplicate.button_text instead.'); model.duplicate.button_text = model.duplicate.link_text; delete model.duplicate.link_text; } if('link' in model.duplicate){ _this.LogDeprecated(model.id + ': model.duplicate.link has been deprecated. Please use model.duplicate.link_on_success instead.'); model.duplicate.link_on_success = model.duplicate.link; delete model.duplicate.link; } } }); }; exports.ParseCustomControls = function () { var _this = this; var queries = _this.CustomControlQueries = {}; for(var controlname in _this.CustomControls){ var control = _this.CustomControls[controlname]; if(control.for){ if(!_.isArray(control.for)) control.for = [control.for]; for(let i=0;i<control.for.length;i++){ var expr = control.for[i]; if(_.isString(expr)) expr = { 'field': { 'name': expr } }; control.for[i] = expr; expr = JSON.parse(JSON.stringify(expr)); var fname = '*'; if(expr.field && expr.field.name){ fname = expr.field.name; delete expr.field.name; if(_.isEmpty(expr.field)) delete expr.field; } var exprstr = JSON.stringify(expr); if(_.isEmpty(expr)) expr = exprstr = '*'; if(!(fname in queries)) queries[fname] = {}; if(!(exprstr in queries[fname])) queries[fname][exprstr] = { expr: expr, controls: [] }; queries[fname][exprstr].controls.push(controlname); } } } }; exports.ApplyCustomControlQueries = function(model, field){ var _this = this; function QueryJSON(obj, expr){ if(obj===expr) return true; if(!obj) return false; if(!expr) return false; for(var elem in expr){ if(expr[elem]===obj[elem]) continue; if(_.isString(expr[elem])||_.isString(obj[elem])) return false; if(_.isArray(expr[elem])){ if(!_.isArray(obj[elem])) return false; if(expr[elem].length != obj[elem].length) return false; for(let i=0;i<expr[elem].length;i++) if(!QueryJSON(expr[elem][i],obj[elem][i])) return false; } else if(expr[elem] && obj[elem]){ if(!QueryJSON(expr[elem],obj[elem])) return false; } else return false; } return true; } function QueryControl(expr){ if(!expr) return false; if(expr=='*') return true; var rslt = true; if(expr.field){ rslt = rslt && QueryJSON(field, expr.field); } if(expr.model){ rslt = rslt && QueryJSON(model, expr.model); } return rslt; } function ApplyQuery(key, query){ var expr = query.expr; if((expr=='*')||QueryControl(expr)){ //Apply controls var controlnames = query.controls; _.each(controlnames, function(controlname){ _this.ApplyCustomControl(model, field, controlname); }); } } var queries = _this.CustomControlQueries; if(queries['*']){ for(let exprstr in queries['*']){ ApplyQuery('*',queries['*'][exprstr]); } } if(field.name && (field.name in queries)){ for(let exprstr in queries[field.name]){ ApplyQuery(field.name, queries[field.name][exprstr]); } } }; exports.ApplyCustomControl = function(model, field, controlname){ var _this = this; if(!controlname){ if(BASE_CONTROLS.indexOf(field.control) >= 0) return; if(!field.control) return; controlname = field.control; } if(!(controlname in _this.CustomControls)) throw new Error('Custom Control not defined: ' + field.control + ' in ' + model.id + ': ' + JSON.stringify(field)); var customcontrol = _this.CustomControls[controlname]; for (var prop in customcontrol) { if(prop=='for') continue; if(prop=='control') continue; //Apply Macro JS var val = customcontrol[prop]; if(_.isString(val) && (val.substr(0,8)=='jsmacro:')){ val = val.substr(8); try{ field[prop] = Helper.JSEval(val, field, {}); } catch(ex){ throw new Error('Error evaluating Custom Control jsmacro: ' + controlname + '.' + prop + ': ' + ex.toString() + '(' + model.id + ': ' + JSON.stringify(field) + ')'); } } else{ if (!(prop in field)){ field[prop] = val; } else if (prop == 'controlclass') field[prop] = field[prop] + ' ' + val; else if (prop == 'captionclass') field[prop] = field[prop] + ' ' + val; else { /* Do not apply */ } } } if('control' in customcontrol){ if (!('_orig_control' in field)) field['_orig_control'] = []; field._orig_control.push(field.control); field.control = customcontrol.control; } _this.ApplyCustomControl(model, field); }; exports.validateDisplayLayouts = function(model){ var _this = this; if(model.display_layouts){ var field_names = {}; _.each(model.fields, function(field){ field_names[field.name] = 1; }); for(var display_layout_name in model.display_layouts){ var display_layout = model.display_layouts[display_layout_name]; var column_names = {}; display_layout.columns = _.reduce(display_layout['columns'],function(rslt,column){ if(_.isString(column)){ let column_name = column; if(!(column_name in field_names)){ _this.LogInit_ERROR('Display layout column not found: '+model.id+'::'+display_layout_name+'::'+column_name); return rslt; } if(column_name in column_names){ _this.LogInit_ERROR('Duplicate display layout column: '+model.id+'::'+display_layout_name+'::'+column_name); return rslt; } rslt.push({name: column_name}); column_names[column_name] = 1; } else if(column){ let column_name = column.name; if(!column_name){ _this.LogInit_ERROR('Display layout column missing "name" property: '+model.id+'::'+display_layout_name+' '+JSON.stringify(column)); return rslt; } if(!(column_name in field_names)){ _this.LogInit_ERROR('Display layout column not found: '+model.id+'::'+display_layout_name+'::'+column_name); return rslt; } if(column_name in column_names){ _this.LogInit_ERROR('Duplicate display layout column: '+model.id+'::'+display_layout_name+'::'+column_name); return rslt; } rslt.push(column); column_names[column.name] = 1; } return rslt; },[]); } } }; exports.ParseEntities = function () { var _this = this; _this.ParseCustomControls(); var codegen = _this.codegen = new jsHarmonyCodeGen(_this); var auto_datatypes = _this.Config.system_settings.automatic_schema && _this.Config.system_settings.automatic_schema.datatypes; var auto_attributes = _this.Config.system_settings.automatic_schema && _this.Config.system_settings.automatic_schema.attributes; var auto_controls = _this.Config.system_settings.automatic_schema && _this.Config.system_settings.automatic_schema.controls; var auto_keys = _this.Config.system_settings.automatic_schema && _this.Config.system_settings.automatic_schema.keys; var validation_level = { }; switch(_this.Config.system_settings.validation_level){ case 'strict': validation_level.strict = 1; //falls through default: validation_level.standard = 1; } var modelsExt = {}; _.forOwn(this.Models, function (model) { //Remove null values for(var prop in model) if(model[prop]===null) delete model[prop]; if(model.fields) _.each(model.fields, function(field){ for(var prop in field) if(field[prop]===null) delete field[prop]; }); if(model.unbound && !('layout' in model)) model.layout = 'exec'; var modelExt = modelsExt[model.id] = { db: undefined, sqlext: undefined, tabledef: undefined, automodel: undefined, isReadOnlyGrid: undefined, }; var modelDB = 'default'; if('database' in model) model.db = model.database; if('db' in model){ if(!(model.db in _this.DBConfig)) _this.LogInit_ERROR('Model ' + model.id + ' uses an undefined db: '+model.db); else modelDB = model.db; model.database = model.db; } var db = modelExt.db = _this.DB[modelDB]; modelExt.sqlext = db.SQLExt; var moduleSchema = (model.module && _this.Modules[model.module] && _this.Modules[model.module].schema) || ''; var tabledef = modelExt.tabledef = db.getTableDefinition(model.table, moduleSchema); if(tabledef && tabledef.table_type){ model._dbdef = { table_type: tabledef.table_type , instead_of_insert: tabledef.instead_of_insert }; } if((model.layout=='grid') && !('commitlevel' in model)){ if(model.actions && !Helper.hasAction(model.actions, 'IUD')) model.commitlevel = 'none'; else if(tabledef && (tabledef.table_type=='view') && !('actions' in model)){ model.commitlevel = 'none'; } else model.commitlevel = 'auto'; } if (!('actions' in model)){ if((model.layout=='exec')||(model.layout=='report')||(model.layout=='multisel')) model.actions = 'BU'; else if(model.layout=='grid'){ if(model.grid_static) model.actions = 'B'; else if(!model.table && model.sqlselect){ model.actions = 'B'; if(model.sqlinsert) model.actions += 'I'; if(model.sqlupdate) model.actions += 'U'; if(model.sqldelete) model.actions += 'D'; } else if(!model.commitlevel || model.commitlevel=='none') model.actions = 'B'; else model.actions = 'BIUD'; } else{ if(model.unbound) model.actions = 'BU'; else if(!model.table){ model.actions = 'B'; if(model.sqlinsert) model.actions += 'I'; if(model.sqlupdate) model.actions += 'U'; if(model.sqldelete) model.actions += 'D'; } else model.actions = 'BIUD'; } } var isReadOnlyGrid = modelExt.isReadOnlyGrid = (model.layout=='grid') && (!model.commitlevel || (model.commitlevel=='none') || !Helper.hasAction(model.actions, 'IU')); if(tabledef){ var autolayout = ''; if((model.layout=='form') || (model.layout=='form-m') || (model.layout=='exec') || (model.layout=='report')) autolayout = 'form'; if(model.layout=='grid') autolayout = 'grid'; if(model.layout=='multisel') autolayout = 'multisel'; if(autolayout=='form'){ if(!tabledef.modelForm) codegen.generateModelFromTableDefition(tabledef,'form',{ db: model.db },function(err,messages,model){ tabledef.modelForm = model; }); modelExt.automodel = tabledef.modelForm; } else if((autolayout=='grid') && isReadOnlyGrid){ if(!tabledef.modelGridReadOnly) codegen.generateModelFromTableDefition(tabledef,'grid',{ db: model.db, readonly: true },function(err,messages,model){ tabledef.modelGridReadOnly = model; }); modelExt.automodel = tabledef.modelGridReadOnly; } else if((autolayout=='grid') && !isReadOnlyGrid){ if(!tabledef.modelGridEditable) codegen.generateModelFromTableDefition(tabledef,'grid',{ db: model.db },function(err,messages,model){ tabledef.modelGridEditable = model; }); modelExt.automodel = tabledef.modelGridEditable; } else if(autolayout=='multisel'){ if(!tabledef.modelMultisel) codegen.generateModelFromTableDefition(tabledef,'multisel',{ db: model.db },function(err,messages,model){ tabledef.modelMultisel = model; }); modelExt.automodel = tabledef.modelMultisel; } } model.xvalidate = new _this.XValidate(); if ('sites' in model) _this.LogInit_WARNING('Model ' + model.id + ' had previous "sites" attribute - overwritten by system value'); if(model.roles){ var roleids = _.keys(model.roles); //Resolve '*' roles for(let i=0;i<roleids.length;i++){ var role = roleids[i]; if(_.isString(model.roles[role])){ if(!('main' in model.roles)) model.roles['main'] = {}; model.roles['main'][role] = model.roles[role]; delete model.roles[role]; } else if(role=='*'){ for(var siteid in _this.Sites){ var newroles = JSON.parse(JSON.stringify(model.roles[role])); model.roles[siteid] = _.extend({},newroles,model.roles[siteid]); } } } } model.sites = Helper.GetRoleSites(model.roles); if ((model.layout != 'exec') && (model.layout != 'report') && !('table' in model) && !(model.unbound) && !model.sqlselect) _this.LogInit_WARNING('Model ' + model.id + ' missing table - use model.unbound property if this is intentional'); //Read-only grids should only have "B" actions if ((model.layout=='grid') && model.actions){ if(!model.commitlevel || (model.commitlevel=='none')){ if(Helper.hasAction(model.actions, 'IUD')){ _this.LogInit_ERROR('Model ' + model.id + ' actions should be "B" if it is a read-only grid and "commitlevel" is not set'); } } } //Add Model caption if not set var originalCaption = true; if (!('caption' in model)) { model.caption = ['', model.id, model.id]; originalCaption = false; if(!model.unbound && (model.layout != 'exec') && (model.layout != 'report') && validation_level.strict) _this.LogInit_WARNING('Model ' + model.id + ' missing caption'); } if(!model.caption) model.caption = ['','','']; else if(_.isString(model.caption) || !_.isArray(model.caption)) model.caption = ['',model.caption,model.caption]; else if(model.caption.length==1) model.caption = ['',model.caption[0],model.caption[0]]; else if(model.caption.length==2) model.caption = ['',model.caption[0],model.caption[1]]; model.class = Helper.escapeCSSClass(model.id); if(model.tabs && !('tabpos' in model)) model.tabpos = 'bottom'; if (!('title' in model)){ if(model.tabs && model.tabs.length && model.tabpos && (model.tabpos=='top')){ /* No action */ } else { if(!originalCaption && tabledef && tabledef.description && _this.Config.system_settings.automatic_schema && _this.Config.system_settings.automatic_schema.metadata_captions) model.title = tabledef.description; else if((model.layout == 'grid') || (model.layout == 'multisel')) model.title = model.caption[2]; else model.title = model.caption[1]; } } if(model.layout=='report'){ if(!('format' in model)) model.format = 'pdf'; if(!_.includes(['pdf','xlsx'], model.format)) _this.LogInit_ERROR(model.id + ': Unsupported report format: ' + model.format); if('onrender' in model) model.onrender = _this.createFunction(model.onrender, ['report', 'callback', 'require', 'jsh', 'modelid'], model.id+' model.onrender', model.path); if('onrendered' in model) model.onrendered = _this.createFunction(model.onrendered, ['report', 'callback', 'require', 'jsh', 'modelid'], model.id+' model.onrendered', model.path); } if('onsqlinserted' in model) model.onsqlinserted = _this.createFunction(model.onsqlinserted, ['callback', 'req', 'res', 'sql_params', 'sql_rslt', 'require', 'jsh', 'modelid'], model.id+' model.onsqlinserted', model.path); if('onsqlupdated' in model) model.onsqlupdated = _this.createFunction(model.onsqlupdated, ['callback', 'req', 'res', 'sql_params', 'sql_rslt', 'require', 'jsh', 'modelid'], model.id+' model.onsqlupdated', model.path); if('onsqldeleted' in model) model.onsqldeleted = _this.createFunction(model.onsqldeleted, ['callback', 'req', 'res', 'sql_params', 'sql_rslt', 'require', 'jsh', 'modelid'], model.id+' model.onsqldeleted', model.path); if (!('ejs' in model)) model.ejs = ''; if (!('header' in model)) model.header = ''; if (!('templates' in model)) model.templates = {}; if ('sort' in model) { if (model.sort && model.sort.length) { for (let i = 0; i < model.sort.length; i++) { if (!_.isString(model.sort[i])) { var j = 0; for (let f in model.sort[i]) { var sortval = ''; var dir = model.sort[i][f].toLowerCase(); if (dir == 'asc') sortval = '^' + f; else if (dir == 'desc') sortval = 'v' + f; else _this.LogInit_ERROR(model.id + ': Invalid sort direction for ' + f); model.sort.splice(i + 1 + j, 0, sortval); j++; } model.sort.splice(i, 1); } } } } //Auto-add primary key var foundkey = false; _.each(model.fields, function (field) { if(field.key) foundkey = true; }); if (!foundkey && (model.layout != 'exec') && (model.layout != 'report') && !model.unbound && !model.nokey){ if(auto_attributes && tabledef){ _.each(model.fields, function (field) { var fielddef = db.getFieldDefinition(model.table, field.name,tabledef); if(fielddef && fielddef.coldef && fielddef.coldef.primary_key){ field.key = 1; foundkey = true; model._auto.primary_key = 1; } }); } //Add Primary Key if Key is not Found if(!foundkey && auto_keys && tabledef){ _.each(tabledef.fields, function(fielddef){ if(fielddef.coldef && fielddef.coldef.primary_key){ addHiddenField(model, fielddef.name, { key: 1 }); foundkey = true; model._auto.primary_key = 1; } }); } if(!foundkey && !isReadOnlyGrid){ _this.LogInit_WARNING('Model ' + model.id + ' missing key - use model.unbound or model.nokey properties if this is intentional'); } } }); //Automatically add bindings _.forOwn(this.Models, function (model) { if(_this.Config.system_settings.automatic_bindings || auto_keys){ if(('nokey' in model) && (model.nokey)){ /* No action */ } else{ if ('tabs' in model) for (let i=0;i<model.tabs.length;i++) { var tab = model.tabs[i]; //tab.target, tab.bindings if(_this.Config.system_settings.automatic_bindings){ _this.AddAutomaticBindings(model, tab, 'Tab '+(tab.name||''), { modelsExt: modelsExt, noErrorOnMissingParentKey: true, log: function(msg){ _this.LogInit_ERROR(msg); } }); } _this.AddBindingFields(model, tab, 'Tab '+(tab.name||''), modelsExt); } if ('duplicate' in model) { //duplicate.target, duplicate,bindings if(_this.Config.system_settings.automatic_bindings){ _this.AddAutomaticBindings(model, model.duplicate, 'Duplicate action', { modelsExt: modelsExt, noErrorOnMissingParentKey: true, log: function(msg){ _this.Log