Source: src/jsPostData.js

/*globals ObjectRow,Environment,sqlFun,AutoIncrementColumn,DataRowState */
/**
 * Manages data storing
 * @module PostData
 */

/* jshint -W116 */
const dsSpace = require('./../client/components/metadata/jsDataSet');
const DataSet = dsSpace.DataSet;

/**
 * {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 dataRowState = dsSpace.dataRowState;

const _ = require('lodash');

/* {Deferred} */
const Deferred = require("JQDeferred");

const async = require('async');

/**
 * Manages a cache of select max done in a transaction
 * @class MaxCacher
 * @method MaxCacher
 * @param {DataAccess} conn
 * @param {Environment} environment
 * @constructor
 */
function MaxCacher(conn,environment){
  /**
   * Cache of all calculated max
   * @property {object} allMax
   * @private
   */
  this.allMax = {};

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

  /* {SqlFormatter} */
  this.formatter = conn.getFormatter();

  /* {Environment} */
  this.environment= environment;
}
MaxCacher.prototype = {
  constructor: MaxCacher
};

/**
 * Gets a hash to retrieve a select max
 * @method getHash
 * @private
 * @param {string} table
 * @param {string} column
 * @param {sqlFun} filter
 * @param {sqlFun} expr
 */
MaxCacher.prototype.getHash = function(table, column,filter,expr){
  return table+'§'+ column+'§'+this.formatter.conditionToSql(filter, this.environment)+'§'+
    this.formatter.toSql(expr,this.environment);
};


/**
 * Get the max for an expression eventually getting it from cache or giving null if reasonably there is no row
 *  on that match the filter
 *  If there is no selector, then the result can be taken from cache if a query with same parameters has already
 *   be done.
 * @method getMax
 * @param {ObjectRow} r objectRow for which evaluate the max
 * @param {string} column  column to evaluate
 * @param {string[]} selectors //selector fields for the calculation
 * @param {sqlFun} filter //filter to apply
 * @param {sqlFun} expr //expression to evaluate
 * @return {Promise<int>}
 */
MaxCacher.prototype.getMax = function (r, column, selectors, filter, expr) {
  const def = Deferred(),
      that = this,
      table = r.getRow().table,
      k = this.getHash(table.name, column, filter, expr);
  if (this.allMax[k]){
    def.resolve(this.allMax[k]);
    return def.promise();
  }
  const keySelectors = _.intersection(selectors, table.key()); //fields both key and selector
  if (keySelectors.length > 0) {
    if (_.find(table.dataset.relationsByChild[table.name], function (parentRel) {
      const parentRows = parentRel.getParents(r);
      if (parentRows.length !== 1) {
        return false;
      }
      const parentRow = parentRows[0];
      if (parentRow.getRow().state !== dataRowState.added) {
        return false;
      }
      return ( _(_.intersection(parentRel.childCols, keySelectors))
        .map(function (col) {
          return parentRel.parentCols[_.indexOf(parentRel.childCols, col)];
        })
        .find(function (parentCol) {
          return table.dataset.tables[parentRel.parentTable].autoIncrement(parentCol) !== undefined;
        }) !== undefined);

    })) {
      //if such a relation is found, return null to mean that there is no row on db satisfying that condition,
      // so the max can be evaluated basing on in-memory informations (for example a cached value)
      def.resolve(null);
      return def.promise();
    }
  }
  this.conn.readSingleValue({tableName: table.name, expr:expr, filter:filter, environment:this.environment})
    .done(function(res){
      if (res===undefined){
        res=null;
      }
      that.allMax[k]=res; //put the result in cache
      def.resolve(res);
    })
    .fail(function(err){
      def.reject(err);
    });
  return def.promise();
};

/**
 *
 * @param {ObjectRow} r
 * @param {string} column
 * @param {sqlFun} filter
 * @param {sqlFun} expr
 * @param {object} value
 */
MaxCacher.prototype.setMax = function (r, column, filter, expr, value) {
  this.allMax[this.getHash(r.getRow().table.name, column, filter, expr)] = value;
};


/**
 * Saves a single DataSet using a given DataAccess
 * @class PostData
 * @method PostData
 * @constructor
 */
function SinglePostData(){
  /*{ObjectRows[]}*/
  this.changedRows = [];

}
SinglePostData.prototype = {
  constructor: SinglePostData
};


/**
 * This function  is called before and after applying changes to db.
 *  The first time is called with post=false and the second time with post=true.
 *  Should give a list of messages to return to client. If it does return an empty array, the transaction is committed.
 *  If it does return some rows, those are merged and returned to client, and transaction is roll-backed.
 *  if canIgnore is false in the first call, the procedure terminates and no data is written at all
 *  This is meant to be replaced or overridden in derived classes
 * @param {DataSet} ds
 * @param {Context} context
 * @return {Promise<SinglePostData>}
 */
SinglePostData.prototype.init = function ( ds,context){
  this.ds = ds;
  this.rowChanges = this.getChanges(ds);
  this.conn= context.dataAccess;
  this.security = context.security;
  this.environment = context.environment;
  if (this.conn) {
    this.sqlConn = this.conn.sqlConn;
  }
  return new Deferred().resolve(this).promise();
};



/**
 * Compose a list of all tables that satisfy the checkFunction
 * checkFunction is called with 2 parameters: table, list where
 * table is the current evaluated DataTable,
 * list is a hash of tables already added to list, where the key is the name of the DataTable
 * Elements are added to list when checkFunction returns true
 * @param {DataSet} d
 * @param checkFunction
 * @return {DataTable[]}
 */
SinglePostData.prototype.sortTables = function(d, checkFunction){
  const result = [];
  let keep = true;
  let added = new Set();

  while (keep){
    keep = false;
    for(const tName in d.tables){
      if (d.tables.hasOwnProperty(tName)){
        let t = d.tables[tName];
        if (added.has(t.name)|| t.isTemporaryTable){
          continue;
        }
        if (checkFunction(t, added)) {
          result.push(t);
          added.add(t.name);
          keep = true;
        }
      }
    }
  }
  return result;

};


/**
 * Adds all Rows (of every Tables referred by "Tables")with a specified State to result
 * @param {DataTable[]} tables
 * @param {DataRowState} state
 * @return {ObjectRow[]}
 */
SinglePostData.prototype.tableOps =function ( tables,state){
  /*{ObjectRow[]}*/
  let result=[];
  for(let i=0;i<tables.length;i++){
    let t = tables[i];
    if (t.isTemporaryTable){
      continue;
    }
    let sorting= t.postingorder;
    if (state===dataRowState.deleted || sorting===undefined){
      t.rows
          .filter((r) => r.getRow().state === state)
          .forEach((r)=>result.push(r));
      continue;
    }

    return t.getSortedRows(
        t.rows.filter((r) => r.getRow().state === state),
        sorting)
        .forEach((r)=>result.push(r));
  }
  return  result;
};

// /**
//  * Returns modified rows of given tables filtering by rowState
//  * @method getTableOps
//  * @param {DataTable[]} tables
//  * @param {DataRowState|string} rowState
//  */
// function getTableOps(tables, rowState) {
//   return _.reduce(tables, function (list, t) {
//     let res = _.filter(t.rows,
//         function (r) {
//           return (r.getRow().state === rowState);
//         }
//     );
//     if (t.postingOrder) {
//       res = _.sortBy(res, t.postingOrder);
//     }
//     return list.concat(res);
//   }, []);
// }

/**
 *  Evaluates the list of the changes to apply to the DataBase, in the order they should be "reasonably" done:
 *  All operation on None
 *  Deletes on  Child first, Parent last
 *  Insert on Parent first, Child last
 *  Updates on Parent first, Child last
 *  If a DataTable has a 'postingOrder' property, it is used to sort rows of that table
 * @param {DataSet} ds
 * @return {ObjectRow[]}
 */
SinglePostData.prototype.getChanges = function(ds) {
  const parentFirst = this.sortTables(ds, this.checkNotChild),
      childFirst = this.sortTables(ds, this.checkNotParent);
  return this.tableOps(childFirst, dataRowState.deleted).concat(
      this.tableOps(parentFirst, dataRowState.added),
      this.tableOps(parentFirst, dataRowState.modified)
  );
};


/**
 * check if T is not a child of a table, except those parent in toIgnore set and self and those without changes
 * @param {DataTable} t
 * @param {Set<string>} toIgnore
 */
SinglePostData.prototype.checkNotChild =  function(t, toIgnore ) {
  let foundRel=t.parentRelations().find(rel=>{
    if (rel.parentTable=== t.name || toIgnore.has(rel.parentTable)){
      return false;
    }
    let parentTable=t.dataset.tables[rel.parentTable];
    if (parentTable.isTemporaryTable){
      return  false;
    }
    //external function will return false if there is some not-unchanged row in parentTable
    //if parentTable has  no change, relation is skipped
    //if parentTable has changes, this is actually a dependent child of that
    return parentTable.hasChanges();
  });
  return  (foundRel===undefined);
};

/**
 * check if T is not a parent of a table, except those child in toIgnore set
 * @param {DataTable} t
 * @param {Set<string>} toIgnore
 */
SinglePostData.prototype.checkNotParent =  function(t, toIgnore ) {
  let foundRel=t.childRelations().find(rel=>{
    if (rel.childTable=== t.name || toIgnore.has(rel.childTable)){
      return false;
    }
    let childTable=t.dataset.tables[rel.childTable];
    if (childTable.isTemporaryTable){
      return  false;
    }
    //external function will return false if there is some child row
    //return childTable.hasChanges();
    return childTable.rows.length>0;
  });
  return  (foundRel===undefined);
};



/**
 * Function called with a DataAccess and changedRows as parameters in order to do eventual
 *   db transaction logging
 * This is meant to be replaced or overridden in derived classes
 * @param {DataAccess} conn transaction connection
 * @return {Deferred}
 */
SinglePostData.prototype.log = function (conn){
  return new Deferred().resolve(true).promise();
};



/**
 * function called in order to do eventual additionally updates when it's sure the transaction
 *  is to be committed, i.e., no check have been raised. This function must
 *   return an array of checks. If it is empty the transaction is committed otherwise is rollbacked
 *  This is meant to be replaced or overridden in derived classes
 * @param {DataAccess} conn  transaction connection
 * @param {BusinessLogicResult} results
 * @return {Promise<bool>}
 */
SinglePostData.prototype.doUpdate = function (conn,  results){
  return new Deferred().resolve(true).promise();
};


/**
 * Calculates all autoincrement property of a DataRow r
 * @param {ObjectRow} r
 * @returns {promise} return true if NO custom autoincrement was found.
 * when a custom autoincrement is found, rows are saved one at a time
 */
SinglePostData.prototype.calcAllAutoId = function(r) {
  const that = this,
      def = Deferred(),
      table = r.getRow().table;
  if (r.getRow().state !== dataRowState.added){
    def.resolve(true);
    return def.promise();
  }
  Deferred.when.apply(Deferred,
      _.map(table.getAutoIncrementColumns(), function (col) {
        return that.calcAutoId(r, table.autoIncrement(col));
      }))
      .done(function(){
        const arr = Array.prototype.slice.call(arguments);
        def.resolve(_.every(arr,function(el){return el;}));
      });
  return def.promise();
};



/**
 * Gets an array of changed rows and returns a sequence of sql command that does the post.
 * @method getSqlStatements
 * @param {DataRow[]}changedRows
 * @param {OptimisticLocking} optimisticLocking
 * @returns {Deferred} Deferred is notified with n sets of (rows,sql) where sql is the sql command used to
 *   save rows. This is useful if some error is returned, to evaluate messages.
 *   Saving stops as soon as an error occurs.
 */
SinglePostData.prototype.getSqlStatements = function (changedRows, optimisticLocking){
  const def = Deferred(),
      that = this;
  let internalIndex = 0,
      rows = [],
      sql,
      failed = false;

  /**
   * Evaluates a sql command to save a row and sends
   * @param {ObjectRow} preparedRow
   * @param {boolean} hasCustomAutoincrement
   */
  function sqlMerge(preparedRow, hasCustomAutoincrement){
    if (failed){
      return;
    }
    rows.push(preparedRow);
    const cmd = that.conn.getPostCommand(preparedRow, optimisticLocking, that.environment),
        errCmd = that.sqlConn.giveErrorNumberDataWasNotWritten(internalIndex);
    if (sql) { //sqlConn is a SqlDriver and knows how to concatenate single commands
      sql = that.sqlConn.appendCommands([sql, cmd, errCmd]);
    } else {
      sql = that.sqlConn.appendCommands([cmd, errCmd]);
    }
    if (sql.length > that.sqlSizeLimit || hasCustomAutoincrement){
      def.notify(rows,sql);
      internalIndex = 0;
      rows = [];
      sql = '';
    } else {
      internalIndex += 1;
    }
  }

  async.eachSeries(changedRows, function(r,callback){
        that.calcAllAutoId(r)
            .done(function(noCustomAutoincrementFound){
              sqlMerge(r, !noCustomAutoincrementFound);
              callback(null);
            })
            .fail(function(err){
              def.reject(err);
              failed=true;
              callback(err);
            });
      },
      function(err){
        if (err) {
          failed = true;
          def.reject(err);
        }
        if (rows.length>0){
          def.notify(rows, sql);
        }
        def.resolve();
      });

  return def.promise();
};


/**
 * Reads again a row from db
 * @method reselect
 * @param {ObjectRow} row
 * @returns {*}
 */
SinglePostData.prototype.reselect = function(row) {
  const table = row.getRow().table;
  row.getRow().rejectChanges();
  return this.conn.selectIntoTable({
    table: table,
    filter: table.keyFilter(row),
    environment: this.environment
  });

};

/**
 * Runs a sequence of db commands in order to save an array of rows
 * @method physicalPostBatch
 * @param {DataAccess} conn
 * @param {OptimisticLocking} optimisticLocking
 * @returns {*} Resolved promise if all ok, rejected promise if errors
 */
SinglePostData.prototype.physicalPostBatch = function(conn, optimisticLocking){
  const def = Deferred(),
      that = this;
  let sqlCmdLaunched = 0,
      sqlCmdRun = 0,
      endOfCmdReached = false,
      failed = false,
      changedRows = this.rowChanges;
  //assume the OptimisticLocking present in the DataSet as default for optimisticLocking
  optimisticLocking = optimisticLocking || this.ds.optimisticLocking;
  function sqlCmdRunner(rows,sql){
    const sqlComplete = that.sqlConn.appendCommands([sql, that.sqlConn.giveConstant(-1)]);
    sqlCmdLaunched += 1;
    that.conn.runCmd(sqlComplete)
        .done(function(res){

          if (res === -1){
            sqlCmdRun += 1;
            if (endOfCmdReached){
              def.resolve();
            }
            return;
          }
          if (res <0 || res >= rows.length){
            def.reject('internal error running command ' + sqlComplete);
            return;
          }

          const row = rows[res];
          const sqlErrorCmd = that.conn.getPostCommand(row, optimisticLocking, that.environment);
          if (row.getRow().state === dataRowState.modified){
            that.reselect(row)
                .done(function(){
                  def.reject('error running command' + sqlErrorCmd);
                })
                .fail(function(){
                  def.reject('error running command' + sqlErrorCmd);
                });
          } else {
            def.reject('error running command' + sqlErrorCmd);
          }
        })
        .fail(function(err){
          failed=true;
          def.reject('Got Error:'+err+' running '+ sqlComplete.substr(0,500));
        });

  }

  //sets optimistic locking fields
  changedRows.forEach(r =>   optimisticLocking.prepareForPosting(r,that.environment));

  if (that.security) {
    changedRows.forEach(r => {
      if (that.security.canPost(r.row, that.environment)) {
        def.reject("Operation on table "+r.table.name+" is forbidden");
        failed=true;
      }
    });
  }
  if (failed) return;

  that.getSqlStatements(changedRows, optimisticLocking)
      .progress(function(rows, sql){
        if (failed) {
          return;
        }
        sqlCmdRunner(rows,sql);
      })
      .done(function(sqlCmdArray){
        if (failed) {
          return;
        }
        endOfCmdReached=true;
        if (sqlCmdLaunched=== sqlCmdRun){
          def.resolve();
        }
      })
      .fail(function(err){
        if (failed) {
          return;
        }
        def.reject(err);
      });


  return def.promise();
};


/**
 * Get a sql command to select all changed rows that belongs to view tables. Views are identified as tables
 *  having a name different from tableForWriting. It's necessary that they have a key to let this method
 *  consider them.
 * @method  getSelectAllViews
 * @private
 * @param {Array.<ObjectRow>} changedRows
 * @returns {Array.<Select>}
 */
SinglePostData.prototype.getSelectAllViews = function (changedRows) {
  return _.reduce(changedRows, function (list, r) {
    if (!r.getRow())return list;
    const table = r.getRow().table;
    if (table.name === table.tableForWriting()) {
      return list;
    }
    if (table.key().length === 0) {
      return list;
    }
    list.push(new Select(_.keys(r))
        .from(table.tableForReading())
        .intoTable(table.name)
        .multiCompare(table.keyFilter(r))
    );
    return list;
  }, []);
};


/**
 * Read all changed view rows from db
 * @method reselectAllViews
 * @returns {*}
 */
SinglePostData.prototype.reselectAllViews = function(){
  const def = Deferred();
  let selectList;

  if (this.rowChanges.length === 0){
    def.resolve();
    return def.promise();
  }
  //QUI DEVO CORREGGERE, non sono tutte le righe da rileggere ma solo quelle relative a viste e che hanno
  // campi particolari, fare riferimento a ReselectAllViews del c#
  selectList = this.getSelectAllViews(this.rowChanges);

  this.conn.mergeMultiSelect(selectList, this.ds, this.environment)
      .done(function(data){
        def.resolve();
      })
      .fail(function(err){
        def.resolve();
      });
  return def.promise();
};



/**
 * Calculates an autoincrement column checking that there is no other rows with that key in table
 * @method calcAutoId
 * @param {ObjectRow} r
 * @param {AutoIncrementColumn} autoIncrementProperty ({DataRow} r, {string} columnName, {DataAccess} conn}
 * @return {promise} //resolves true if it was a regular auto increment fields, false if it was a custom one
 */
SinglePostData.prototype.calcAutoId = function(r, autoIncrementProperty) {
  const that = this,
      def = Deferred(),
      table = r.getRow().table,
      field = autoIncrementProperty.columnName;

  ///TODO: verificare come operare l'impostazione: se lo fa il client, al momento sicuramente non è ricevuta dal server
  if (autoIncrementProperty.customFunction){
    autoIncrementProperty.customFunction(r, field, this.conn)
        .done(function(res){
          table.safeAssign(r,field,res);
          def.resolve(false);
        });
    return def.promise();
  }

  const selector = autoIncrementProperty.getSelector(r),
      fieldExpr = autoIncrementProperty.getExpression(r),
      prefix = autoIncrementProperty.getPrefix(r);


  this.maxCacher.getMax(r, field, autoIncrementProperty.selector, selector, fieldExpr)
      .then(function (res) {
        let foundID, newID;
        if (res === null || res === undefined) {
          newID = 1;
        } else {
          newID = res+1;
        }
        that.maxCacher.setMax(r, field, selector, fieldExpr, newID);
        if (! autoIncrementProperty.isNumber){
          newID = newID.toString();
          while (newID.length < autoIncrementProperty.idLen) {
            newID = '0' + newID;
          }
          newID = prefix + newID;
        }
        table.safeAssign(r, field, newID);
        def.resolve(true);
      });
  return def.promise();
};



/**
 * Saves one or more DataSet using a given DataAccess
 * @class PostData
 * @method PostData
 * @constructor
 */
function PostData(){
  /* {SinglePostData} */
  this.allPost = [];
 /* IInnerPoster */
 this.innerPostingClass = null;
 /* OptimisticLocking */
 this.defaultOptimistickLocking=null;
}



PostData.prototype = {
  constructor: PostData,
  /**
   *
   * @param ds
   * @param conn
   * @return {SinglePostData}
   */
  createSingleDataSetPost: function(ds,conn){
    return new SinglePostData();
  },
  /**
   * Sets a default for optimistic locking
   * @param {OptimisticLocking} locking
   */
  setOptimisticLocking(locking){
   this.defaultOptimistickLocking = locking;
  }
};


PostData.prototype.createBusinessLogicResult = function (){
  return new BusinessLogicResult();
};


/**
 *
 * @param {DataSet} ds
 * @param {Context} context
 * @return {Promise<SinglePostData>}
 */
PostData.prototype.init = function ( ds,context){
  if (!this.conn){
    this.conn = context.dataAccess;
  }
  let p =this.createSingleDataSetPost();
  this.allPost.push(p);
  this.context = context;

  return  p.init(ds, context)
      .then(()=> this.getBusinessLogic(context, p.rowChanges))
      .then(bl=> {
        p.businessLogic = bl;
        return p;
      });
};

/**
 * @private
 * @return {boolean}
 */
PostData.prototype.someChange = function (){
  return this.allPost.some(p => p.rowChanges.length>0);
};

/**
 * Check if this PostClass is run-time nested insider another one
 * @param {DataSet} ds
 * @param {IInnerPoster} innerPoster
 * @return {boolean}
 */
PostData.prototype.setInnerPosting = function (ds, innerPoster){
  this.innerPostingClass = innerPoster;
  this.innerPostingDataSet = ds;
};

/**
 * Check if this PostClass is run-time nested insider another one
 * @return {boolean}
 */
PostData.prototype.isInnerPoster = function (){
  return this._isInnerPoster;
};

/**
 * Establish that this PostClass is run-time nested insider another one
 */
PostData.prototype.setAsInnerPoster = function (){
  this._isInnerPoster = true;
};







/**
 * Simple check result
 * @class BasicMessage
 * @param {string} msg
 * @param {boolean} canIgnore
 */
function BasicMessage(msg,canIgnore){
  /**
   * @property {boolean} canIgnore
   */
  this.canIgnore=canIgnore;

  /**
   * @property {string} msg
   */
  this.msg=msg;
}

/**
 * @method setMessage
 * Sets the message of the rule
 * @return {*}
 */
BasicMessage.prototype.setMessage = function (msg){
  this.msg=msg;
};

/**
 * @method getMessage
 * Gets the message of the rule
 * @return {string}
 */
BasicMessage.prototype.getMessage = function (){
  return this.msg;
};


/**
 * Gets an identifier for this message, uses to suppress duplicates messages
 * @return {string|*}
 */
BasicMessage.prototype.getId = function (){
  return this.msg;
};


function BusinessLogicResult(){
  /* {boolean} a message can be ignored if it is a warning*/
  this.canIgnore=true;

  this.autoIgnore = false;

  /* {boolean} if true is a post check*/
  this.post = false;

  /* BasicMessage[] */
  this.checks=[];

  /**
   * Previously ignored warnings
   * {Set<string>}
   */
  this.hashIgnored =  new Set();

  /**
   * Added errors (can't be ignored)
   * {Set<string>}
   */
  this.hashError =  new Set();
}


/**
 * Adds a message to the collection, returns the added message
 * @param {BasicMessage} check
 * @return {BasicMessage}
 * */
BusinessLogicResult.prototype.addMessage = function (check){
  let id = check.getId();
  if (check.canIgnore){
    if (!this.autoIgnore) {
      if (!this.hashIgnored.has(id)) {
        this.hashIgnored.add(id);
        this.checks.push(check);
      }
    }
  }
  else {
    this.canIgnore=false;
    if (!this.hashError.has(id)){
      this.hashError.add(id);
      this.checks.push(check);
    }
  }

  return check;
};

/**
 * Adds an error to the message collection, returns the created message
 * @param {string} message
 * @param {boolean} post
 * @return {BasicMessage}
 */
BusinessLogicResult.prototype.addError = function (message,post){
  return  this.addMessage(new BasicMessage(message,false));
};
/**
 * Adds a warning to the message collection, returns the created message
 * @param {string} message
 * @param {boolean} post
 * @return {BasicMessage}
 */
BusinessLogicResult.prototype.addWarning = function (message,post){
  let m=  this.addMessage(new BasicMessage(message,true));
  m.post = post;
  return  m;
};


/**
 * Adds a db error
 * @param {string} message
 * @param {boolean} post
 */
BusinessLogicResult.prototype.addDbError = function (message,post){
  const check = new BasicMessage(message,false);
  this.checks.push( check);
  return check;
};



/**
 * Merges messages with existing ones
 * @param {BusinessLogicResult} messages
 * @returns {boolean} true if any new message was added to collection
 */
BusinessLogicResult.prototype.mergeMessages = function (messages){
  let that=this;  //to avoid eslint messages
  let nChecks = this.checks.length;
  messages.checks.forEach(/* {BasicMessage} */  m =>{
    that.addMessage(m);
  });
  if (!messages.canIgnore) {
    this.canIgnore=false;
  }
  return nChecks!== this.checks.length;
};



function IBusinessLogic(){

}

IBusinessLogic.prototype = {
  constructor: IBusinessLogic,
  /**
   * @param {BusinessLogicResult} result
   * @param {DataAccess} conn transaction connection
   * @param {boolean} post
   */
  getChecks:function (result, conn,post){
    return  new Deferred().resolve(result).promise();
  }
};

/**
 * This is meant to be replaced or overridden in derived classes
 * @param {Context} context
 * @param {ObjectRow[]} rowChanges
 * @return {Promise<IBusinessLogic>}
 */
PostData.prototype.getBusinessLogic = function (context, rowChanges){
  let bl= new IBusinessLogic();

  return new Deferred().resolve(bl).promise();
};

/**
 * Builds a chained function, chaining each the Deferred function with "then"
 * @param  {Array.<Deferred>} tasks each task is a function that returns a deferred. Not the deferred itself!
 * @return {Promise}
 */
function promiseWaterfall(tasks) {
  let f = Deferred().resolve(true).promise();
  // concateno con then ogni deferred dell'array di input
  _.forEach(
      tasks,
      function(def) {
        f = f.then(()=>{
          try {
            return def();
          }
          catch (err){
            return  Deferred().reject(err).promise();
          }

        } );
      });

  return f;
}

/**
 *
 * @param {function[]} tasks
 */
function promiseParallel(tasks) {
  Deferred.when.apply(Deferred, tasks.map(function (task) {
    return task();
  }));
}


/**
 * This function  is called before and after applying changes to db.
 *  The first time is called with post=false and the second time with post=true.
 *  Should give a list of messages to return to client. If it does return an empty array, the transaction is committed.
 *  If it does return some rows, those are merged and returned to client, and transaction is roll-backed.
 *  if canIgnore is false in the first call, the procedure terminates and no data is written at all
 *  This is meant to be replaced or overridden in derived classes
 * @param {DataAccess} conn
 * @param {boolean} post  True if post check otherwise precheck are requested
 * @return {Promise<BusinessLogicResult>}
 */
PostData.prototype.getChecks = function (conn, post){
  /* BusinessLogicResult */
  let  result = this.createBusinessLogicResult();
  //Merges all checks coming from singlePosts

  let /* Deferred[] */ allChecks = _.map(this.allPost, (p)=> ()=>p.businessLogic.getChecks(result,conn,post));

  return promiseWaterfall(allChecks).
        then(()=>{
          return result;
        }
  );

};


PostData.prototype.clearMaxCache = function (conn){
  this.allPost.forEach(p=>{
    // Resets all evaluated cached max
    p.maxCacher = new MaxCacher(conn, p.environment);
  });
};

/**
 * Executes all data posting
 * @param {DataAccess} conn
 * @param {OptimisticLocking} locking
 * @returns {Promise} promise fails on errors
 */
PostData.prototype.doAllPhisicalPostBatch = function (conn, locking){
  let allPostingFn = _.map(this.allPost, (p)=>  ()=>p.physicalPostBatch(conn, locking));
  return promiseWaterfall(allPostingFn);
};


/**
 * log all operations made
 * @param {DataAccess} conn
 * @returns {Promise} promise fails on errors
 */
PostData.prototype.doAllLog = function (conn){
  let allLoggingFn = _.map(this.allPost, (p)=> ()=>p.log(conn, p.rowChanges) );
  return promiseWaterfall(allLoggingFn);
};


/**
 * Executes all post-saving updates
 * @param {DataAccess} conn
 * @param {BusinessLogicResult} result
 * @returns {Promise} promise fails on errors
 */
PostData.prototype.doAllUpdate = function (conn, result){
  let allUpdateFn = _.map(this.allPost, (p)=> ()=>p.doUpdate(conn, result));
  return promiseWaterfall(allUpdateFn).
    then(()=>result);
};

/**
 * Read again view data from database
 * @param {DataAccess} conn
 * @return {Promise}
 */
PostData.prototype.reselectAllViewsAndAcceptChanges = function(conn){
  let that=this;
  let allReselectFn = _.map(this.allPost, (p)=>()=>{
    return  p.reselectAllViews(conn).then(()=>{
      _.forEach(p.rowChanges, /*ObjectRow */r=> r.getRow().acceptChanges() );
    });
  });
  if (this.innerPostingClass){
    allReselectFn.push(()=>that.innerPostingClass.reselectAllViewsAndAcceptChanges(conn));
  }

  return promiseWaterfall(allReselectFn);
};

/**
 * @returns {Array.<dataSetName:string,  changes:Array.<DataRow>>}
 */
PostData.prototype.getAllChanges = function(){

  let res =   _.map(this.allPost,p=>{return {datasetName: p.ds.name, changes : p.rowChanges};});
  if (res.length===1)return  res[0].changes;
  return  res;

};


/**
 * Saves a dataSet and return empty list if successful or a list of messages if they have to be verified before
 *  effectively committing changes. If successfull, also returns saved DataSet
 * @method doPost
 * @param {object} options
 * @param {string} [options.isolationLevel = DataAccess.isolationLevels.readCommitted]
 * @param {OptimisticLocking} [options.optimisticLocking]
 * @param {BusinessLogicResult} [options.previousRules]
 * @return  { Promise<{canIgnore:boolean, checks:BasicMessage[], data:DataSet}> }
 */
PostData.prototype.doPost = function(options) {
  const opt = options || {},
      that = this;
  let opened = false,
      tranOpen = false,
      terminated = false;
  /* {Deferred}  */
  const def = Deferred();
  _.defaults(opt, {
    isolationLevel: isolationLevels.readCommitted,
    optimisticLocking: this.defaultOptimistickLocking
  });
  function cleanResult(res){
    let r= _.pick(res,["canIgnore","checks"]);
    if (res.data){
      r.data= res.data;
      r.checks=[];
    }
    return r;
  }
  /*{BusinessLogicResult}*/
  let result = opt.previousRules || this.createBusinessLogicResult();
  if (!result.canIgnore) return def.resolve(cleanResult(result)).promise();


  if (!this.someChange()){
    def.resolve(cleanResult(result)); //default response on unchanged dataset
    return def.promise();
  }


  function appendArray(array1, array2) {
    array1.push.apply(array1, array2);
  }

  this.clearMaxCache(this.conn);

  /**
   * Invoke a method if it is not inner posting
   * @param {string} method
   * @return {*}
   */
  function optionalMethod(method){
    if (that.isInnerPoster()){
      return Deferred().resolve(false);
    }
    return  that.conn[method]()
        .then(()=>true);
  }


  /**
   * Resolve the promise with result, and do the necessary cleanup: rollback transaction if a transaction is open,
   *  and close connection if the connection is open. If further errors arise, add them to checks as dbErrors
   * @method resolve
   * @returns {Deferred}
   */
  function resolve() {
    terminated = true;
    //If there is an open transaction, it means that there has been an error
    if (tranOpen) {
      optionalMethod("rollback")
          .then(()=> that.recursiveCallAfterPost(false))
          .then(()=>optionalMethod("close"))
          .then(()=>def.resolve(cleanResult(result)))
          .fail(function (err) {
            optionalMethod("close")
                .done(function () {
                  result.addDbError(err,true);
                  def.resolve(cleanResult(result));
                });
          });
      return def.promise();
    }
    if (opened) {
      optionalMethod("close")
          .done(function () {
            def.resolve(cleanResult(result));
          });
    } else {
      def.resolve(cleanResult(result));
    }
    return def.promise();
  }
  let anyNewMessage=false;


  //open connection
  try {
    optionalMethod("open")
        .then(function () {
          opened= true;
          if (terminated) return;
          //begin transaction
          if (!that.isInnerPoster()) {
            //Only outer poster begins and rollbacks/commits transaction
            return that.conn.beginTransaction(opt.isolationLevel);
          }
          return  true;
        })
        .then(function () {
          tranOpen = true;
          // CALL PRE - CHECKS
          return that.getChecks( that.conn, false);
        })
        .then(function (/*BusinessLogicResult*/    preChecks) {
          anyNewMessage = result.mergeMessages(preChecks);
          if (result.canIgnore === false) {
            return resolve();
          }
        })
        .then(function () {
          if (terminated) return;
          return that.doAllPhisicalPostBatch(that.conn, opt.optimisticLocking);
        })
        .then(function () {
          if (terminated) return;
          return that.doAllLog(that.conn);
        })
        .then(function () {
          if (terminated) return;
          //CALL POST-CHECK
          return that.getChecks( that.conn, true);
        })
        .then(function (/* BusinessLogicResult */ postChecks) {
          if (terminated) return;
          let newMessages = result.mergeMessages(postChecks);
          anyNewMessage = anyNewMessage || newMessages;
          if (anyNewMessage) {
            return resolve(); //forces a rollback
          }

          return that.doAllUpdate(that.conn, that.createBusinessLogicResult());
        })
        .then(function (/*  {BusinessLogicResult} */ extChecks) {
          if (terminated) return;
          anyNewMessage = result.mergeMessages(extChecks);
          if (anyNewMessage) {
            return resolve(); //forces a rollback
          }
          if (!that.innerPostingClass) return;
          let prevRuleLen = result.checks.length;
          return that.innerPostingClass.init(that.innerPostingDataSet, that.context)
              .then(function () {
                return that.innerPostingClass.doPost(_.assign({}, opt, {previousRules: result}));
              })
              .then(
                  /**
                   * @param {boolean} res.canIgnore
                   * @param {BasicMessage[]} res.checks
                   * @param {DataSet} res.data
                   * @return {exports}
                   */
                  function (res) {
                    if (res.checks.length > prevRuleLen) return resolve();
              });
        })
        .then(function () {
          if (terminated) return;
          //all is fine so we can do a commit
          return optionalMethod("commit");
        })
        .then(function (res) {
          if (terminated) return;
          tranOpen = false;
          if (that.isInnerPoster()) return;
          return that.reselectAllViewsAndAcceptChanges();
        })
        .then(function () {
          if (terminated) return;
          if (that.isInnerPoster()) return;
          return that.recursiveCallAfterPost(true);
        })
        .then(function () {
          if (terminated) return;
          result.data = that.getAllChanges();
          resolve(); //closes connection and returns data
        })
        //any fail will be caught here
        .fail(function (err) {
          result.addDbError(err);
          resolve();
        });
  }
  catch (err){
    result.addDbError(err);
    resolve();
  }

  return def.promise();
};
/**
 *
 * @param {boolean} committed
 */
PostData.prototype.recursiveCallAfterPost= function (committed){
  let inner= this.innerPostingClass;
  if (!inner)return;
  let inners=[];
  while (inner){
    inners.push(inner);
    inner = inner.getInnerPostingClass();
  }
  let def  = _.map(inners, inner=> ()=> inner.afterPost(this.conn,committed));
  return promiseWaterfall(def);
};


function IInnerPoster(){
  /* PostData */
  this.p = new PostData(); //this can be another class in derived IInnerPoster class
}

IInnerPoster.prototype = {
  constructor: IInnerPoster,

  /**
   *
   * @param {DataSet} ds
   * @param {Context} context
   * @return {Promise<SinglePostData>}
   */
  init : function ( ds,context){
    this.p.setAsInnerPoster(); //set p as the inner poster
    return this.p.init(ds, context);
  },

  /**
   * Called after data has been committed or rolled back
   * @param {DataAccess} conn
   * @param {boolean} committed
   * @return {*}
   */
  afterPost: function(conn, committed){
    return Deferred().resolve();
  },

  /**
   * Same as PostData method
   * @param {DataAccess} conn
   * @return {Promise}
   */
  reselectAllViewsAndAcceptChanges: function(conn){
    if (this.p===null) return Deferred().resolve();
    return this.p.reselectAllViewsAndAcceptChanges(conn);
  },

  getInnerPostingClass: function (){
    return this.p.innerPostingClass;
  },

  /**
   * Saves a dataSet and return empty list if successful or a list of messages if they have to be verified before
   *  effectively committing changes.
   * @method doPost
   * @param {object} options
   * @param {string} [options.isolationLevel = DataAccess.isolationLevels.readCommitted]
   * @param {OptimisticLocking} [options.optimisticLocking]
   * @param {BusinessLogicResult} [options.previousRules]
   * @return { {canIgnore:boolean, checks:BasicMessage[], data:DataSet} }
   */
  doPost: function (options){
    return this.p.doPost(options);
  }

};


PostData.prototype.sqlSizeLimit = 2000;



module.exports = {
  SinglePostData:SinglePostData,
  IInnerPoster:IInnerPoster,
  PostData: PostData,
  MaxCacher: MaxCacher,
  BusinessLogicResult: BusinessLogicResult,
  BasicMessage:BasicMessage
};