Source: src/jsBusinessLogic.js

/*globals ObjectRow,Environment,sqlFun,objectRow,Context,MsgParser, jsDataQuery */

/**
 * Manages inviocation of business logic
 * @module jsBusinessLogic
 */


/* jshint -W069 */
const GetDataSpace = require('../src/jsGetData');
const dsSpace = require('./../client/components/metadata/jsDataSet');
const DataSet = dsSpace.DataSet;
const DataColumn = dsSpace.DataColumn;
const DataRowVersion = dsSpace.dataRowVersion;
const DataRowState = dsSpace.dataRowState;

const Parser = require('./jsMsgParser').MsgParser;


/* {DataRow} */
const DataRow = dsSpace.DataRow;

const DataTable = dsSpace.DataTable;

const jsMultiSelect = require('./jsMultiSelect');
const DataAccess = require('./jsDataAccess').DataAccess;

const Select = jsMultiSelect.Select;
const isolationLevels = require('./jsDataAccess').isolationLevels;

const q = require("./../client/components/metadata/jsDataQuery").jsDataQuery;
const _ = require('lodash');
const BusinessLogicResult = require("./jsPostData").BusinessLogicResult;
const BasicMessage = require("./jsPostData").BasicMessage;
const PostData=require("./jsPostData").PostData;
/**
 * jsBusinessLogic
 * Use of this class should be
 *
 * let jBs = new jsBusinessLogic(conn, environment, rowChanges);
 * jBs
 * let Post = new PostData(conn, environment);
 */



/**
 *
 * @type function
 */
const Deferred = require("JQDeferred");



/**BusinessMessage
 * Represents a message connected to a changed (modified, added, deleted) row
 * @class
 * @augments BasicMessage
 * @param {RowChange} [options.rowChange] row to check
 * @param {DataRow} [options.r] DataRow to check, needed if rowChange is not provided
 * @param {boolean} options.post true is is a post commit data check
 * * @param {string} options.toCompile
 * @param {string} options.shortMsg  rule name, audit["title"]
 * @param {string}  options.longMsg complete message, used for merging , audit["message"] compiled
 * @param {string} [options.idRule] id of the business rule, audit["idaudit"], means to be the b.r. "group code"
 * @param {int} [options.idDetail]    id of the detail for the business rule, audit["idcheck"], b.r. number
 * @param {Environment} options.environment
 * @param {boolean} options.canIgnore true if it is a warning, false if it is an unrecoverable error (audit["severity"].toLowerCase()==="w")
 */
function BusinessMessage (options){
    BasicMessage.apply(this,[options.longMsg,options.canIgnore]);
    this.rowChange = options.rowChange;

    if (options.rowChange){
        this.r=options.rowChange.r;
    }
    if (options.r){
        this.r=options.r;
    }

    this.shortMsg=options.shortMsg; //nome della regola (audit["title"])
    this.idRule=options.idRule;     // audit["idaudit"], means to be the b.r. "group code"
    this.idDetail=options.idDetail; // audit["idcheck"], b.r. number
    this.environment=options.environment;
    this.post = options.post;

    /**
     *
     * @type {OneSubst[]}
     */
    this.openSubstitutions=[];


    let compiled = "";

    let /*MsgParser*/ parser = this.getParser(options.longMsg);
    let token= parser.getNext();
    while(token!== null){
        if (token.skipped) {
            compiled += token.skipped;
        }
        if (token.found!== null){
            compiled+= this.compileParameter(options.rowChange,token.found);
        }
        token= parser.getNext();
    }
    this.setMessage(compiled);
}

BusinessMessage.prototype =  _.extend(
    new BasicMessage("dummy",false),
    {
            constructor: BusinessMessage,
            superClass: BasicMessage.prototype,
        getId: function (){
                return this.msg+"#"+this.idRule;
        },


    }
);

/**
 * @method getParser
 * param {string} msg
 * @return {MsgParser}
 */
BusinessMessage.prototype.getParser= function(msg){
    return new Parser(msg,"%<",">%");
};

/**
 * Gets parameter from row or delays evaluation adding an open substitution to the openSubstitutions list
 * @method
 * @param {DataRow} focusedRow
 * @param {string} colName
 * @param {string} msgTable
 * @param {string} fromTable
 * @return {string}
 */
BusinessMessage.prototype.getParameter = function(focusedRow, colName,msgTable,fromTable){
    if (!focusedRow){
        return "";
    }
    let colFound = this.findPostingColumn(focusedRow.table,colName);
    if (colFound){
        return focusedRow.current[colFound];
    }
    let original="%<"+msgTable+"."+colName+">%";
    this.openSubstitutions.push(new OneSubst(colName,msgTable,original,fromTable));
    return original;
};


/**
 * Translate a Parameter name into a value, taking data from A and related. If it is not possible to extract it,
 *  pushes a new open substitution to the openSubstitutions array and returns the uncompiled expression
 * @param {RowChange} rowChange
 * @param {string} expr
 */
BusinessMessage.prototype.compileParameter = function(rowChange, expr){
    let r= rowChange.r;
    let parameter = expr.trim();
    let pieces = parameter.split(".");
    let tableName;
    if (pieces.length>1) {
        tableName= pieces[0];
    }
    else {
        tableName=r.table.name;
    }
    let colExpression;

    Object.values(r.table.columns).some(/* {DataColumn}*/ c=>{
        if (c.name=== parameter){//manage the simplest case
            colExpression= r[parameter];
            return true;
        }
        if (c.expression === parameter){
            colExpression= this.getParameter(r,c.name,tableName,r.table.name);
            return  true;
        }
    });

    if (colExpression!==undefined){
        return colExpression;
    }
    if (pieces.length>1) { //parameter like "tablename.columnName"
        let colName= pieces[1];
        let rRelated=rowChange.getRelated(tableName);
        if (!rRelated){
            let original="%<"+parameter+">%";
            this.openSubstitutions.push(new OneSubst(colName,tableName,original,r.table.name));
            return  original;
        }
        return this.getParameter(rRelated,colName,tableName,r.table.name);
    }
    else { //parameter like "fieldName"
        let o = this.environment.sys(parameter);
        if (o) {
            return o;
        }
        return  this.getParameter(r, parameter, tableName, r.table.name);

    }
};





/**
 * Evaluates the column corresponding to a db field
 * @param {DataTable} t
 * @param {string} field
 * @returns {string|null}
 */
BusinessMessage.prototype.findPostingColumn = function (t, field){
    /** @type {DataColumn} */
    let c= t.columns[field];
    if (c){ //a field was found with given name
        if (t.tableForWriting()===t.name){
            return field;
        }
        if (c.forPosting===field){
            return field;
        }
    }
    //searches for a column having "forPosting" set as given field name
    if (t.tableForWriting()!==t.name) {
        let postingCol = _.find(t.columns, c => c.forPosting === field);
        if (postingCol) {
            return postingCol.name;
        }
    }
    if (c===undefined) {
        return null;
    } //field was not found

    if (c.forPosting ) {
        return null;
    } //a field was found but it has another posting column (
    return field; //forPosting is the same as field
};



/**
 * Manages business logic invocation
 * @class BusinessLogic
 * @method BusinessLogic
 * @param {Context} context
 * @param {ObjectRow[]} rowChanges
 * @constructor
 */
function BusinessLogic(context, rowChanges){
    /* {Context} */
    this.context = context;

    /*{DataAccess}*/
    this.conn= context.sqlConn;

    /* {DbDescriptor} */
    this.dbDescriptor = context.dbDescriptor;

    /*{Environment}*/
    this.environment = context.environment;

    /* {Connection} the sql driver, can be jsSqlDriver or jsMySqlDriver */
    this.driver = context.sqlConn;

    /* {ObjectRow[]}  */
    this.rowChanges = rowChanges;

    /* @property {Promise<DataSet>} auditsPromise*/
    this.auditsPromise = this.getRules(rowChanges); //starts evaluating

    /* property {BusinessMessage[]} messages */
    this.messages = [];

    /*{jsDataQuery}*/
    this.ruleFilter = this.getFilterForRules();

    this.maxQueryLength= 10000;

}



BusinessLogic.prototype = {
    constructor: BusinessLogic,
    maxBatchSize: 40000,
    envFilterVariableName:"filterrules",
    /**
     *
     * @return {jsDataQuery}
     */
    getFilterForRules: function(){
        return this.environment.sys(this.envFilterVariableName);
    },
    destroy: function() {
        const def = Deferred();
        if (!this.auditsPromise) {
            return def.resolve(true).promise();
        }
        this.auditsPromise.then(() => {
                def.resolve(true);
            },
                err => def.reject(err)
        );
        return def.promise();
    }
};

/**
 * Creates a db error
 * @param {string} message
 * @param {boolean} post
 */
BusinessLogicResult.prototype.makeDbError = function (message,post){
    return new BusinessMessage({post:post,shortMsg:"Generic error", longMsg:message,canIgnore:false});
};

/**
 * Adds a db error
 * @param {string} message
 * @param {boolean} post
 */
BusinessLogicResult.prototype.addDbError = function (message,post){
    return this.addMessage(this.makeDbError(message,post));
};




/**
 * Get stored procedure name to be called to check r audits
 * @param {ObjectRow} r
 * @param {boolean} post
 * @return {string}
 */
BusinessLogic.prototype.getProcName = function (r, post){
    let suffix = post? "post":"pre";
    let  row= r.getRow();
    let postingTable = row.table.tableForWriting();
    return "check_"+postingTable+"_"+this.opCode(row)+"_"+suffix;
};

/**
 * Gives a code for operation being applied to r
 * @param {DataRow} r
 * @return {string}
 */
BusinessLogic.prototype.opCode = function (r){
    switch (r.state ){
        case  DataRowState.added: return "i";
        case  DataRowState.deleted: return "d";
        case  DataRowState.modified: return "u";
    }
    return undefined;
};

/**
 * calculate a filter to obtain rule parameters
 * @param {ObjectRow} r
 * @param {boolean} post
 * @return {sqlFun}
 */
BusinessLogic.prototype.getProcFilter= function (r, post) {
    /** {DataTable */
    let  row= r.getRow();
    return q.and (q.eq("precheck", post?"N":"S"),
                        q.isNotNull("sqlcmd"),
                        q.ne("severity","I"),
                        q.eq("tablename",row.table.tableForWriting()),
                        q.eq("opkind",this.opCode(row).toUpperCase())
    );
};

/**
 * Creates a dataset to read db checks
 * @return {DataSet}
 */
BusinessLogic.prototype.createAuditDataSet = function (){
    let ds = new DataSet("audit");
    ds.newTable("tableop");
    let auditcheckview= ds.newTable("auditcheckview");
    ds.newTable("auditparameter");
    ds.newRelation("tableop_auditcheckview",
        "tableop",["tablename","opkind"],
        "auditcheckview", ["tablename","opkind"]);
    ds.newRelation("tableop_auditparameter",
        "tableop",["tablename","opkind"],
        "auditparameter", ["tablename","opkind"]);
    auditcheckview.staticFilter(q.and(q.isNotNull("sqlcmd"),q.ne("severity","I")));
    auditcheckview.orderBy("idaudit ASC, idcheck ASC");
    return ds;
};

/**
 * Gets necessary rules from a database in order to save a set of row change. This can ben invoked
 *  before the start of a transaction.
 * @param {ObjectRow[]} rowChanges  changes made in memory
 * @returns {Deferred<DataSet>}
 */
BusinessLogic.prototype.getRules = function  ( rowChanges){
    let ds = BusinessLogic.prototype.createAuditDataSet();
    let tableOp= ds.tables["tableop"];
    let added= new Set();
    _.forEach(rowChanges,function(r){
        const row= r.getRow();
        const t= row.table.name;
        const kind = BusinessLogic.prototype.opCode(row);
        const k= t+"#"+kind;
        if (!added.has(k)) {
            added.add(k);
            tableOp.newRow({tablename:t, opkind:kind});
        }
    });
    tableOp.acceptChanges();
    let def= Deferred();

    GetDataSpace.getStartingFrom(this.context, tableOp).
        then(function(){
            def.resolve(ds);
        })
        .fail(err=>def.reject(err));

    return def.promise();
};

/**
 * Evaluates a series of messages
 * @param {BusinessLogicResult} result
 * @param {DataAccess} conn Current connection having the transaction attached
 * @param {boolean} post
 * @returns  {Promise<BusinessLogicResult>}
 */
BusinessLogic.prototype.getChecks =  function (result, conn, post) {
    let def= Deferred();
    try {
        this.getChecksAsync(result,conn,post).then(function(r){
            def.resolve(result);
        });
    }
    catch(err){
        def.reject(err);
    }
    return def.promise();
};

/***
 * Extracts audit checks for every row change in input
 * @param {DataTable} auditview
 * @param {ObjectRow[]} changes
 * @param {boolean} post
 * @returns {ObjectRow[]} an array of auditview rows for each audit sp name
 */
BusinessLogic.prototype.groupChecks = function (auditview,changes,post){
    let audits = {};
    changes.forEach(r=> {
        let spName = this.getProcName(r, post);
        if (audits[spName]) {
            return; //already evaluated
        }

        let filterCheck = this.getProcFilter(r, post);
        if (this.ruleFilter) {
            filterCheck = q.and(filterCheck, this.ruleFilter);
        }

        audits[spName] = auditview.select(filterCheck)
            .sort(function (a, b) { //sort by idaudit asc, idcheck asc
                if (a.idaudit > b.idaudit) {
                    return 1;
                }
                if (a.idaudit < b.idaudit) {
                    return -1;
                }
                if (a.idcheck > b.idcheck) {
                    return 1;
                }
                if (a.idcheck < b.idcheck) {
                    return -1;
                }
                return 0;
            });

    });
    return  audits;
};

/**
 * Evaluates a series of messages
 * @param {BusinessLogicResult} result
 * @param {DataAccess} conn Current connection having the transaction attached
 * @param {boolean} post
 * @returns  {Promise<BusinessLogicResult>}
 */
BusinessLogic.prototype.getChecksAsync = async function (result, conn, post){
    const def = Deferred();



    /* {DataSet} */
    let auditsDS = await this.auditsPromise;

    //extracts only rules applying in this phase (pre/post)
    /* {{objectRow[]}} */
    let auditChecks= this.groupChecks(auditsDS.tables["auditcheckview"], this.rowChanges, post);

    let logCalls = new Set();
    let batchCommand= [];
    let batchLen=0;
    /*{Deferred}*/
    let overallInvoke=null;
    /* {RowChange[]} */
    let changes= [];
    let numAudit=0;

    //this construct is necessary to execute different batch serially
    await this.rowChanges.reduce( async (previousResult,r)=>{
        await previousResult;
        let spName= this.getProcName(r,post);
        if (!auditChecks[spName]){
            return [];
        }
        let nChecks= auditChecks[spName].length;
        if (nChecks===0){
            return [];
        }
        numAudit+=1;
        let varName= this.driver.variableNameForNBits(numAudit, nChecks);
        let sqlType = this.driver.sqlTypeForNBits(nChecks);

        let rc = new RowChange(nChecks, r.getRow(),auditChecks[spName],varName,this.driver.colNameFromVarName(varName));
        changes.push(rc);

        //this is an array of parameters required to invoke the check sp.
        let parameters = this.parametersFor(auditsDS.tables["auditparameter"],rc,post);


        //evaluates an hash to detect if the same call has already been queued
        let proc_sign=spName+":";
        parameters.forEach(p =>{
            proc_sign+= p.name+"="+p.value+";";
        });
        if (logCalls.has(proc_sign)){ //call already queued
            return [];
        }
        logCalls.add(proc_sign);

        //let varDeclaration= this.driver.declareAndClearVariableForNBits(varName,nChecks);
        //batchCommand.push(varDeclaration);
        //puts result in variable named varName, that at the end will go into a column named
        parameters.splice(0,0, this.driver.createSqlParameter(undefined,'@res',varName,sqlType,true));

        let slqCheckInvoke= this.driver.getSqlCallSPWithNamedParams({spName:spName,paramList:parameters,skipSelect:true});
        batchCommand.push(slqCheckInvoke);
        batchLen+=slqCheckInvoke.length;

        if(batchLen>this.maxBatchSize){
            let batch=this.driver.appendCommands(batchCommand);
            //every batch must be executed in sequence, stopping on the first db error
            await  this.execCheckBatch(conn,changes,batch,result,post);
            numAudit=0;
            batchCommand=[];
            batchLen=0;
            changes= [];
        }
        return [];
    },[]);


    if (batchLen>0){ //same as before
        let batch=this.driver.appendCommands(batchCommand);
        await  this.execCheckBatch(conn,changes,batch,result,post);
    }

    //reads missing data to fill messages from database
    await this.refineMessages(conn, result.checks);

    return result;

};

/**
 * Evaluate queries for business messages open substitutions calling setQuery and setCols
 * @param {DataSet} d
 * @param {BusinessMessage[]} results
 * @return {Deferred}
 */
BusinessLogic.prototype.refineMessages_evaluateQueries= async function(d, results){

    return  results.reduce(async (dummyMessage,bm)=>{
        await dummyMessage;
        //console.log("reducing businessmessage "+bm.msg);
        return bm.openSubstitutions.reduce(async (dummySubst,o)=> {
            //console.log(bm.msg+": reducing openSubstitutions "+o.tableName);
            if (o.query) {
                return "query already present";
            }
            await  dummySubst;
            let /*DataTable*/ t = d.tables[o.tableName];
            if (!t){
                try {
                    t = await this.dbDescriptor.createTable(o.tableName);
                }
                catch (err){
                    //console.log(err);
                    return err;
                }

                d.addTable(t);
            }
            let /*DataTable*/ base=d.tables[o.fromTable];
            if(!base){
                try {
                    base = await this.dbDescriptor.createTable(o.fromTable);
                    d.addTable(base);
                }
                catch (err){
                    //console.log(err);
                    return err;
                }
            }
            if (base.key().length === 0 || t.key().length===0){
                let postingT;
                try {
                    postingT = await this.dbDescriptor.createTable(bm.r.table.tableForWriting());
                }
                catch (err){
                    //console.log(err);
                    return err;
                }

                if (base.key().length === 0 && postingT.key().length>0){
                    let absentColumns= postingT.key().filter(c=>base.columns[c]===undefined);
                    if (absentColumns.length===0){
                        //console.log("base key set to "+base.name+" from table "+postingT.name);
                        t.key(postingT.key());
                        base.key(postingT.key());
                    }
                }
                if (t.key().length === 0 && postingT.key().length>0){
                    let absentColumns= postingT.key().filter(c=>t.columns[c]===undefined);
                    if (absentColumns.length===0){
                        //console.log("t key set to "+t.name+" from table "+postingT.name);
                        t.key(postingT.key());
                    }
                }
            }
            let cols= [];
            let /*jsDataQuery*/ query = this.getQuery(base,bm.r, t, cols);//fromT = base, toTable= t
            if (query===undefined){ //empty query
                //console.log("assigning true to o.error");
                o.error=true;
                // results.push(new BusinessMessage({
                //     rowChange:bm.rowChange,post:bm.post,shortMsg:"Error in Business rule",
                //     longMsg:"Reference to table "+o.tableName+"."+o.field+" in rule "+
                //         bm.idRule+" "+bm.r.table.name+ " "+bm.idDetail+" is not valid.",canIgnore:false
                // }));
                let err="Reference to table "+o.tableName+"."+o.field+" in rule "+
                    bm.idRule+" on "+bm.r.table.name+ " id:"+bm.idDetail+" is not valid (missing key column).";
                //console.log(err);
                return err;
            }
            if (!t.columns[o.field]){ //empty query
                //console.log("assigning true to o.error");
                o.error=true;
                // results.push(new BusinessMessage({
                //     rowChange:bm.rowChange,post:bm.post,shortMsg:"Error in Business rule",
                //     longMsg:"Reference to table "+o.tableName+"."+o.field+" in rule "+
                //         bm.idRule+" "+bm.r.table.name+ " "+bm.idDetail+" is not valid.",canIgnore:false
                // }));
                let err="Reference to table "+o.tableName+"."+o.field+" in rule "+
                    bm.idRule+" on "+bm.r.table.name+ " id:"+bm.idDetail+" is not valid (missing  column "+o.field+").";
                //console.log(err);
                return err;
            }
            o.setCols(cols);
            o.setQuery(query);

            //Sets query for all substitutions for that message on the same table
            bm.openSubstitutions.forEach(oo=>{
                if (oo.tableName!== o.tableName || oo.fromTable !== o.fromTable || oo.query!==null) {
                    return;
                }
                oo.setQuery(o.query);
                oo.setCols(cols);
                if (!t.columns[oo.field]){
                    oo.error=true; //t does not contain that field
                }
            });

            return "Ok";
        },undefined);

    },undefined);

};

/**
 * Group all queries into SubstGroup
 * @param {BusinessMessage[]} results
 * @param {int} maxQueryLength max len for queries
 * @return {SubstGroup[]}
 */
BusinessLogic.prototype.refineMessages_groupQueries= function(results, maxQueryLength){
    /*{SubstGroup[]}*/
    if (maxQueryLength===undefined){
        maxQueryLength = this.maxQueryLength;
    }
    let subs = [];
    results.forEach(bm =>{
        if (bm.error) {
            return;
        }
        bm.openSubstitutions.forEach(o=>{
            if (o.error || o.query===null) {
                return;
            }
            let found= subs.find(s=>{
                return s.mergeQuery(o,maxQueryLength);
            });
            if (found===undefined){
                let g = new SubstGroup(o.tableName);
                g.mergeQuery(o,maxQueryLength); //surely returns true, because it is empty and on the same table
                subs.push(g);
            }
        });
    } );
    return subs;
};

/**
 * Execute queries stored in grouped substitutions
 * @param {SubstGroup[]} subs
 */
BusinessLogic.prototype.refineMessages_executeQueries= async function(subs){
    return await subs.reduce(async (dummy,s)=>{
        await  dummy; //executes query one at a time
        s.data= await this.context.dataAccess.select({
            tableName:s.tableName,
            filter: q.or(s.query),
            columns: Array.from(s.queryCols).join(","),
            environment: this.environment
        });
        return subs;
    },null);
};


/**
 * Apply substitutions basing on data got in subs
 * @param {SubstGroup[]} subs
 * @param {BusinessMessage[]} msgs
 * @return {*}
 */
BusinessLogic.prototype.refineMessages_applySubstitutions= function(subs, msgs){
    msgs.forEach(bm => {
        bm.openSubstitutions.forEach(o => {
            if (o.error){
                o.newValue= "[bad reference to "+o.tableName+"."+o.field+"]";
            }
            else {
                let somethingFound= subs.find(s=>{
                    if (!s.data){
                        return  false;
                    }
                    if (s.tableName!==o.tableName) {
                        return false;
                    }
                    let rowFound=  s.data.find(o.query);
                    if (!rowFound){
                        return  false;
                    }
                    o.newValue = rowFound[o.field];
                    return true;
                });
                if (!somethingFound){
                    o.newValue="[row not found in table "+o.tableName+" with filter "+o.queryString+"]";
                    o.error=true;
                }
            }
            bm.setMessage(bm.msg.split(o.original).join(o.newValue)); //replaceAll is not supported in node.js 14
        });
    });
};

/**
 * returns a Deferred that resolves when f resolves or throw
 * @param f
 */
function fromAsyncToDeferred(f){

}

/**
 * Reads data from db in order to compile messages. Tries to optimize db access performing as little reads as possible
 * @param {DataAccess} conn
 * @param {BusinessMessage[]} results
 * @return {Promise<BusinessMessage[]>}
 */
BusinessLogic.prototype.refineMessages= async function (conn, results){
    let allReads = new Set(); //hashes of already appended query
    let d = new DataSet("rules");

    //step 1: evaluate query to apply to calc all message fields
    await  this.refineMessages_evaluateQueries(d,results);

    //step 2: group all queries
    /*{SubstGroup[]}*/
    let subs = this.refineMessages_groupQueries(results, this.maxQueryLength);

    //step 3: executes queries
    await this.refineMessages_executeQueries(subs);

    //step 4: apply substitutions
    this.refineMessages_applySubstitutions(subs,results);

};
//= this.getQuery(base,bm.rowChange, t, cols);

/**
*  Evaluates a jsDataQuery to read data of toTable from database
 * FromT is a table while toTable could be a view, so could not have a primary key
 * @param {DataTable} fromT
 * @param {DataRow} fromR
 * @param {DataTable} toTable
 * @param {Array.<string>} cols key columns  and eventually  ayear field if it belongs to table columns
 * @return {jsDataQuery}
 */
BusinessLogic.prototype.getQuery = function (fromT, fromR, toTable, cols){
    let /*jsDataQuery*/ filter;
    //Tries to complete toTable's key with the key of fromT if it exists
    //console.log("getQuery("+fromT.name+",fromR,"+toTable.name+",cols)");
    if (toTable.key().length ===0){
        //console.log("setting key from "+fromT.name);
        let newKey= fromT.key().reduce((/*string[]*/ keys, /*string*/keyField)=>{
            if (!toTable.columns[keyField]){
                //console.log("skipping "+keyField);
                return;
            }
            let clause=q.eq(keyField,fromR.current[keyField]);
            filter =  filter?  q.and(filter,clause):  clause;
            cols.push(keyField);
            //console.log("adding "+keyField+" from "+fromT.name);
        },[]);
        if (toTable.columns["ayear"] && fromT.isKey("ayear")===false){
            let clause=q.eq("ayear",this.environment.field("ayear"));
            filter = filter? q.and(filter, clause): clause;
            cols.push("ayear");
        }
        return filter;
    }
    let primaryKeyIncomplete=false;

    toTable.key().forEach(k=>{
        if (! fromT.columns[k]){
            //console.log("skipping 2 "+k+" from "+fromT.name);
            primaryKeyIncomplete=true;
            return;
        }
        let clause=q.eq(k,fromR.current[k]);
        filter =  filter? q.and(filter,clause): clause ;
        cols.push(k);
        //console.log("adding 2 "+k+" from "+fromT.name);
    });

    if (toTable.columns["ayear"] && primaryKeyIncomplete && fromT.isKey("ayear")===false){
        filter = q.and(filter, q.eq("ayear",this.environment.field("ayear")));
        cols.push("ayear");
    }
    return  filter;

};

/**
 * Executes a batch and adds messages to results
 * @param {DataAccess} conn
 * @param {RowChange[]}  checks
 * @param {string} cmd
 * @param {BusinessLogicResult} result used to push found errors
 * @param {boolean} post
 * @return {*}
 */
BusinessLogic.prototype.execCheckBatch=  async function (conn,
                                                  checks,
                                                  cmd,
                                                         result,
                                                  post) {
    let mapNames= _.map(checks,c=>{
        return {varName:c.varName,colName:this.driver.colNameFromVarName(c.varName)};
    });
    let sql = this.driver.appendCommands([cmd, this.driver.getSelectListOfVariables(mapNames)]);
    try {
        let res = await conn.runSql(sql); //returns an array of objects
        //res is an object with a column for each var name contained in checks
        if (res === undefined || res === null || res.length === 0) {
            result.addDbError("Error invoking business logic\nNo result got from business logic", post);
            return;
        }
        let valueRead= res[0];
        //converts results in messages
        checks.forEach(c => {
            let colName= this.driver.colNameFromVarName(c.varName);
            //every column in the result relates to the RowChange having same index
            if (valueRead[colName]===undefined){
                result.addDbError("Error invoking business logic\ncolumn "+
                    colName+" has not been included in the result.", post);
                return;
            }
            /* {string} 01 sequence decoding the result*/
            let bits  = this.driver.getBitArray(valueRead[colName],c.nChecks);
            this.addMessages(c,bits,result,post);
        });
    }
    catch (err) {
        result.addDbError("Error invoking business logic\nRunning command +" + cmd + ".\n" + err, post);
    }
};

/**
 * Takes error messages and append them to results
 * @param {RowChange} rowChange
 * @param {string} bits 01 representation of sp result
 * @param {BusinessLogicResult} result set, messages are added to this list
 * @param {boolean} post  true if post message
 * @return {*}
 */
BusinessLogic.prototype.addMessages = function(rowChange,bits, result, post){
    ///static void EasyProcedureMessageCollection.DO_ADD_MESSAGES
    for(let i=0; i<rowChange.nChecks; i++){

        if (bits[i]==="1"){
            let audit=rowChange.audits[i];      //row of auditcheckview

            //pushes current message evaluated to the output list
            //there can be more than one BusinessMessage attached to the same rowChange object
            //every BusinessMessage has a list of open substitutions
            result.addMessage(new BusinessMessage({
                rowChange:rowChange,
                post:post,
                shortMsg: audit["title"],
                longMsg:    audit["message"],
                canIgnore:  audit["severity"].toLowerCase()==="w",
                idDetail : audit["idcheck"],
                idRule: audit["idaudit"],
                environment: this.environment
            }));

        }
    }

};

/**
 * Evaluates the filter to obtain parameters for a audit call
 * @param {DataRow} r
 * @param {boolean} post
 */
BusinessLogic.prototype.filterParameters = function (r,post){
    return q.mcmpEq({
                tablename:r.table.tableForWriting(),
                opkind:this.opCode(r).toUpperCase(),
                isprecheck: post?"N":"S"
    });
};

/**
 * Establish if a field must be passed to Business audits
 * @param {DataRow} r
 * @param {string} colName
 */
BusinessLogic.prototype.isTempAutoIncrement = function ( r, colName) {
    if (r.state === DataRowState.added && r.table.autoIncrementColumns[colName]!==undefined) {
        return true;
    }
    if (r.state !== DataRowState.added) {
        return  false;
    }
    let result= false;
    _.forEach(r.table.dataset.relationsByChild[r.table.name],rParentRelation=>{
        if (result){
            return;
        }
        let colRelIndex = rParentRelation.childCols.indexOf(colName);
        if (colRelIndex<0){
            return;
        }
        let parentRows = rParentRelation.getParents(r.current);
        if (parentRows.length===0){
            return;
        }
        let parentRow=parentRows[0];
        if (parentRow.getRow().state!== DataRowState.added){
            return;
        }
        result= this.isTempAutoIncrement(parentRow.getRow(), rParentRelation.parentCols[colRelIndex]);
    });

    return  result;
};


/**
 * Returns a collection of (name, value) couples, evaluated basing on auditParameters
 * @param {DataTable} auditParameters
 * @param {RowChange} rc
 * @param {boolean} post
 * @returns  {Array.<{name:string, value}>}
 */
BusinessLogic.prototype.parametersFor = function (auditParameters, rc,post) {
    /* {DataRow} */
    let row = rc.r;
    let filter = this.filterParameters(row, post);
    /* Array.<{tablename:string, parameterid:int, paramtable:string, paramcolumn:string, flagoldvalue:string}> */
    let pp = _.sortBy(auditParameters.select(filter), ["parameterid"]);

    let result = [];
    let that = this;

    let namedParamSupported= this.driver.namedParameterSupported();
    let pushOptionalParam = function (){
        if (!namedParamSupported){
            result.push(that.driver.createSqlParameter(null));
        }
    };

    pp.forEach(
        /** {tablename:string, parameterid:int, paramtable:string, paramcolumn:string, flagoldvalue:string} */
        p => {
            if (p.paramtable === "sys") {
                let o;
                if (p.paramcolumn.startsWith("sys_")) {
                    o = this.environment.sys(p.paramcolumn.substr(4));
                }
                else {
                    o = this.environment.sys(p.paramcolumn);
                }
                result.push(that.driver.createSqlParameter(o, '@'+p.paramcolumn));
                return;
            }
            if (p.paramtable === "usr") {
                let o;
                if (p.paramcolumn.startsWith("usr")) {
                    o = this.environment.usr(p.paramcolumn.substr(4));
                }
                else {
                    o = this.environment.usr(p.paramcolumn);
                }
                result.push(that.driver.createSqlParameter(o, '@usr_'+p.paramcolumn));
                return;
            }
            let newValueParameter = p.flagoldvalue.toUpperCase() !== "S";
            let paramName = newValueParameter ? "@NEW" : "@OLD";
            if (p.paramtable.toLowerCase() !== p.tablename.toLowerCase()) {
                paramName += "_" + p.paramtable;
            }
            paramName += "_" + p.paramcolumn;

            /*{DataRow} */
            let relatedDataRow = rc.getRelated(p.paramtable);
            if (relatedDataRow === null) {
                pushOptionalParam();
                return;
            }

            let rowVer;
            let rState = relatedDataRow.state;
            if (rState === DataRowState.deleted) {
                rowVer = DataRowVersion.original;
                newValueParameter = false;
            }
            else {
                rowVer = newValueParameter || (rState === DataRowState.added) ? DataRowVersion.current : DataRowVersion.original;
            }
            let foundColumn = BusinessMessage.prototype.findPostingColumn(relatedDataRow.table, p.paramcolumn);
            if (foundColumn === null) {
                pushOptionalParam();
                return;
            }
            if (newValueParameter && that.isTempAutoIncrement(relatedDataRow, foundColumn)) {
                pushOptionalParam();
                return;
            }
            let paramValue= relatedDataRow.getValue(foundColumn, rowVer);
            if (paramValue!==null){
                result.push(that.driver.createSqlParameter(paramValue, paramName));
            }
            else {
                pushOptionalParam();
            }
        });
    return result;
};


/**
 *
 * @class
 * @param {string} field
 * @param {string} tableName
 * @param {string}  original Table name.field as in original message
 * @param {string} fromTable Table name of changed row
 * @constructor
 */
function OneSubst(field, tableName,original,fromTable){
    /**
     * when true, substitution is wrong
     * @property {boolean} error
     */
    this.error=false;

    this.tableName = tableName;

    /**
     * %<table.field>% or %<field>%
     * @property {string} original
     */
    this.original= original;

    /**
     * string that will replace the "original" sequence in the message
     * @property {string|null} newValue
     */
    this.newValue = null;

    /**
     * Table name of changed row
     * @property {string} fromTable
     */
    this.fromTable = fromTable;

    /**
     * Field name
     * @property {string} field
     */
    this.field= field;



    /**
     *
     * @property {string}
     */
    this.queryString= null;

    /**
     *
     * @property {jsDataQuery}
     */
    this.query= null;

    /**
     * Columns present
     * @property {Set<string>} queryCols
     */
    this.queryCols  =  new Set();
}

OneSubst.prototype = {
    constructor: OneSubst,

    /**
     * Sets the first query and puts the hash in a property
     *  @param {jsDataQuery} query
     */
    setQuery : function (query){
        this.queryString = query.toString();
        this.query = query;
    },
    /**
     *
     * @param {string[]}cols
     */
    setCols : function (cols){
        cols.forEach(c => this.queryCols.add(c));
    },


};

/**
 * Groups a set of OneSubst
 * @class
 * @param {string} tableName
 * @constructor
 */
function SubstGroup(tableName){
    /**
     * @property {string} tableName
     */
    this.tableName= tableName;

    /**
     * @property {jsDataQuery[]} query
     */
    this.query  =  [];

    /**
     * @property {Set<string>} querySet
     */
    this.querySet  =  new Set();

    /**
     * @property {Set<string>} queryCols
     */
    this.queryCols  =  new Set();

    this.querySize=0;


    /**
     * @property {ObjectRow[]} data
     */
    this.data = null;
}


SubstGroup.prototype = {
    constructor: SubstGroup,

    /**
     * adds a new query if it is on same table and it is not already present
     * @param {OneSubst} subst
     * @param {int} maxQueryLength
     * @return {boolean} true if could merge
     */
    mergeQuery: function (subst,maxQueryLength){
        if(subst.error){
            return false;
        }
        if (subst.tableName!== this.tableName){
            return false;
        }

        if (this.querySet.has(subst.queryString)){
            this.queryCols.add(subst.field);
            return true;
        }

        if (this.querySize>=maxQueryLength){
            return  false;
        }
        this.querySet.add(subst.queryString);
        if (this.querySize>0){
            this.querySize+=5;
        }
        this.querySize+= subst.queryString.length;
        this.query.push(subst.query);
        subst.queryCols.forEach(c => this.queryCols.add(c));
        this.queryCols.add(subst.field);
        return true;
    }
};

/**
 * Represents a changed row with related information to display a message
 * @class
 * @param {int} nChecks number of checks  applied to the row
 * @param {DataRow} r   DataRow being changed (added/modified/deleted)
 * @param {ObjectRow[]} audits  rows from auditcheckview related to this change
 * @param {string} varName  variable name used in stored procedure invocation
 * @param {string} colName  column name that will receive the result
 * @constructor
 */
function RowChange(nChecks,r,audits,varName,colName){
    this.nChecks = nChecks;

    /**
     * changed DataRow
     * @property {DataRow} r
     */
    this.r=r;

    /**
     * @property {string} tableName
     */
    this.tableName = r.table.name;

    this.audits=audits;
    this.varName = varName;
    this.colName = colName;


    /* {{DataRow}} dictionary of related rows*/
    this.related = {};

    this.hasBeenScanned = new Set();

}


RowChange.prototype = {
    constructor: RowChange
};


/**
 * Get a DataRow related to the RowChange, in a given tablename
 * @param {string} tableName
 * @return {DataRow|undefined}
 */
RowChange.prototype.getRelated = function(tableName){
    let realName = this.r.table.tableForWriting();
    if (realName=== tableName){
        return this.r;
    }
    let related = this.related[tableName];
    if (related){
        return  related;
    }
    this.searchRelated(tableName);
    return  this.related[tableName];
};

/**
 * Fills related searching the specified table
 * @param {string} tableName
 * @return {*}
 */
RowChange.prototype.searchRelated = function(tableName){
    if (this.hasBeenScanned.has(tableName)){
        return;
    }
    this.hasBeenScanned.add(tableName);

    let r=this._getRelatedRow(tableName);
    if(r){
        this.related[tableName]=r.getRow();
    }
};


/**
 * Searches a row in parent/child tables
 * @param {string} relatedTableName
 * @return {ObjectRow|null}
 */
RowChange.prototype._getRelatedRowExactTable= function (relatedTableName) {
    let related = this.r.getChildInTable(relatedTableName);
    if (related.length === 1) {
        return related[0];
    }
    related = this.r.getParentsInTable(relatedTableName);
    if (related.length === 1) {
        return related[0];
    }
    return null;
};

/**
 * Searches a row in parent/child tables
 * @param {string} relatedTableName
 * @return {ObjectRow|null}
 */
RowChange.prototype._getRelatedRow= function (relatedTableName) {
    let exact = this._getRelatedRowExactTable(relatedTableName);
    if (exact){
        return exact;
    }
    let result=null;
    let that=this;
    Object.values(this.r.table.dataset.tables).some(t=>{
        if (t.tableForWriting()===relatedTableName && t.name!== relatedTableName){
            result=that._getRelatedRowExactTable(t.name);
            return  (result!==null);
        }
    });
    return result;
};


/**BusinessPostData
 * A class able to save data invoking business rules
 * @class BusinessPostData
 * @augments PostData
 * @method PostData
 * @param {Context} context
 * @constructor
 */
function BusinessPostData (context){
    PostData.apply(this);

    /* {Context} */
    this.context = context;

    /*{BusinessLogic}*/
    this.businessLogic= null;

}

BusinessPostData.prototype =  _.extend(
    new PostData(),
    {
        constructor: BusinessPostData,
        superClass: PostData.prototype,

        createBusinessLogicResult: function (){
            return new BusinessLogicResult();
        },

        /***
         *
         * @param ds
         * @return {Promise<SinglePostData>}
         */
        init : function ( ds) {
            return this.superClass.init.call(this, ds, this.context);
        },


        /**
         * This is meant to be replaced or overridden in derived classes
         * @param {Context} context
         * @param {ObjectRow[]} rowChanges
         * @return {BusinessLogicResult}
         */
        getBusinessLogic : function (context, rowChanges){
            let bl= new BusinessLogic(context, rowChanges);
            return bl.auditsPromise
                .then(()=>{
                    return bl;
                });
        }

    }
);



module.exports = {
    BusinessPostData:BusinessPostData,
    BusinessLogic: BusinessLogic,
    BusinessMessage:BusinessMessage,
    OneSubst:OneSubst,
    SubstGroup:  SubstGroup,

    _RowChange:RowChange
};