"use strict";

var mongoConnectorVersionStr = "6.2.0.26011409";
var mongoConnectorVersionNum = 62000.26011409;

const cluster = require('cluster');
const os = require('os');

// Setup the enviroment then open/read the configuration file parameters
process.env.NODE_ENV = process.env.NODE_CONFIG_ENV ? process.env.NODE_CONFIG_ENV : "appxconnector";
process.env.NODE_CONFIG_DIR = __dirname + "/";
var config = require('config');
global.appConfig = config.get('mongoConnectorConfig');

const connectorPort    = process.env.APPX_MONGO_CONNECTOR_PORT; // Port the appxConnector listens on for client connections
const workers          = os.cpus().length;                      // Number of worker processes spawned to listen for incoming connections

//  *********************************************************
//  Configuration Section - Begin
//  *********************************************************

const sslEnabled       = appConfig.sslEnabled;                 	// Are we using SSL for our connections?
const sslPrivateKey    = appConfig.sslPrivateKey;				// SSL Privte Key file is using SSL
const sslCertificate   = appConfig.sslCertificate;  		    // SSL Certificate file if using SSL
const sslCertAuthority = appConfig.sslCertAuthority;    		// SSL Certificate Authority file if using SSL
const sslPfxFile	   = appConfig.sslPfxFile;					// SSL Pfx type Certificate file
const sslPfxPassphrase = appConfig.sslPfxPassphrase;			// The password for an encrypted Pfx type Certificate file

const mongoDatabase    = appConfig.mongoDatabase;               // The name of the database in Mongo we use to cache all of our data
const mongoPrefs       = appConfig.mongoPrefs;                  // The name of the database in Mongo we use to store user prefs
const mongoHost        = appConfig.mongoHost;                   // The hostname of the server mongo is running on
const mongoPort        = appConfig.mongoPort;                   // The port number on that server that mongo is listening on
const mongoLocale      = appConfig.mongoLocale;
const appxdebug        = appConfig.appxdebug;

//  *********************************************************
//  Configuration Section - End
//  *********************************************************

function dlog( msg, obj ) {
    if( appxdebug ) {
		var nowStr = new Date(Date.now()).toISOString();
		if( obj ) {
			console.log( nowStr + " - " + msg + "..." );
			console.dir(obj);
		} else {
			console.log( nowStr + " - " + msg );
		}
    }
}

function dlogForce( msg, obj ) {
    if( true ) {
		var nowStr = new Date(Date.now()).toISOString();
		if( obj ) {
			console.log( nowStr + " - " + msg + "..." );
			console.dir(obj);
		} else {
			console.log( nowStr + " - " + msg );
		}
    }
}

if (process.argv.length > 2) {
    process.chdir(process.argv[2]);
} else if (process.env.APPX_CONNECTOR_DIR) {
    process.chdir(process.env.APPX_CONNECTOR_DIR);
}

if (workers < 1) {
    dlog("appxMongoConnector Master process running, pid: " + process.pid);
    masterCode();
    workerCode();
} else {
    if (cluster.isMaster) { //cluster.isMaster was deprecated in v16.0.0, should be changed to cluster.isPrimary
        masterCode();
    } else {
        dlog("appxMongoConnector Worker process started, id: " + cluster.worker.id + ", pid: " + process.pid);
        process.title = "node appxMongoConnector.js worker #" + cluster.worker.id;
        workerCode();
    }
}

function masterCode() {
    //  *********************************************************
    //  Master process code
    //  *********************************************************

    if (workers > 0) {
        dlog("appxMongoConnector Master process running, pid: " + process.pid);
        dlog(`Creating ${workers} appxMongoConnector worker processes`);

        for (let i = 0; i < workers; i++) {
            cluster.fork();
        }
    }
}

// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
async function workerCode() {

    /************************************************************
    Library Imports - Begin
    *************************************************************/

    var GridFSBucket = require('mongodb-legacy').GridFSBucket;
    var MongoClient = require('mongodb-legacy').MongoClient;
    var fs = require('fs');
    var { Readable } = require('stream');

    /************************************************************
        Library Imports - End
    *************************************************************/

    // Global variables and Objects

    var mongoUrl = 'mongodb://' + mongoHost + ':' + mongoPort + '/?maxpoolSize=20';
	var mongoOptions = {
		 appName: 'appxMongoConnector',
		 retryReads: true,
		 retryWrites: true
	};
    var mongoCacheDb = null;
    var mongoUserPrefsDb = null;
    var mongoStatus = "Running";

	// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
	var client = new MongoClient(mongoUrl, mongoOptions);
	try {
		await client.connect();
        mongoCacheDb = client.db(mongoDatabase);
		if (await mongoCacheDb.dropDatabase({})) {
			mongoCacheDb = client.db(mongoDatabase);
		}
        mongoUserPrefsDb = client.db(mongoPrefs);
		if (await mongoUserPrefsDb.dropDatabase({})) {
			mongoUserPrefsDb = client.db(mongoPrefs);
		}
		/*  Start - The commented code below is not used and will be removed in the future, by 12/31/2025 */
		/*
		// Run database cleanup every 5 minutes
        setInterval(databaseCleanup, 300000);
		*/
		/*  End - The commented code below is not used and will be removed in the future, by 12/31/2025 */

        createWebSocket();

	} catch (e) {
        dlog("Error during connection: " + e);
		mongoStatus = "Error";
	}

    var buildMongoSelection = function buildMongoSelection(gridRequest) {
        var selection = [];
        var result = {};
        if (gridRequest["_search"] == "true") {
            var filters = JSON.parse(gridRequest.filters);
            var groupOp = "$" + filters.groupOp.toLowerCase();
            var rules = filters.rules;

            for (var i = 0; i < rules.length; i++) {
                var rule = rules[i];
                var field = rule.field;
                var op = rule.op;
                var data = rule.data;
                var mongo = {};
                var datatype = field.slice(-1);

                /*Change search fields back to correct fields, not sorting fields*/
                var colModelPosition = parseInt(field.substring(field.length - 2, field.length - 1)) - 3;
                var gridRequestPropertyIndex = "colModel[" + colModelPosition + "][index]";
                var gridRequestPropertyName = "colModel[" + colModelPosition + "][name]";

                if (field === gridRequest[gridRequestPropertyIndex]) {
                    field = gridRequest[gridRequestPropertyName];
                }

                if (datatype === "D") {
                    for (var key in gridRequest) {
                        if (gridRequest[key] === field && key.indexOf("index") !== -1) {
                            var tempKey = key.replace(/index/, "name");
                            if (gridRequest[tempKey] !== field) {
                                field = gridRequest[tempKey];
                            }
                        }
                    }
                }

                switch (datatype) {
                    case "I":
                        data = parseInt(data);
                        break;
                    case "F":
                        data = parseFloat(data);
                        break;
                    case "B":
						var tmpdata = data.toLowerCase();
						if( tmpdata == "y" || tmpdata == "1" || tmpdata == "t" || tmpdata == "true" || tmpdata == "yes" ) {
							data = "Y";
						} else {
							data = "N";
						}
						break;
                }

                mongo[field] = {};

                /* Test for uppercase letters in search string. If search string
                 ** contains uppercase then we do case sensitive search. Otherwise
                 ** searches are case insensitive.*/
                if (/[A-Z]/.test(data) || !isNaN(data)) {
                    switch (op) {
                        case 'eq':
                            mongo[field]["$eq"] = data;
                            break;
                        case 'ne':
                            mongo[field]["$ne"] = data;
                            break;
                        case 'lt':
                            mongo[field]["$lt"] = data;
                            break;
                        case 'gt':
                            mongo[field]["$gt"] = data;
                            break;
                        case 'le':
                            mongo[field]["$lte"] = data;
                            break;
                        case 'ge':
                            mongo[field]["$gte"] = data;
                            break;
                        case 'nu':
                            mongo[field]["$eq"] = " ";
                            break;
                        case 'nn':
                            mongo[field]["$ne"] = " ";
                            break;
                        case 'bw':
                            mongo[field]["$regex"] = "^" + data;
                            break;
                        case 'ew':
                            mongo[field]["$regex"] = data + "$";
                            break;
                        case 'cn':
                            mongo[field]["$regex"] = data;
                            break;
                        case 'bn':
                            mongo[field]["$regex"] = "^(?!" + data + ").*";
                            break;
                        case 'en':
                            mongo[field]["$regex"] = ".*(?<!" + data + ")$";
                            break;
                        case 'nc':
                            mongo[field]["$regex"] = "^((?!" + data + ").)*$";
                            break;
                    }
                } else {
                    switch (op) {
                        case 'eq':
                            mongo[field]["$regex"] = "^(" + data + ")$";
                            break;
                        case 'ne':
                            mongo[field]["$regex"] = "^(?!" + data + ")$";
                            break;
                        case 'lt':
                            mongo[field]["$lt"] = data;
                            break;
                        case 'gt':
                            mongo[field]["$gt"] = data;
                            break;
                        case 'le':
                            mongo[field]["$lte"] = data;
                            break;
                        case 'ge':
                            mongo[field]["$gte"] = data;
                            break;
                        case 'nu':
                            mongo[field]["$eq"] = " ";
                            break;
                        case 'nn':
                            mongo[field]["$ne"] = " ";
                            break;
                        case 'bw':
                            mongo[field]["$regex"] = "^" + data;
                            break;
                        case 'ew':
                            mongo[field]["$regex"] = data + "$";
                            break;
                        case 'cn':
                            mongo[field]["$regex"] = data;
                            break;
                        case 'bn':
                            mongo[field]["$regex"] = "^(?!" + data + ").*";
                            break;
                        case 'en':
                            mongo[field]["$regex"] = ".*(?<!" + data + ")$";
                            break;
                        case 'nc':
                            mongo[field]["$regex"] = "^((?!" + data + ").)*$";
                            break;
                    }
                }

                if (mongo[field].hasOwnProperty("$regex")) {
                    mongo[field]["$options"] = "i";
                }

                selection.push(mongo);
            }

            result[groupOp] = selection;
        }

        return result;
    }

    var buildMongoSort = function buildMongoSort(gridRequest) {
        var sorter = {};
        var ord = 1;

        if (gridRequest.sidx && gridRequest.sidx.length > 0) {
            if (gridRequest.sord && gridRequest.sord == 'asc')
                sorter[gridRequest.sidx] = 1;
            else {
                sorter[gridRequest.sidx] = -1;
                ord = -1;
            }
        }

        /*If table was previously sorted we use that as a secondary sort*/
        if (Object.hasOwnProperty.call(gridRequest, "lastSortName[]") && gridRequest["lastSortName[]"].length >= 1) {
            for (var i = gridRequest["lastSortName[]"].length - 1; i >= 0; i--) {
                if (gridRequest["lastSortName[]"][i] !== gridRequest.sidx) {
                    if (gridRequest["lastSortOrder[]"] === undefined || gridRequest["lastSortOrder[]"][i] === "asc") {
                        sorter[gridRequest["lastSortName[]"][i]] = 1;
                    } else {
                        sorter[gridRequest["lastSortName[]"][i]] = -1;
                    }
                }
            }
        } else {
            sorter["initialSort"] = ord;
        }

        return sorter;
    }

	// Bug #5178 - Updated to use async/await to support node mongoddb drivers => v4.x because callbacks for collections have been removed
    var fetchtabledata = async function fetchtabledata(message, res) {
        // This code is full of nested callbacks.  We have to open the database, then
        // in that callback we have to open the collection, then inside that callback we have to
        // process the count function, then in that callback we get the requested records, and inside
        // that callback we send the records to the client.  Whew, that's a lot of callbacks.

        // We are in the OPEN callback
        try{
			var collection = mongoCacheDb.collection(message.collection);

			// We are in the GET COLLECTION callback
			var selection = buildMongoSelection(message);
			var sorter = buildMongoSort(message);
			var collist = {};
			var collistId2 = { id2: 1 };
			var caseSort = message.caseSort == "true";
			var collationOptions =  { "locale": mongoLocale };

			if( caseSort == true ) {
				collationOptions["locale"] = "simple";
			}

			if (collection !== undefined) {
				if (message.collist) {
					collist = JSON.parse(message.collist);
				}

				// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
				await collection.createIndex(sorter, { collation: collationOptions });

				var items = await collection.find( { $and: [ {"datalookupid": message.datalookupid}, selection]}, collistId2)
											.collation(collationOptions)
											.sort(sorter)
											.toArray();

				var totalcount = (items === undefined || items == null) ? 0 : items.length;
				var selectedRow = -1;
				if( message.findId2 && totalcount > 0 ) {
					for( var k = 0; k < totalcount; k++ ) {
						if( message.findId2 === items[k].id2 ) {
							selectedRow = k + 1;
							break;
						}
					}
				}

				// We are in the COUNT results callback
				items = await collection.find({ $and: [{ "datalookupid": message.datalookupid}, selection]}, collist)
										.collation(collationOptions)
										.sort(sorter)
										.skip((parseInt(message.page) - 1) * parseInt(message.rows))
										.limit(parseInt(message.rows))
										.toArray();

				// We are in the GET RECORDS callback
				var rowtmp = [];
				var result = {};

				result.records = parseInt(totalcount);
				result.page = parseInt(message.page);
				result.total = Math.ceil(parseInt(totalcount) / parseInt(message.rows));
				if( message.findId2 ) {
					result.findId2RowNo = selectedRow;
				}
				if (items) {
					for (var i = 0; i < items.length; i++) {
						rowtmp.push(items[i]);
					}
				}

				result["rows"] = rowtmp;
				res.end(JSON.stringify(result).replace(/[<][/]?p[>]/gi,""));
			}

        } catch (e) {
            dlog("ERROR: connecting to db");
            dlog(e);
            dlog(e.stack);
        }
    };

/*  Start - The commented code below is dead and will be removed in the future, by 12/31/2025 */
/*
    var fetchtabledataOld = function fetchtabledataOld(message, res) {
        // This code is full of nested callbacks.  We have to open the database, then
        // in that callback we have to open the collection, then inside that callback we have to
        // process the count function, then in that callback we get the requested records, and inside
        // that callback we send the records to the client.  Whew, that's a lot of callbacks.

        // We are in the OPEN callback
        try {
            mongoCacheDb.collection(message.collection, function mongoCacheDb_collectionCallback(err, collection) {
                // We are in the GET COLLECTION callback
                var selection = buildMongoSelection(message);
                var sorter = buildMongoSort(message);
                var collist = {};

                if (collection !== undefined) {
                    if (message.collist) {
                        collist = JSON.parse(message.collist);
                    }

                    collection.createIndex(sorter);

                    collection.find({
                        $and: [{
                            "datalookupid": message.datalookupid
                        }, selection]
                    }).count(function mongoCountCallback(err, totalcount) {
                        // We are in the COUNT results callback
                        collection.find({
                            $and: [{
                                "datalookupid": message.datalookupid
                            }, selection]
                        }, collist).sort(sorter).skip((parseInt(message.page) - 1) * parseInt(message.rows)).limit(parseInt(message.rows)).toArray(function mongoFindCallback(err, items) {
                            // We are in the GET RECORDS callback
                            var rowtmp = [];
                            var result = {};

                            result.records = parseInt(totalcount);
                            result.page = parseInt(message.page);
                            result.total = Math.ceil(parseInt(totalcount) / parseInt(message.rows));

                            if (items) {
                                for (var i = 0; i < items.length; i++) {
                                    rowtmp.push(items[i]);
                                }
                            }

                            result["rows"] = rowtmp;
                            res.end(JSON.stringify(result));
			    dlog(Date.now() + " - Sent grid date reply <<<<<<<");
                        });
                    });
                } else {
                    dlog("Error getting table data: " + err);
                }
            });
        } catch (e) {
            dlog("ERROR: connecting to db");
            dlog(e);
            dlog(e.stack);
        }
    };
*/
/*  End - The commented code above is dead and will be removed in the future, by 12/31/2025 */

	// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
    var getuserprefs = async function getuserprefs(message, res) {
		try {
			var myQuery = { _id: message.prefKey };
			var result = await mongoUserPrefsDb.collection(message.prefType).findOne(myQuery);
			if( result ) {
				var reply = { result: "ok", prefData: result.prefData }
				res.end(JSON.stringify(reply));

			} else {
				var reply = { result: "ok", prefData: "{}" }
				res.end(JSON.stringify(reply));
			}

		} catch (ex) {
			dlog("getUserPrefs() find failed, err = " + ex, message);
            var reply = { result: "failed" }
            res.end(JSON.stringify(reply));
		}
	}

	// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
    var setuserprefs = async function setuserprefs(message, res) {
		try {
			var myQuery = { _id: message.prefKey };
			await mongoUserPrefsDb.collection(message.prefType).deleteOne(myQuery);

			if( message.prefData !== '{}' ) {
				var myQuery = { _id: message.prefKey, prefData: message.prefData };
				await mongoUserPrefsDb.collection(message.prefType).insertOne(myQuery);

			} else {
				var reply = { result: "ok" }
				res.end(JSON.stringify(reply));
			}

		}  catch (ex) {
			// Bug #5189 - Code generated/modified by AI tool to fix any issues that it found
			dlog("setUserPrefs() delete failed, err = " + ex, message);
		}
	}

	// Bug #5189 - Code generated/modified by AI tool to fix any issues that it found
	// Bug #5178 - Updated to use async/await to support node mongoddb drivers => v4.x because callbacks for collections have been removed
    var fetchtabledata_findrow = async function fetchtabledata_findrow(message, res) {
		try {
			var collection = mongoCacheDb.collection(message.collection);
			var selection = buildMongoSelection(message);
			var sorter = buildMongoSort(message);
			var collist = { "_id": false, "id2": true };

			// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
			var items = await collection.find({ $and: [{"datalookupid": message.datalookupid}, selection] }, { projection: collist })
				.sort(sorter)
				.toArray();

			// Process the records to find the matching row
			var result = {};
			var rowNo = -1;

			for( var j = 0; j < items.length; j++ ) {
				if( items[j].id2 === message.findId2 ) {
					rowNo = j;
					break;
				}
			}

			result.records = items.length;
			result.rowNo = rowNo;

			res.end(JSON.stringify(result));
		} catch (e) {
			dlog("ERROR: fetchtabledata_findrow failed");
			dlog(e);
			dlog(e.stack);
		}
    }

    /**
     * Export table data to  CSV file for download
     *
     * @param message: The postData from the client telling us what to export
     * @param res: http response from browser
     */
	// Bug #5178 - Updated to use async/await to support node mongoddb drivers => v4.x because callbacks for collections have been removed
    var fetchtabledata_csv = async function fetchtabledata_csv(message, res) {
	    // Helper to add a leading zero if needed
        function zeroFill( i ) {
            return  i < 10  ? '0' + i : '' + i;
        }

        // Helper to create a raw date/time string from a date object
        function getDateStr( dateObj ) {
            let result = dateObj.getFullYear().toString() +
            zeroFill( dateObj.getMonth()   ) +
            zeroFill( dateObj.getDay()     ) + "-" +
            zeroFill( dateObj.getHours()   ) +
            zeroFill( dateObj.getMinutes() ) +
            zeroFill( dateObj.getSeconds() );
            return result;
        }

        // Helper to convert a value to CSV compatible format
        function itemToCsv( itemName, itemValue ) {
            let isStr = itemName == null || itemName == undefined || itemName.match(/A$/) ? true : false;
            if( isStr === false )
				return itemValue.trim();
            if( itemValue.indexOf('"') > -1 || itemValue.indexOf(',') > -1 || itemValue.indexOf('\n') > -1 || itemValue.indexOf('\r') > -1 ) {
				return '"'+itemValue.replace(/["]/g,'""')+'"';
            }
            return itemValue.trim();
        }

        try{
			var collection = mongoCacheDb.collection(message.collection);

			// We are in the GET COLLECTION callback
			var selection = buildMongoSelection(message);
			var sorter = buildMongoSort(message);
			var collist = {};
			var csvHeader = "";
			//add collation
			var caseSort = message.caseSort == "true";
			var collationOptions =  { "locale": mongoLocale };

			if( caseSort == true ) {
				collationOptions["locale"] = "simple";
			}
			//make sort column indexed so mongo doesnt overflow
			// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
			await collection.createIndex(sorter, { collation: collationOptions });

			// Let's put together a subset of field names and labels to requet from the database
			let csvList = [];
			message.colModel.forEach(function(item){
				if( (item.hidden === undefined || item.hidden === false) && item.name !== "rn" ) {
					// Build out list of columns to request from our mongo table
					collist[item.name] = 1;
					// Build a list to use names/labels to use in building out CSV records
					csvList.push( {
					name:  item.name,
					label: item.label && item.label.length > 0 ? item.label : item.name
					});
				}
			});

			// Let's set up our GridFSBucket to hold out CSV file
			let fileName = "tableExport-" + getDateStr( new Date() ) + ".csv";
			let gb = new GridFSBucket(mongoCacheDb, { bucketName: message.collection });
			let rStream = new Readable({ read(size) {  } });
			let uploadStream = gb.openUploadStream(fileName);
			uploadStream.options.metadata = {
				'url': "/getFile/" + message.collection + "/" + fileName,
				'id': uploadStream.id
			};

			// Add a finish handler to send the result after the CSV file is completely written
			uploadStream.once("finish", function uploadStream_onceFinish_csv() {
				let result = { "status": true, "url": "/getFile/"+message.collection+"/"+fileName };
				res.end(JSON.stringify(result));
			});

			// Add a stream pipe to make it easier to write CSV data into our gridfs file
			rStream.pipe(uploadStream);

			// Create and write the CSV header record
			csvList.forEach(function (item) {
				if( csvHeader.length > 0 )
					csvHeader += ",";
				csvHeader += itemToCsv( null, item.label );
			});
			rStream.push(csvHeader + '\n');

			// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
			var rows = await collection.find({ $and: [{"datalookupid": message.datalookupid}, selection] }, { projection: collist } )
										.collation(collationOptions)
										.sort(sorter)
										.toArray();
			// We have data, parse through each item
			if (rows) {
				for (var i = 0; i < rows.length; i++) {
					// Build a CSV data record
					let csvRec = "";
					let comma = "";
					csvList.forEach(function (item) {
						csvRec += comma + itemToCsv(item.name,rows[i][item.name]);
						comma = ",";
					});
					rStream.push(csvRec + "\n");
				}
			} else {
				dlog("Error: csv to array error, no rows");
			}
			rStream.push(null);
        } catch (e) {
            dlog("ERROR: connecting to db");
            dlog(e);
            dlog(e.stack);
        }
    };

    /**
     * Get list of record keys for range selection
     *
     * @param message: The postData from the client telling us what keys to get
     * @param res: http response from browser
     */
	// Bug #5178 - Updated to use async/await to support node mongoddb drivers => v4.x because callbacks for collections have been removed
    var fetchrangeofkeys = async function fetchrangeofkeys(message, res) {
        try {
			var collection = mongoCacheDb.collection(message.collection);

			// We are in the GET COLLECTION callback
            var selection = buildMongoSelection(message);
            var sorter = buildMongoSort(message);
            var collist = { "_id": false, "id2": true };
			var keylist = [];
			var keyBeg = message.keyBeg;
			var keyEnd = message.keyEnd;

			// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
			var rows = await collection.find({ $and: [{"datalookupid": message.datalookupid}, selection] }, { projection: collist } )
			  .sort(sorter)
			  .toArray();

			// Bug #5189 - Code generated/modified by AI tool to fix any issues that it found
			// We have data, parse through each item until we hit the end of the range
			if (rows) {
				for (var i = 0; i < rows.length; i++) {
					if( keyBeg == undefined || keyEnd == undefined ) {
						keylist.push(rows[i]["id2"]);
					}
					if( keyBeg != undefined && keyBeg == rows[i]["id2"] ) {
						keyBeg = undefined;
						if( keyEnd == undefined ) {
							break;
						} else {
							keylist.push(rows[i]["id2"]);
						}
					}
					if( keyEnd != undefined && keyEnd == rows[i]["id2"] ) {
						keyEnd = undefined;
						if( keyBeg == undefined ) {
							break;
						} else {
							keylist.push(rows[i]["id2"]);
						}
					}
				}
			}

			let result = { "status": true, "keys": keylist };
			res.end(JSON.stringify(result));

        } catch (e) {
            dlog("ERROR: connecting to db");
            dlog(e);
            dlog(e.stack);
        }
    };

    function createWebSocket() {
        if (sslEnabled)
            var https = require('https');
        else
            var https = require('http');

		// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
        async function processDataRequest(req, res) {
            var postData = "";
            var query;
            var url = require('url');
			// Changing to non-deprecated version
            //var url_parts = url.parse(req.url, true);
            var url_parts = url.parse(req.url);

            /*Options is sent for CORS, when there is headers attached to request. Use this to reply so request
             **doesn't time out*/
            if (req.method === "OPTIONS") {
                res.writeHead("200", {
                    "Access-Control-Allow-Origin": req.headers.origin,
                    "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE, OPTIONS",
                    "Access-Control-Allow-Headers": "Content-type, Accept, X-Custom-Header, X-Requested-With"
                });
                res.end();
            } else if (req.method === "POST") {
                if (url_parts.pathname.indexOf("/upload/") != -1) {
                    /*If we are sending file to be uploaded to mongo*/
                    var fgb = url_parts.pathname.substring(url_parts.pathname.indexOf("/upload/") + 8);
		            fgb = fgb.substring(0, fgb.indexOf("/"));
                    var skipLen = fgb.length + 9;
                    var gb = new GridFSBucket(mongoCacheDb, {
                        bucketName: fgb
                    });
                    var fileName = url_parts.pathname.substring(url_parts.pathname.indexOf("/upload/") + skipLen);
					fileName = encodeURIComponent(decodeURIComponent(fileName));
                    var uploadStream = gb.openUploadStream(fileName);

                    uploadStream.on("error", function uploadStream_onError(error) {
                        dlogForce("appxMongoConnector /upload/ ERROR filename="+uploadStream.filename+", length="+uploadStream.length+", error="+error,uploadStream.state);
                        res.writeHead("500", { 'Access-Control-Allow-Origin': '*','Bucket': fgb });
                        res.write("Error uploading file to mongo database");
                        dlog(error);
                        uploadStream = null;
                    });
                    uploadStream.once("finish", function uploadStream_onceFinish() {
                        dlogForce("appxMongoConnector /upload/ FINISH filename="+uploadStream.filename+", length="+uploadStream.length);
                        res.writeHead("201", {'Access-Control-Allow-Origin': '*', 'Bucket': fgb });
                        res.end();
                    });
                    req.pipe(uploadStream);
                } else if (url_parts.pathname.indexOf("/userPrefs/") != -1) {
                    /*Upload user preferences to mongo. Currently only saved table preferences*/
                    var fgb = url_parts.pathname.substring(url_parts.pathname.indexOf("/userPrefs/") + 11);
		            fgb = fgb.substring(0, fgb.lastIndexOf("/"));
                    var gb = new GridFSBucket(mongoUserPrefsDb, {
                        bucketName: fgb
                    });

					// Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
					await gb.drop();
                    upload();

                    function upload() {
                        var uploadStream = gb.openUploadStream(fgb);
                        uploadStream.on("error", function uploadStream_onError(error) {
                            res.writeHead("500", { 'Access-Control-Allow-Origin': '*', 'Bucket': fgb });
                            res.write("Error uploading file to mongo database");
                            dlog("Error uploading file to mongo database");
                            dlog(error);
                            uploadStream = null;
                        });
                        uploadStream.once("finish", function uploadStream_onceFinish() {
                            res.writeHead("201", { 'Access-Control-Allow-Origin': '*', 'Bucket': fgb });
                            res.end();
                        });

                        req.pipe(uploadStream);
                    }
                } else {
                    req.on("data", function req_onDataCallback(chunk) {
                        postData += chunk.toString();
                    });

                    req.on("end", function req_onEndCallback() {
                        if( req.headers["content-type"] === "application/json" ) {
                            query = JSON.parse(postData);
                        } else {
                            var qString = require("querystring");
                            query = qString.parse(postData);
                        }
                        getData();
                    });
                }
            } else if (req.method === "HEAD") {

            } else {
                query = url_parts.query;
                getData();
            }

            async function getData() {
                var mime = require('mime');

                /*Create a readable stream to pass the chunks into, for piping to mongo*/
                var rStream = new Readable({
                    read() {}
                });

                /*
                 **Function send HTTP error back to browser for display.
                 **
                 **@param mRes: Response to http request
                 */
                function fileNotFound() {
                    /*If file is not found, then we return a file not found
                     **error*/
                    res.writeHead(404, { 'Content-Type': 'text/plain' });
                    res.write('404 Not Found\n');
                    res.end();
                }

                if (url_parts.pathname.indexOf('/getGridData') != -1) {
		            dlog("From " + (cluster.isMaster ? ("master process (pid: " + process.pid + ")"):("worker process (id: " + cluster.worker.id + ", pid: " + process.pid + ")" )) + " - Got request for the grid data... ");

                    res.writeHead(200, {'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' });

                    await fetchtabledata(query, res);
                }
                if (url_parts.pathname.indexOf('/getGridFindRow') != -1) {
		            dlog("From " + (cluster.isMaster ? ("master process (pid: " + process.pid + ")"):("worker process (id: " + cluster.worker.id + ", pid: " + process.pid + ")" )) + " - Got request for grid findrow... ");

					res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' });

                    await fetchtabledata_findrow(query, res);
                } else if (url_parts.pathname.indexOf('/getGridCsv') != -1) {
		            dlog("From " + (cluster.isMaster ? ("master process (pid: " + process.pid + ")"):("worker process (id: " + cluster.worker.id + ", pid: " + process.pid + ")" )) + " - Got request to export grid to a cvs file... ");

					res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' });

					await fetchtabledata_csv(query, res);
				} else if (url_parts.pathname.indexOf('/getRangeKeys') != -1) {
		            dlog("From " + (cluster.isMaster ? ("master process (pid: " + process.pid + ")"):("worker process (id: " + cluster.worker.id + ", pid: " + process.pid + ")" )) + " - Got request to GET range keys... ");

					res.writeHead(200, {'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' });

					await fetchrangeofkeys(query, res);
				} else if (url_parts.pathname.indexOf('/setUserPrefs') != -1) {
		            dlog("From " + (cluster.isMaster ? ("master process (pid: " + process.pid + ")"):("worker process (id: " + cluster.worker.id + ", pid: " + process.pid + ")" )) + " - Got request to SET user preferences... ");

					res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' });

					await setuserprefs(query, res);
				} else if (url_parts.pathname.indexOf('/getUserPrefs') != -1) {
		            dlog("From " + (cluster.isMaster ? ("master process (pid: " + process.pid + ")"):("worker process (id: " + cluster.worker.id + ", pid: " + process.pid + ")" )) + " - Got request to GET user preferences... ");

					res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' });

					await getuserprefs(query, res);
				} else if (url_parts.pathname.indexOf("/getResource/") != -1) {
					// Bug #5189 - Code generated/modified by AI tool to fix any issues that it found
					var gb = new GridFSBucket(mongoCacheDb, { bucketName: "resource" });

                    var fileName = encodeURI(url_parts.pathname.substring(url_parts.pathname.indexOf("/getResource/") + 13));
                    var downloadStream = gb.openDownloadStreamByName(fileName);
                    var mimeType = mime.getType(fileName);

                    res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': mimeType, 'Cache-Control': 'max-age=31536000' });

                    rStream.pipe(res);

                    downloadStream.on("error", function downloadStream_onError() {
                        fileNotFound(res);
                    });

                    downloadStream.on("data", function downloadStream_onData(data) {
                        rStream.push(Buffer.from(data));
                    });

                    downloadStream.on("end", function downloadStream_onEnd() {
                        rStream.push(null);
                    });

				} else if (url_parts.pathname.indexOf("/getFile/") != -1) {
                    /*If we pushed file into mongo to be displayed in the browser or use
                     **the browser to save file to client*/
                    var fgb = url_parts.pathname.substring(url_parts.pathname.indexOf("/getFile/") + 9);
		            fgb = fgb.substring(0, fgb.indexOf("/"));
                    var skipLen = fgb.length + 10;

					dlog("fgb collection = " + fgb);

                    var gb = new GridFSBucket(mongoCacheDb, { bucketName: fgb });
                    var contentType = 'application/octet-stream';
                    var fileName = url_parts.pathname.substring(url_parts.pathname.indexOf("/getFile/") + skipLen);

					dlog("filename = " + fileName);

					if (url_parts.pathname.indexOf("pushAndOpen") > -1) {
                        contentType = mime.getType(fileName);
                    }
                    var mimeType = mime.getType(fileName);

					// Bug #5189 - Code generated/modified by AI tool to fix an issue with Adobe Acrobat Extension for Google Chrome
                    var fileDoc = await mongoCacheDb.collection(fgb + ".files").findOne({ filename: fileName });
                    if (!fileDoc) {
                        fileNotFound();
                        return;
                    }

					// Bug #5189 - Code generated/modified by AI tool to fix an issue with Adobe Acrobat Extension for Google Chrome
                    var fileLength = fileDoc.length;

                    // Bug #5189 - Code generated/modified by AI tool to fix an issue with Adobe Acrobat Extension for Google Chrome
                    res.writeHead(200, {
                        'Access-Control-Allow-Origin': '*',
                        'Content-Type': mimeType,
                        'Content-Length': fileLength,
                        'Content-Disposition': 'inline; filename="' + encodeURIComponent(fileName) + '"',
                        'Accept-Ranges': 'bytes',
                        'Cache-Control': 'max-age=31536000'
                    });

                    rStream.pipe(res);

                    var downloadStream = gb.openDownloadStreamByName(fileName);

                    downloadStream.on("error", function downloadStram_onError() {
						// Bug #5189 - Code generated/modified by AI tool to fix any issues that it found
                        rStream.push(null);
                    });

                    downloadStream.on("data", function downloadStream_onData(data) {
                        rStream.push(Buffer.from(data));
                    });

					// Bug #5189 - Code generated/modified by AI tool to fix an issue with Adobe Acrobat Extension for Google Chrome
					// Bug #5178 - Updated to use async/await to support node mongoddb drivers => v4.x because callbacks for collections have been removed					
                    downloadStream.on("end", async function downloadStream_onEnd() {
                        if (this.s.file !== null && this.s.file._id !== null) {
                            var id = this.s.file._id;
							// Bug #5189 - Code generated/modified by AI tool to fix an issue with Adobe Acrobat Extension for Google Chrome
                            setTimeout(async function() {
                                try {
                                    await gb.delete(id);
                                } catch (err) {
                                    // Ignore errors - file may already be deleted by another request
                                    dlog("getFile: delete skipped (already deleted): " + id);
                                }
                            }, 30000); // 30 second delay before deletion
                        }
                        rStream.push(null);
                    });

				} else if (url_parts.pathname.indexOf("/userPrefs/") != -1) {
                    /*Grab user preferences stored in mongo and send down to client*/
                    var fgb = url_parts.pathname.substring(url_parts.pathname.indexOf("/userPrefs/") + 11);
		            fgb = fgb.substring(0, fgb.lastIndexOf("/"));

                    var gb = new GridFSBucket(mongoUserPrefsDb, {
                        bucketName: fgb
                    });

                    res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json', 'Cache-Control': 'no-cache, no-store, must-revalidate' });

                    rStream.pipe(res);

					// Bug #5189 - Code generated/modified by AI tool to fix any issues that it found
                    function pushEmpty() {
                        rStream.push(Buffer.from("{}"));
                        rStream.push(null);
                    }

                    // Bug #5189 - Code generated/modified by AI tool to fix any issues that it found
					try {
                        var filesQuery = mongoUserPrefsDb.collection(fgb + ".files").find({
                            filename: fgb
                        });
                        // Task 669 - Using mongodb promises API via async/await instead of the nodeJS mongodb callbacks API
                        var docs = await filesQuery.toArray();

                        if (docs.length > 0) {
                            var downloadStream = gb.openDownloadStreamByName(fgb);
                            downloadStream.on("error", function downloadStream_onError() {
                                pushEmpty();
                            });

                            downloadStream.on("data", function downloadStream_onData(data) {
                                rStream.push(Buffer.from(data));
                            });

                            downloadStream.on("end", function downloadStream_onEnd() {
                                rStream.push(null);
                            });

                        } else {
                            pushEmpty();
                        }
					// Bug #5189 - Code generated/modified by AI tool to fix any issues that it found	
                    } catch (error) {
                        dlog("Resource handler query.toArray error: " + error);
                        pushEmpty();
                    }
				}
            }
        }

		// Configure the websocket
		if (sslEnabled) {
			// Start as a secure https server
			let certificatePath;
			if (sslPfxFile === "") {
				var options = {
					key: fs.readFileSync(sslPrivateKey),
					cert: fs.readFileSync(sslCertificate),
					ca: fs.readFileSync(sslCertAuthority)
				};
				certificatePath =  require('path').parse(sslPrivateKey);
			} else {
				var options = {
					pfx: fs.readFileSync(sslPfxFile),
					passphrase: sslPfxPassphrase,
					ca: fs.readFileSync(sslCertAuthority)
				};
				certificatePath =  require('path').parse(sslPfxFile);
			}

			let mongoHttpsServer = https.createServer(options, function https_createServerCallback(req, res) {
				processDataRequest(req, res);
			}).listen(connectorPort);

			let timeout;
			fs.watch(certificatePath.dir, (event, filename) => {
					clearTimeout(timeout);
					timeout = setTimeout(() => {
					if (certificatePath.dir + "\\" + filename == sslCertificate || certificatePath.dir + "\\" + filename == sslPrivateKey) {
						console.log("The file " + filename + " changed so, we will update the secure context.");
						try {
							mongoHttpsServer.setSecureContext({
								key: fs.readFileSync(sslPrivateKey),
								cert: fs.readFileSync(sslCertificate),
								ca: fs.readFileSync(sslCertAuthority)
							});
						} catch (ex) {
							console.log("Update of secure context failed. " + ex);
							logactivity("Update of secure context failed. " + ex);
						}
					} else if (certificatePath.dir + "\\" + filename == sslPfxFile) {
						console.log("The pfx file " + filename + " changed, so we will update the secure context.");
						try {
							mongoHttpsServer.setSecureContext({
								pfx: fs.readFileSync(sslPfxFile),
								passphrase: sslPfxPassphrase,
								ca: fs.readFileSync(sslCertAuthority)
							});
						} catch (ex) {
							console.log("Update of secure context failed. " + ex);
							logactivity("Update of secure context failed. " + ex);
						}
					} else {
						console.log("The file " + filename + " changed but it's none of our concern!");
					}
				}, 5000);
			});
		} else {
			// Start as a non-secure http server
			https.createServer(function https_createServerCallback(req, res) {
				processDataRequest(req, res);
			}).listen(connectorPort);
		}
    }

    /*
    **Function to check mongodb for collections that have no open processes
    **and remove the collections from database
    */
/*  Start - The commented code below is not used and will be removed in the future, by 12/31/2025 */
/*
    function databaseCleanup() {
        mongoCacheDb.listCollections().toArray(function (err, items) {
            const exec = require('child_process').exec;
            let collName = [];
            let cmd = '';
            var processes;
            for (var i = 0; i < items.length; i++) {
                var name = items[i].name;
                if (!isNaN(name)) {
                    collName.push(name);
                }
            }
            switch (process.platform) {
                case 'win32':
                    cmd = "tasklist";
                    break;
                case 'darwin':
                case 'linux':
                    cmd = "ps ax";
                    break;
                default:
                    cmd = "ps ax";
                    break;
            }
            exec(cmd, (err, stdout, stderr) => {
                for (var i = 0; i < collName.length; i++) {
                    if (stdout.indexOf(collName[i]) === -1) {
                        mongoCacheDb.dropCollection(collName[i], {}, function (err) {
                            if (err) {
                                dlog(err);
                            }
                        });
                    }
                }
            });
        });
    }
*/
/*  End - The commented code below is not used and will be removed in the future, by 12/31/2025 */

}