Source: client/components/metadata/GetDataInvoke.js

/*global sqlFun,ObjectRow */

/**
 * @module GetDataInvoke
 * @description
 * Fakes client web service invocation of GetDataServices, in order to share metadata classes
 */


const Deferred = require("./EventManager").Deferred;
/*jsDataQuery*/
const q = require('./jsDataQuery').jsDataQuery;
const getDataUtils = require("./GetDataUtils");
const logger =  require('./Logger').logger ;
const logType = require('./Logger').logTypeEnum;
const model = require('./MetaModel').metaModel;
const _ = require('lodash');
const utils = require("./utils").utils;
const mSel = require('./../../../src/jsMultiselect');
const Select = mSel.Select;

const GetDataSet = require("./GetDataSet");
const GetData = require("./../../../src/jsGetData");
const {orderBy} = require("lodash");
const jsDataSet = require("./jsDataSet");
const {readFile} = require("fs/promises");
const path = require("path");

function SelectBuilder(){
        /*DataTable*/ this.table = null;
        /*string*/ this.top = null;
        /*string*/ this.tableName = null;
        /*sqlFun*/ this.filter = null;
    }

SelectBuilder.prototype = {
        constructor: SelectBuilder
};


    /**
     * Exposes server web services as local functions so that code can use them seemingly
     * @constructor GetDataInvoke
     * @param {Context} ctx
     */
    function GetDataInvoke(ctx) {
        /*Context*/ this.ctx = ctx;
        /*DataAccess*/ this.conn= ctx.dataAccess;
        /*Environment*/ this.env=ctx.environment;
        /*DbDescriptor*/ this.dbDescriptor = ctx.dbDescriptor;
        this.getMeta = ctx.getMeta;
    }

    GetDataInvoke.prototype = {
        constructor: GetDataInvoke,

        /**
         * @method readCached
         * @public
         * @description ASYNC
         * Reads all cached tables
         * @param {DataSet} d
         * @returns {Deferred}
         */
        readCached: function (d) {
            /* {filter:sqlFun, top:string, table:DataTable}[] */
            let selectList = [];
            let that = this;
            const allPromises = [];
            _.forEach(d.tables,
                function (t) {
                    if (!model.cachedTable(t)) return;
                    if (!model.canRead(t)) return;
                    allPromises.push(that.doGetTable(t, null, true, null, selectList));
                });
            return Deferred("readCached").from(
                Deferred.when(allPromises)
                    .then(function () {
                        if (selectList.length === 0) return false;
                        return that.multiRunSelect(selectList)
                            .then(function () {
                                _.forEach(selectList,
                                    function (sel) {
                                        model.getTemporaryValues(sel.table);
                                    });
                                return true;
                            });
                    }));

        },

        /**
         * @method doGetTable
         * @public
         * @description ASYNC
         * Gets a DataTable with an optional set of Select. If a list of select is given, adds a select in selectList
         * @param {DataTable} t DataTable to Get from DataBase
         * @param {sqlFun} filter
         * @param {boolean} clear  if true table is cleared before reading
         * @param {string} top  parameter for "top" clause of select
         * @param {Array.<filter:sqlFun, top:string, table:DataTable>} selectList
         * @returns {Deferred}
         */
        doGetTable: function (t, filter, clear, top, selectList) {
            let def = Deferred("doGetTable");
            // log per controllo se il filtro di tipo mcmp non è su colonne presenti sulla tabella.
            let columnFilterInTable = function (t, f) {
                if (!t || !f) return true;
                if (f.myName === "mcmp") {
                    return _.every(f.myArguments[0], function (c) {
                        return !!t.columns[c];
                    });
                }
                return true;
            };
            if (!columnFilterInTable(t, filter)) {
                // console.warn("warning: doGetTable: Table t " + t.name + " misses some filtered columns", filter);
                return def.resolve(false);
            }

            if (!model.canRead(t)) return def.resolve(false);
            let that = this;
            if (clear) {
                t.clear();
            }

            let mergedFilter = t.staticFilter();

            if (filter && mergedFilter) {
                mergedFilter = q.and(filter, mergedFilter);
                mergedFilter.isTrue = filter.isTrue;
            }
            else {
                mergedFilter = filter ? filter : mergedFilter;
            }

            let mySel = null;
            let doGetResult = true;

            return def.from(
                utils._if(selectList !== undefined)
                    ._then(function () {
                        mySel = {
                            filter: mergedFilter,
                            top: top,
                            table: t
                        };
                        selectList.push(mySel);
                        doGetResult = mySel;
                        return true; //new Deferred("doGetTable").resolve().promise();
                    })
                    ._else(function () {
                        return that.runSelectIntoTable(t, mergedFilter, top);
                    })
                    .then(function () {
                        if (mergedFilter === null || mergedFilter === undefined || mergedFilter.isTrue) {
                            model.setAsRead(t); //table has been read fully
                        }
                        if (!selectList) model.getTemporaryValues(t);
                        return doGetResult;
                    })).promise();
        },

        /**
         * @method getRowsByFilter
         * @public
         * @description ASYNC
         * Returns the Rows from the DataTable "table" based on the clause "filter"
         * @param {sqlFun} filter
         * @param {boolean} multiCompare
         * @param {DataTable} table
         * @param {string} top
         * @param {boolean} prepare
         * @param {Array.<filter:sqlFun, top:string, table:DataTable>} [selList]
         * @returns {Deferred}
         */
        getRowsByFilter: function (filter, multiCompare, table, top, prepare, selList) {
            let def = Deferred('getRowsByFilter');
            if (!model.canRead(table)) return def.resolve().promise();

            let mergedFilter = table.staticFilter();

            if (filter !== null && filter !== undefined) {
                if (mergedFilter) {
                    mergedFilter = q.and(filter, mergedFilter);
                }
                else {
                    mergedFilter = filter;
                }
            }
            let that = this;
            let res = utils._if(selList !== undefined)
                ._then(function () {
                    selList.push({filter: mergedFilter, top: top, table: table});
                })
                ._else(function () {
                    return that.runSelectIntoTable(table, mergedFilter, top);
                })
                .then(function () {
                    model.setAsRead(table);
                    return true;
                });

            return def.from(res).promise();
        },

        /**
         * @method selectCount
         * @public
         * @description ASYNC
         * Gets the rows count of the "tableName" filtered on "filter"
         * @param {string} tableName
         * @param {sqlFun} filter
         * @returns {Deferred}
         */
        selectCount: function (tableName, filter) {
            return this.conn.selectCount({tableName: tableName, filter: filter, environment: this.env});
        },

        /**
         * @method runSelect
         * @public
         * @description ASYNC
         * Returns a deferred DataTable where the rows are select on "tableName" filtered on "filter". If "columnList" is "*" it returns all columns,
         * otherwise only those specified in "columnList".
         * If top is specified it returns only a max of "top" rows, otherwise it returns all the rows.
         * @param {string} tableName
         * @param {string} columnList
         * @param {sqlFun} filter
         * @param {string} [top]
         * @returns {Deferred<DataTable>}
         */
        runSelect: function (tableName, columnList, filter, top) {
            let def = Deferred('runSelect');
            let res = this.conn.select({
                tableName: tableName,
                columns: columnList,
                top: top,
                filter: filter,
                environment: this.env,
            });
            return def.from(res).promise();
        },

        /**
         * @method addRowToTable
         * @public
         * @description SYNC
         * Adds a copy of the row "r" to the DataTable "t", and returns the linked DataRow
         * @param {DataTable} t
         * @param {ObjectRow} r
         * @returns {ObjectRow}
         */
        addRowToTable: function (t, r) {
            let newRow = t.newRow();
            let oldTable = r.getRow().table;
            _.forEach(t.columns, function (c) {
                if (oldTable.columns[c.name]) {
                    newRow[c.name] = r[c.name];
                }
            });

            t.add(newRow);
            newRow.getRow().acceptChanges();
            return newRow;
        },


        /**
         * Executes a bunch of select, based on "selectList". Not much different from a multiple runSelectIntoTable
         * @method multiRunSelect
         * @public
         * @param {Array.<filter:sqlFun, top:string, table:DataTable>} selectList
         * @returns {Deferred}
         */
        multiRunSelect: function (selectList) {

            if (selectList.length === 0) {
                logger.log(logType.WARNING, "You are calling multiRunSelect with selectList empty");
                return Deferred("multiRunSelect").resolve().promise();
            }
            let adaptedList = selectList.map(sel =>
                new Select(model.columnNameList(sel.table))
                    .from(sel.table.tableForReading())
                    .where(sel.filter)
                    .top(sel.top)
            );
            return this.conn.multiSelect({selectList: adaptedList, environment: this.env})
                .progress(
                    /**
                     * @param {{tableName: string, set: number, rows : object[]}} data
                     */
                    function (data) {
                        //data is an object:
                        // data mi aspetto sia un DataTable

                        // recupero il dataTable da riempire dalla lista di input
                        let destTableArr = _.filter(selectList, sel => sel.table.tableForReading() === data.tableName);
                        if (destTableArr.length > 0) {
                            /*DataTable*/
                            let destTable = destTableArr[0].table;
                            let tableWasEmpty = (destTable.rows.length === 0);
                            destTable.mergeArray(data.rows, !tableWasEmpty);
                            //getDataUtils.mergeRowsIntoTable(destTable, data.rows, !tableWasEmpty);
                        }
                    })
                .then(
                    function () {
                        // vanno serializzate le chiamate alle sel.onRead(), ove siano definite
                        let allDeferredOnRead = utils.filterArrayOnField(selectList, 'onRead');
                        return utils.thenSequence(allDeferredOnRead);
                    });
        },

        /**
         * @method runSelectIntoTable
         * @public
         * @description ASYNC
         * Reads a set of rows in a given DataTable "t" based on clause "filter"
         * @param {DataTable} t
         * @param {sqlFun} filter. The filter to apply to the select
         * @param {string} top. Max num of rows to read
         * @returns {DataTable} The table with the rows read
         */
        runSelectIntoTable: function (t, filter, top) {
            let def = Deferred("runSelectIntoTable");
            let res = this.runSelect(t.tableForReading(), model.columnNameList(t), filter, top)
                .then(function (rows) {
                    //merges rows into t
                    t.mergeArray(rows, t.rows.length !== 0);
                    //getDataUtils.mergeRowsIntoTable(t, dt.rows, t.rows.length !== 0);
                    return def.resolve(t);
                });
            return def.from(res).promise();
        },

        /**
         * @method createTableByName
         * @public
         * @description ASYNC
         * Creates and returns a DataTable "tableName" with the specified columns
         * @param {string} tableName
         * @param {string} columnList
         * @returns {Deferred<DataTable>}
         */
        createTableByName: function (tableName, columnList) {
            let def = Deferred('createTableByName');
            let res = this.dbDescriptor.createTable(tableName);
            return def.from(res).promise();
        },

        /// <summary>
        ///
        /// </summary>
        /// <param name="tableName">string</param>
        /// <param name="listType">string</param>
        /// <returns>string[]</returns> an array of 2 strings. At the first position the new tableName, at the second the new listType


        /**
         * Executes the mapping of tableName and listType on "web_listredir" table taking a new tableName
         *  (usually a custom view) and a new listType
         * @param {string} tableName
         * @param {string} listType
         * @return {Deferred<string[]>}
         */
        getMappingWebListRedir: function (tableName, listType) {
            let def = Deferred('getMappingWebListRedir');
            /*string[]*/
            let standardMap = [tableName, listType];
            this.conn.select({
                tableName: tableName,
                filter: q.and(q.eq("tablename", tableName), q.eq("listtype", listType)),
                environment: this.env,
                columns: "newtablename,newlisttype"
            })
                .then(rows => {
                    if (rows.length === 0) {
                        def.resolve(standardMap);
                        return;
                    }
                    let map = rows[0];
                    def.resolve([map["newtablename"], map["newlisttype"]]);
                })
                .fail(err => {
                    def.resolve(standardMap);
                });
            return def.promise();
        },


        /**
         * @method getPagedTable
         * @public
         * @description ASYNC
         * Returns the rows paginated of the DataTable "tableName".
         * The rows are filtered based on clause "filter"
         * @param {string} tableName
         * @param {number} nPage
         * @param {number} nRowPerPage
         * @param {sqlFun} filter
         * @param {string} listType
         * @param {string} sortBy
         * @returns {Deferred<DataTable>}
         */
        getPagedTable: function (tableName, nPage, nRowPerPage, filter, listType, sortBy) {
            let def = Deferred('getPagedTable');
            let newTableName;
            let newListType;
            let dtOriginal;
            let meta;
            let aborted = false;
            let totPages;
            let totalRows;

            let res = this.getMappingWebListRedir(tableName, listType)
                .then(map => {
                    newTableName = map[0];
                    newListType = map[1];
                    return this.dbDescriptor.createTable(newTableName);
                })
                .then(table => {
                    if (aborted) return;
                    dtOriginal = table;
                    if (_.keys(dtOriginal.columns).length === 0) {
                        def.reject("Table/view " + tableName + " is not configured in columntypes");
                        aborted = true;
                        return;
                    }
                    meta = this.getMeta(newTableName);
                    if (!sortBy) {
                        let sortMeta = meta.getSorting(newListType);
                        if (sortMeta) {
                            sortBy = sortMeta;
                        }
                        else {
                            if (table.columns.title) {
                                sortBy = "title";
                            }
                            else {
                                sortBy = _.keys(table.columns)[0];
                            }
                        }
                    }
                    let staticFilter = meta.getStaticFilter(newListType);
                    if (staticFilter) {
                        filter = q.and(filter, staticFilter);
                    }
                    return this.conn.selectCount({tableName: newTableName, filter: filter, environment: this.env});
                })
                .then(totRows => {
                    if (aborted) return;
                    totalRows = totRows;
                    totPages = Math.ceil(totRows / nRowPerPage);
                    let sql;
                    if (totPages < 2 || !sortBy) {
                        sql = this.conn.getSelectCommand({
                            tableName: newTableName,
                            columns: "*",
                            filter: filter,
                            environment: this.env
                        });
                    }
                    else {
                        let firstRow = (nPage - 1) * nRowPerPage + 1;
                        sql = this.conn.sqlConn.getPagedTableCommand(newTableName,
                            this.conn.getFormatter().toSql(filter, this.env),
                            firstRow,
                            nRowPerPage);
                    }
                    return this.conn.runSql(sql);
                })
                .then(data => {
                    if (aborted) return;
                    if (!data) {
                        aborted = true;
                        def.reject("Query error:" + filter.toString());
                        return;
                    }

                    if (dtOriginal.key().length === 0) {
                        dtOriginal.key(meta.primaryKey());
                    }

                    dtOriginal.loadArray(data.rows, false);
                    def.resolve(dtOriginal, totPages, totalRows);
                })
                .fail(err => {
                    def.reject(err);
                });

            return def.promise();
        },


        /**
         * @method getDsByRowKey
         * @public
         * @description ASYNC
         * Fills the dataSet starting from a table
         * @param {DataRow} dataRow
         * @param {DataTable} table primaryTable
         * @param {string} [editType] used to retrieve a DataSet. If not specified, table.dataset is used
         * @returns {Deferred<DataSet>}
         */
        getDsByRowKey: function (dataRow, table, editType) {

            let def = Deferred('getDsByRowKey');
            if (!model.canRead(table)) return def.resolve(null);

            if (_.some(table.key(), cname => dataRow.current[cname] === undefined)) {
                def.reject("Table view " + dataRow.table.name + " has different column key name respect to " + table.name);
            }

            let filter = table.keyFilter(dataRow.current);

            let ds = table.dataset;
            if (editType) {
                ds = this.createEmptyDataSet( table.name, editType);
            }

            if (filter !== null && table.staticFilter()) {
                filter = q.and(filter, table.staticFilter());
            }

            GetData.fillDataSetByFilter(this.ctx, ds.tables[table.name], filter)
                .then(result => {
                        def.resolve(ds);
                    },
                    err => {
                        def.reject(err);
                    }
                );
            return def.promise();
        },

        /**
         * @method getByKey
         * @public
         * @description ASYNC
         * Returns a deferred with the DataRow of the table, filtered based on the keys of the DataTable "table", where the values are those of "row"
         * @param {DataTable} table
         * @param {DataRow} row
         * @returns {Deferred<DataRow>}
         */
        getByKey: function (table, row) {
            let def = Deferred('getByKey');

            let filter = table.keyFilter(row.current);

            let res = this.runSelect(table.name, "*", filter)
                .then(function (dt) {
                    if (!dt) return def.resolve(null);
                    if (dt.rows.length === 0) return def.resolve(null);
                    model.getTemporaryValues(table);
                    return def.resolve(dt.rows[0].getRow());
                });

            return def.from(res).promise();
        },


        /**
         * @method getDataSet
         * @public
         * @description ASYNC
         * Returns a deferred resolved with jsDataSet based on "tableName" and "editType" keys
         * @param {string} tableName
         * @param {string} editType
         * @returns {DataSet})
         */
        getDataSet: function (tableName, editType) {
            return GetDataSet.getDataSet(tableName, editType);
        },

        /**
         * @method createEmptyDataSet
         * @public
         * @description ASYNC
         * Returns a deferred resolved with jsDataSet based on "tableName" and "editType" keys
         * @param {string} tableName
         * @param {string} editType
         * @returns Promise<DataSet>
         */
        createEmptyDataSet: function (tableName, editType) {
            return GetDataSet.createEmptyDataSet(this.ctx, tableName, editType);
        },

        /**
         * @method prefillDataSet
         * @public
         * @description ASYNC
         * Returns a deferred resolved with the DataSet. It loads data from cached DataTable, based on "staticfilter" property
         * @param {jsDataSet} dsTarget
         * @param {string} tableName
         * @param {string} editType
         * @returns {Deferred<DataSet>})
         */
        prefillDataSet: function (dsTarget, tableName, editType) {
            let def = Deferred("prefillDataSet");


            // costruisco coppie chiave valore, poi lo serializzo facendo un json
            /* {filter:sqlFun, top:string, table:DataTable}[] */
            let selectTable = [];

            _.forEach(dsTarget.tables,
                t => {
                    if (model.cachedTable(t)) {
                        selectTable.push({filter: t.staticFilter(), top: null, table: t});
                    }
                });

            return this.multiRunSelect(selectTable)
                .then(()=>{
                    return dsTarget;
                });
        },

        /**
         * @method fillDataSet
         * @public
         * @description ASYNC
         * Reads and fills from the server the DatSet with tableName.editType, filters it on "filter" and merges it into dsTarget.
         * Returns a deferred resolved with the DataSet merged.
         * @param {DataSet} dsTarget
         * @param {string} tableName
         * @param {string} editType
         * @param {sqlFun} filter
         * @returns {Deferred<DataSet|string>}
         */
        fillDataSet: function (dsTarget, tableName, editType, filter) {
            let def = Deferred("fillDataSet");
            let res = GetData.fillDataSetByFilter(this.ctx, dsTarget.tables[tableName],filter);
            return  def.from(res).promise();
        },

        /**
         * @method doGet
         * @public
         * @description ASYNC
         * Returns a deferred resolved with a DataSet. The dataSet is the "ds" merged with the dataset filtered on the datarow values.
         * @param {DataSet} ds
         * @param {DataRow} dataRow
         * @param {string} primaryTableName
         * @param {boolean} onlyPeripherals
         * @returns {Deferred}
         */
        doGet: function (ds, dataRow, primaryTableName, onlyPeripherals) {
            let def = Deferred("doGet");
            let /*DataTable*/ primaryTable = ds.tables[primaryTableName];
            let res =  GetData.doGet(this.ctx, primaryTable, onlyPeripherals, dataRow);
            return  def.from(res).promise();
        },


        /**
         * @method doGetTableRoots
         * @public
         * @description ASYNC
         * Gets some row from a datatable, with all child rows in the same table
         * @remarks it was DO_GET_TABLE_ROOTS() in TreViewDataAccess
         *
         * @param {DataTable} table
         * @param {jsDataQuery} filter
         * @param {boolean} clear
         */
        doGetTableRoots: function (table, filter, clear) {
            let def = Deferred('doGetTableRoots');
            if (!model.canRead(table)) return def.resolve(false);
            let res = this.doGetTable(table, filter, clear, null);
            return def.from(res).promise();
        },

        /**
         * @method describeColumns
         * @public
         * @description ASYNC
         * @param {DataTable} table
         * @param {string} listType
         * Calls the describeColumns server side method on "tableName" and "listType"
         * @returns {Deferred<DataTable>}
         */
        describeColumns: function (table, listType) {
            let def = Deferred('describeColumns');
            let meta = this.getMeta(table.name);
            let res=  meta.describeColumns(table,listType)
                .then(t=>{
                    def.resolve();
                });

            return def.from(res).promise();
        },

        /**
         * @method describeTree
         * @public
         * @description ASYNC
         * @param {DataTable} table
         * @param {string} listType
         * Calls the describeTree server side method on "tableName" and "listType"
         * @returns {Deferred<DataTable>}
         */
        describeTree: function (table, listType) {
            let def = Deferred('describeTree');
            let meta = this.getMeta(table.name);
            let res=  meta.describeTree(table,listType);

            return def.from(res).promise();
        },

        /**
         * @method getSpecificChild
         * @public
         * @description ASYNC
         * Gets a row from a table T taking the first row by the filter
         * startCondition AND (startfield like startval%)
         * If more than one row is found, the one with the smallest startfield is
         * returned. Used for AutoManage functions. and treevewmanger
         * @param {DataTable} table
         * @param {sqlFun} startCondition
         * @param {string} startValueWanted
         * @param {string} startFieldWanted
         * @returns {Deferred<ObjectRow>}
         */
        getSpecificChild: function (table, startCondition, startValueWanted, startFieldWanted) {
            let def = Deferred("getSpecificChild");

            let filter = q.and(startCondition, q.like(startFieldWanted, startValueWanted));
            let res = this.getData.getByFilter(this.ctx, table, filter, orderBy)
                .then(/*DataRow[]*/ rows=>{
                    if (rows.length === 0){
                        return null;
                    }
                    if (rows.length === 1){
                        return rows[0];
                    }
                    return  null;
                });
            return def.from(res).promise();
        },

        /**
         * @method launchCustomServerMethod
         * @public
         * @description ASYNC
         * Launches a post call to the server with eventName that is the method custom to call, and with custom "prms"
         * @param {string} method
         * @param {object} prms
         * @returns {Deferred}
         */
        launchCustomServerMethod: function (method, prms) {
            let def = Deferred('launchCustomServerMethod');

            switch (method){
                default: def.reject(method + "is not a supported custom server method")
            }

            return def.promise();
        },

        /**
         * @method doReadValue
         * @public
         * @description ASYNC
         * Returns a single value from a table based on filter. "select value from tableName where filter"
         * @param {string} tableName
         * @param {sqlFun} filter
         * @param {sqlFun} expr
         * @param {string} orderBy
         * @returns {Deferred}
         */
        doReadValue: function (tableName, filter, expr, orderBy) {
            let def = Deferred('doReadValue');

            let res = this.conn.readSingleValue({
                tableName:tableName,
                expr:expr,      //attenzione che qui è cambiata la convenzione, ora è sqlFun prima era string
                filter:filter,
                environment:this.environment,
                orderBy:orderBy
            });

            return def.from(res).promise();
        },

    };

    module.exports = GetDataInvoke;