jsharmony
Version:
Rapid Application Development (RAD) Platform for Node.js Database Application Development
1,148 lines (1,086 loc) • 158 kB
JavaScript
/*
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