jsharmony
Version:
Rapid Application Development (RAD) Platform for Node.js Database Application Development
501 lines (462 loc) • 20.6 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 fs = require('fs');
var path = require('path');
var os = require('os');
/* eslint-disable quotes */
function jsHarmonyConfigBase(){
}
//Validate Configuration, if applicable
jsHarmonyConfigBase.prototype.Validate = function(jsh, desc){
if(!jsh) throw new Error('jsHarmony object required for validation');
if('_validProperties' in this){
var props = _.keys(this);
var diff = _.difference(props,this._validProperties,['_validProperties']);
if(diff && diff.length){
jsh.LogInit_ERROR('Invalid setting'+((diff.length>1)?'s':'')+(desc?' in '+desc:'')+' config: '+diff.join(', '));
}
}
return true;
};
//Initialize Configuration - Apply Default Values
jsHarmonyConfigBase.prototype.Init = function(cb){
if(cb) return cb();
};
//Merge target configuration with existing
jsHarmonyConfigBase.prototype.Merge = function(config, jsh, sourceModuleName, handlers){
if(!handlers) handlers = {};
for(var prop in config){
if(prop == '__TRANSFORM__') jsh.ApplyModelTransform(this, config[prop]);
else if(!(prop in this)) this[prop] = config[prop];
else if(prop in handlers) handlers[prop](this, config);
else if(_.isArray(this[prop]) && _.isArray(config[prop])){ for(var j = 0;j<config[prop].length;j++) this[prop].push(config[prop][j]); }
else if(_.isObject(this[prop])) _.merge(this[prop], config[prop]);
else this[prop] = config[prop];
}
};
/////////////////
//jsHarmonyConfig
/////////////////
function jsHarmonyConfig(config){
//Application Name
this.app_name = 'jsHarmony';
//Custom application settings
this.app_settings = { };
//Salt used in front-end algorithms
this.frontsalt = ''; //REQUIRED: Use a 60+ mixed character string
//Customer Service / Support email displayed to users
this.support_email = 'donotreply@company.com';
//Email where errors get sent
this.error_email = '';
//"From" email for auto-generated emails
this.mailer_email = 'DO NOT REPLY <donotreply@company.com>';
//Google API Settings
this.google_settings = {
api_key: '',
unauthenticated_access: false, //Send key to unauthenticated clients
};
//Debug Settings
this.debug_params = {
jsh_error_level: 3, //Bitmask: 1 = ERROR, 2 = WARNING, 4 = INFO, 8 = PERFORMANCE :: Messages generated while parsing jsHarmony configs
appsrv_requests: false, //Record all APPSRV requests on LOG/CONSOLE
pipe_log : true, //Show LOG messages on CONSOLE
hide_deprecated: false, //Hide deprecated property error messages
disable_email: false, //Disable sending outgoing emails
report_debug: false, //Display report warnings (Null value, etc.)
report_interactive: false, //Launch Report Viewer in Interactive Mode - Show Browser in Desktop
auth_debug: false, //Debug Login / Authentication - Log Hashes
delay_requests: 0, //Add a delay of this many milliseconds to all router requests (for testing latency)
debug_log_to_disk: false, //Log debug messages to disk
db_requests: false, //Log every database request through DB.js
db_raw_sql: false, //Log raw database SQL requests
db_log_level: 6, //Bitmask: 2 = WARNING, 4 = NOTICES :: Database messages logged to the console / log
db_error_sql_state: true, //Log SQL state during DB error
log_socket: true, //Enable DEV users to connect via WebSockets and read log
monitor_globals: false, //Enable client-side monitoring of global / window variables: display a message if a new variable is found
ignore_globals: [], //Ignore these global variables in the client-side global monitor
frontend_debug: false, //Display informational debug messages from front-end JS
dev_client_js: false, //Redirect /js/jsHarmony.js to /js/jsHarmony.dev.js for development
};
//Number of rows returned per grid load
this.default_rowlimit = 50;
//Maximum number of rows returned in export
this.export_rowlimit = 5000;
//Maximum file upload size (50MB)
this.max_filesize = 50000000;
//Maximum file storage in User Temp Folder (50MB)
this.max_user_temp_foldersize = 50000000;
//Seconds before Public Temp Folder files are deleted on next upload. Default is 5 minutes
this.public_temp_expiration = 5 * 60;
//Seconds before User Temp Folder files are deleted on next upload. Default is 6 hours
this.user_temp_expiration = 6 * 60 * 60;
//Timeout for report generation (default 90 seconds)
this.report_timeout = 90 * 1000;
//jsHarmony System Settings
this.system_settings = {
//Make datalock lookups case-insensitive
"case_insensitive_datalocks": true,
//Do not check for Image Extension on startup
"ignore_image_extension": false,
//Do not check for Report Extension on startup
"ignore_report_extension": false,
//Enable direct http access to encrypted content
"allow_insecure_http_encryption": false,
//Suffix override for jsHarmony cookie name - default is _PORT, to enable multiple applications on the same domain
"cookie_suffix": undefined,
//Automatically add bindings to models
"automatic_bindings": true,
//Automatically add datalocks to model fields
"automatic_datalocks": true,
//Automatically add parameters to AppSrv functions if they are in the querystring, and automatically define foreign keys
"automatic_parameters": true,
//Automatically look up and apply database schema - data types, required fields, primary keys, controls
"automatic_schema": { //Set "automatic_schema": false to disable any initial database schema lookup
"metadata_captions": true, //Use system meta data for field captions and model titles
"datatypes": true, //Load datatypes from the database (type, length, precision, required validation, primary key, read-only)
"attributes": true, //Load extended attributes from the database (required validation, primary key, read-only)
"controls": true, //Load controls from the database
"lovs": true, //Load LOVs (List of Values - code/code2) from the database
"keys": true //Generate primary and foreign keys based on table keys
},
//Deprecated Options Compatibility
"deprecated": {
"disable_button_inheritance": { }, //Disable button inheritance for each Module in the array. ex: { "application": true }
"disable_sqlwhere_on_form_update_delete": false, //Disable auto-adding sqlwhere conditions to update and delete SQL statements
},
//Model validation level - "standard", "strict"
// Strict: MISSING_CAPTION
"validation_level": "standard"
};
//Valid file upload extensions
this.valid_extensions = [".jpg", ".jpeg", ".pdf", ".png", ".svg", ".tif", ".tiff", ".gif", ".mp4", ".ogv", ".webm", ".mp3", ".wav", ".avi", ".txt", ".xlsm", ".xls", ".xlsx", ".bak", ".zip", ".csv"];
//Valid image extensions
this.supported_images = ['.jpg','.jpeg','.gif','.png','.svg'];
//File viewers
this.file_viewers = { /* '.ext': function(jsh, req, res, options, fpath, fname){ ... } */ };
//Time before log entries are flushed to disk
this.LogSleepDelay = 1000;
//Application base path (containing models folder)
this.appbasepath = '';
//Data folder path (ending in /)
this.datadir = '';
//Log folder path (ending in /)
this.logdir = '';
//Folder containing local application models
this.localmodeldir = '';
//jsHarmony module path
this.moduledir = path.dirname(module.filename);
//Whether or not to display sample data instead of application server data
this.use_sample_data = 0;
//Show a message if the user does not have an HTML5 browser
this.require_html5_after_login = true;
//Whether or not the system is run in interactive (CLI) mode
this.interactive = false;
//Server configuration
this.server = {
http_port: 0, //HTTP Port
request_timeout: 2*60*1000, //Web request timeout
add_default_routes: true //Add default Express routes on init
//http_ip: '0.0.0.0', //HTTP IP
//https_ip: '0.0.0.0', //HTTPS IP
//https_port: 0, //HTTPS Port
//https_cert: '', //Path to https-cert.pem
//https_key: '', //Path to https-key.pem
//https_ca: '', //Path to https-ca.pem
};
if(process.env.PORT) this.server.http_port = process.env.PORT;
if(process.env.TLSPORT) this.server.https_port = process.env.TLSPORT;
//AppSrv Field Mapping
this.field_mapping = {
"user_id": "user_id",
"user_hash": "user_hash",
"user_status": "user_status",
"user_email": "user_email",
"user_name": "user_name",
"user_firstname": "user_firstname",
"user_lastname": "user_lastname",
"user_last_ip": "user_last_ip",
"user_last_tstmp": "user_last_tstmp",
"user_role": "user_role",
"rowcount": "xrowcount",
"code_val": "code_val",
"code_val1": "code_val1",
"code_val2": "code_val2",
"code_txt": "code_txt",
"code_seq": "code_seq",
"code_end_date": "code_end_date",
"code_parent": "code_parent",
"timestamp": "timestamp",
"current_user": "current_user",
"code_sys": "code_sys",
"code_app": "code_app",
"code2_sys": "code2_sys",
"code2_app": "code2_app",
"code": "code",
"code2": "code2"
};
//UI Field Mapping
this.ui_field_mapping = {
"code_val": "code_val",
"code_txt": "code_txt",
"code_parent_id": "code_parent_id",
"code_icon": "code_icon",
"code_id": "code_id",
"code_parent": "code_parent",
"code_seq": "code_seq",
"code_type": "code_type"
};
//DB Schema Replacement
this.schema_replacement = [];
//Remote Queues
this.queues = {}; //queueid: { "actions": "BIUD", "roles": {"SYSADMIN":"*"} },
//Model Macros
this.macros = {};
//Dynamic Model Bindings
this.dynamic_bindings = {};
//Default Button Definitions
this.default_buttons = {
"insert": { "icon": "insert", "text": "Add %%%CAPTION%%%", "class": "xbuttoninsert" },
"update": { "icon": "update", "text": "Edit %%%CAPTION%%%" },
"browse": { "icon": "browse", "text": "View %%%CAPTION%%%" }
};
//Model Groups for Dynamic Bindings
this.model_groups = {};
//Datalock Definitions
this.datalocks = {};
//Datalock Option Definitions
this.datalock_options = {};
//Field Encryption Salts
this.salts = {};
//Field Encryption Passwords
this.passwords = {};
//When jsHarmony server is started
this.onServerReady = []; //function(cb, servers){ return cb(); }
//When jsHarmony config is loaded
this.onConfigLoaded = []; //function(cb, jsh){ return cb(); }
//When the database drivers are loaded, before the schema is read
this.onDBDriverLoaded = []; //function(cb, jsh){ return cb(); }
//When a client tries to connect to a jsHarmony queue
this.onQueueSubscribe = []; //function(cb, req, res, queueid){ return cb(); }
//Overriding the database context
this.onGetDBContext = null; //function(jsh, req, model, db){ return 'system'; }
//Theme CSS files
this.themes = {
'light': [ 'jsHarmony.theme.light.css' ],
'classic': [ 'jsHarmony.theme.classic.css' ]
};
this.theme = 'light';
//Additional CSS files for jsHarmony.css
this.css_extensions = [
path.dirname(module.filename) + '/public/jquery-ui/css/jquery-ui-1.10.3.custom.min.css',
path.dirname(module.filename) + '/public/js/colorbox/colorbox.css',
];
//Additional JS files for jsHarmony.js
this.js_extensions = [];
//Default report fonts
this.default_report_fonts = [
{
"font-family": "Roboto",
"font-style": "normal",
"font-weight": 400,
"src": "jsharmony/public/fonts/Roboto-Regular.ttf",
"format": "truetype", //embedded-opentype, woff2, woff, truetype, svg
"css": "body { font-family: 'Roboto'; }"
},
{
"font-family": "Roboto",
"font-style": "bold",
"font-weight": 700,
"src": "jsharmony/public/fonts/Roboto-Bold.ttf",
"format": "truetype" //embedded-opentype, woff2, woff, truetype, svg
}
];
//Load jsHarmony in Silent Mode (without the standard console messages)
this.silentStart = false;
//Load models on startup (set to false for database / initialization scripting)
this.loadModels = true;
//Mailer settings
this.mailer_settings = null; /*{
//SMTP
type: 'smtp',
host: 'mail.company.com',
port: 465,
auth: {
user: 'donotreply@company.com',
pass: ''
},
secure: true,
debug: false,
tls: { rejectUnauthorized: false },
maxConnections: 5, //Max parallel SMTP connections
maxMessages: 10 //Messages sent per SMTP connection
//Amazon SES
//type: 'ses',
//accessKeyId: "xxx",
//secretAccessKey: "xxx",
//rateLimit: 10 // Messages per second
};*/
//Modules
this.modules = {};
//DB Specific Configuration
this.forDB = {}; //{ pgsql: [ {config1}, {config2} ] }
this._validProperties = _.keys(this);
if(config) this.Merge(config);
}
jsHarmonyConfig.prototype = new jsHarmonyConfigBase();
jsHarmonyConfig.prototype.Init = function(cb){
if(!this.appbasepath) this.appbasepath = path.dirname(require.main.filename);
if(!this.datadir) this.datadir = this.appbasepath + '/data/';
if(!this.logdir) this.logdir = this.datadir + 'log/';
if(!this.localmodeldir) this.localmodeldir = this.appbasepath + '/models/';
if(cb) return cb();
};
jsHarmonyConfig.prototype.Merge = function(config, jsh, sourceModuleName){
if(config){
if(!sourceModuleName && config.sourceModuleName) sourceModuleName = config.sourceModuleName;
for(var prop in config){
//Handle modules
if(prop=='sourceModuleName') continue;
else if(prop=='modules'){
for(var moduleName in config.modules){
if((moduleName in this.modules) && (this.modules[moduleName].Merge)){
this.modules[moduleName].Merge(config.modules[moduleName], jsh, sourceModuleName);
}
else {
_.merge(this.modules[moduleName], config.modules[moduleName]);
}
}
}
//Merge arrays
else if(_.includes(['schema_replacement','css_extensions','js_extensions'],prop)) this[prop] = this[prop].concat(config[prop]);
//Replace objects
else if(_.includes(['valid_extensions','supported_images','server'],prop)) this[prop] = config[prop];
//Merge first level objects
else if(_.includes(['default_buttons', 'field_mapping', 'ui_field_mapping', 'salts', 'passwords', 'macros', 'model_groups', 'dynamic_bindings'],prop)){
for (var elem in config[prop]) this[prop][elem] = config[prop][elem];
}
//Fully merge any other objects
else if(_.isObject(this[prop])) _.merge(this[prop], config[prop]);
//Replace any other values
else this[prop] = config[prop];
}
}
};
jsHarmonyConfig.prototype.LoadJSConfigFile = function(jsh, fpath){
if(!fpath) throw new Error('Config file path is required');
if (!fs.existsSync(fpath)) return;
try{
var transformConfig = require(fpath);
transformConfig(jsh, jsh.Config, jsh.DBConfig);
}
catch(ex){
jsh.LogInit_ERROR('Error loading config file: '+fpath + ', '+ex.toString()+' '+ex.stack);
process.exit(1);
}
};
jsHarmonyConfig.prototype.LoadJSConfigFolder = function(jsh, fpath){
//Include appropriate config file based on Path
if(!fpath) fpath = jsh.Config.appbasepath;
if(!fpath) fpath = path.dirname(require.main.filename);
//Create array of application path
var fbasepath = fpath;
var fbasename = '';
var patharr = [];
while ((fbasename = path.basename(fbasepath))) {
patharr.unshift(fbasename);
fbasepath = path.dirname(fbasepath);
}
var configFiles = [];
//app.config.js
configFiles.push(fpath + '/app.config.js');
//Localized config
configFiles.push(fpath + '/app.config.local.js');
//Config based on Application Path
if(patharr.length) configFiles.push(fpath + '/app.config.' + patharr.join('_') + '.js');
//Config based on Hostname
configFiles.push(fpath + '/app.config.' + os.hostname().toLowerCase() + '.js');
//Enable client to reorder / add config files
if(jsh.onLoadingConfig) jsh.onLoadingConfig(configFiles);
//Load config files
for(var i=0;i<configFiles.length;i++) this.LoadJSConfigFile(jsh, configFiles[i]);
};
jsHarmonyConfig.prototype.LoadJSONConfigFile = function(jsh, fpath, sourceModule, dbDriver){
if(!fpath) throw new Error('Config file path is required');
if (!fs.existsSync(fpath)) return;
var config = jsh.ParseJSON(fpath, (sourceModule ? sourceModule.name : undefined), "Config");
//Add namespace to model names
if(config && sourceModule){
if(config.model_groups){
for (var model_group in config.model_groups){
var model_group_members = config.model_groups[model_group];
for(var i=0;i<model_group_members.length;i++){
model_group_members[i] = jsHarmonyConfig.addNamespace(model_group_members[i], sourceModule);
}
}
}
}
//Merge or delay-merge config
if(dbDriver){
//Add to database-specific config
if(!(dbDriver in this.forDB)) this.forDB[dbDriver] = [];
if(sourceModule) config.sourceModuleName = sourceModule.name;
this.forDB[dbDriver].push(config);
}
else {
//Merge config
this.Merge(config, jsh, (sourceModule?sourceModule.name:undefined));
}
};
jsHarmonyConfig.prototype.LoadJSONConfigFolder = function(jsh, fpath, sourceModule){
var _this = this;
//Include appropriate config file based on Path
if(!fpath && jsh.Config.appbasepath) fpath = path.join(jsh.Config.appbasepath, 'models');
if(!fpath) fpath = path.join(path.dirname(require.main.filename), 'models');
else if(fpath.substr(fpath.length-1,1)=='/') fpath = fpath.substr(0,fpath.length-1);
//Create array of application path
var fbasepath = fpath;
var fbasename = '';
var patharr = [];
while ((fbasename = path.basename(fbasepath))) {
patharr.unshift(fbasename);
fbasepath = path.dirname(fbasepath);
}
//Load app.config.js
this.LoadJSONConfigFile(jsh, fpath + '/_config.json', sourceModule);
//Load app.config.local.js
this.LoadJSONConfigFile(jsh, fpath + '/_config.local.json', sourceModule);
//Load config based on Application Path
this.LoadJSONConfigFile(jsh, fpath + '/_config.' + patharr.join('_') + '.json', sourceModule);
//Load config based on Hostname
this.LoadJSONConfigFile(jsh, fpath + '/_config.' + os.hostname().toLowerCase() + '.json', sourceModule);
//Load config based on Default Database Driver
var dbDrivers = jsh.getDBDrivers();
_.each(dbDrivers, function(dbDriver){
_this.LoadJSONConfigFile(jsh, fpath + '/_config.' + dbDriver + '.json', sourceModule, dbDriver);
});
if(jsh.DBConfig['default'] && jsh.DBConfig['default']._driver){
var defaultDBDriver = jsh.DBConfig['default']._driver.name;
this.LoadJSONConfigFile(jsh, fpath + '/_config.' + defaultDBDriver + '.json', sourceModule);
}
};
jsHarmonyConfig.Base = jsHarmonyConfigBase;
//Add namespace where applicable
jsHarmonyConfig.addNamespace = function(modelid, sourceModule){
if(!sourceModule) return modelid;
if(!modelid) return modelid;
if(modelid[0]=='/') return modelid;
return sourceModule.namespace + modelid;
};
exports = module.exports = jsHarmonyConfig;