Source: src/jsGetData.js

/*globals Context,sqlFun,DataRelation,ObjectRow  */


const dsSpace = require('../client/components/metadata/jsDataSet'),
    dataRowState = dsSpace.dataRowState,
    _ = require('lodash'),
    dq = require('../client/components/metadata/jsDataQuery'),
    dbList = require('./jsDbList'),
    multiSelect = require('./jsMultiSelect'),
    Model= require('./../client/components/metadata/MetaModel');



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


/**
 * Utility class with methods to fill a DataSet starting from a set of rows
 * @class getData
 */
function GetDataSpace() {
}

GetDataSpace.prototype = {
    constructor: GetDataSpace,
    fillDataSetByKey: fillDataSetByKey,
    fillDataSetByFilter: fillDataSetByFilter,
    getFilterByExample: getFilterByExample,
    doGet:doGet,
    getStartingFrom: getStartingFrom,
    getFilterKey: getFilterKey, //for testing purposes
    scanTables: scanTables, //for testing purposes
    getParentRows: getParentRows, //for testing purposes
    getChildRows: getChildRows, //for testing purposes
    getAllChildRows: getAllChildRows, //for testing purposes
    getRowsByFilter: getRowsByFilter, //for testing purposes
    getByKey: getByKey,//for testing purposes
    getByFilter: getByFilter //for testing purposes
};

/**
 * Evaluates a Filter basing on the key of a table and an object
 * Assumes key = object with all key necessary fields
 * @method getFilterKey
 * @private
 * @param {Context} context
 * @param {string} tableName
 * @param {object[]|object} keyValues
 * @returns {sqlFun}
 */
function getFilterKey(context, tableName, keyValues) {
    const def = Deferred();
    context.dbDescriptor.table(tableName)
        .then(function (tableDescr) {
            def.resolve(dq.mcmp(tableDescr.getKey(), keyValues));
        })
        .fail(function (err) {
            def.reject(err);
        });
    return def.promise();
}

/**
 * Gets a a filter comparing all example fields
 * @method getFilterByExample
 * @param {Context} context
 * @param {string} tableName
 * @param {object} example
 * @param {boolean}  [useLike=false]  --if true, uses 'like' for any string comparisons, otherwise uses equal comparison
 * @return {sqlFun} DataRow obtained with the given filter
 */
function getFilterByExample(context, tableName, example, useLike) {
    const def = Deferred();
    if (useLike) {
        def.resolve(dq.mcmpLike(example));
    }
    else {
        const fields = _.keys(example);
        if (fields.length > 0) {
            def.resolve(dq.mcmp(fields, example));
        }
        else {
            def.resolve(dq.constant(true));
        }
    }
    return def.promise();
}
/**
 * Gets an array of datarow given a filter
 * @method getByFilter
 * @param {Context} ctx
 * @param {DataTable} table
 * @param {sqlFun} filter
 * @param {string} [orderBy]
 * @return {DataRow[]} DataRow obtained with the given filter
 */
function getByFilter(ctx,  table, filter, orderBy) {
    const def = Deferred();
    let result;
    ctx.dataAccess.selectIntoTable(
        { table: table, filter: filter,  environment: ctx.environment, orderBy:orderBy})
        .then(function () {
            result = table.select(filter);
            if (result.length === 0) {
                def.reject('there was no row in table ' + table.name + ' filtering with ' + filter.toString());
                return;
            }
            def.resolve(result);
        })
        .fail(function (err) {
            def.reject(err);
        });
    return def.promise();
}


/**
 * Gets a single row given its key, that must be contained in key
 * @method getByKey
 * @private
 * @param {Context} ctx
 * @param {DataTable} table
 * @param {object[]} keyValues
 * @return {DataRow}  DataRow obtained with the given key
 */
function getByKey(ctx, table, keyValues) {
    const def = Deferred(),
        that = this;
    getFilterKey(ctx, table.name, keyValues)
        .then(function (sqlFilter) {
            return that.getByFilter(ctx, table, sqlFilter);
        })
        .done(function (r) {
            def.resolve(r[0]);
        })
        .fail(function (err) {
            def.reject(err);
        });
    return def.promise();
}

/**
 * Fills a DataSet given the key of a row
 * @method fillDataSetByKey
 * @param {Context} ctx
 * @param {DataSet} ds
 * @param {DataTable} table
 * @param {object[]|object} keyValues
 * @returns {*}
 */
function fillDataSetByKey(ctx, ds, table, keyValues) {
    const def = Deferred(),
        that = this;
    let result;
    that.getByKey(ctx, table, keyValues)
        .then(function (r) {
            result = r;
            return that.getStartingFrom(ctx, table);
        })
        .then(function () {
            def.resolve(result);
        })
        .fail(function (err) {
            def.reject(err);
        });
    return def.promise();
}

/**
 * Fill a dataset starting with a set of filtered rows in a table
 * @method fillDataSetByFilter
 * @param {Context} ctx
 * @param {DataTable} table  main table
 * @param {sqlFun} filter
 * @return {DataRow[]} DataRow obtained with the given filter
 */
function fillDataSetByFilter(ctx,  table, filter) {
    const def = Deferred(),
        that = this;
    let result;
    that.getByFilter(ctx, table, filter)
        .then(function (arr) {
            result = arr;
            return that.getStartingFrom(ctx, table);
        })
        .then(function () {
            def.resolve(result);
        })
        .fail(function (err) {
            def.reject(err);
        });
    return def.promise();
}

/**
 *
 * @param {DataTable} table
 * @param {{DataTable}} visited
 * @param {{DataTable}} toVisit
 */
function recursivelyMarkSubEntityAsVisited(table, visited, toVisit){
    let ds = table.dataset;
    _.forEach(table.childRelations(),
        /**
         *
         * @param {DataRelation} rel
         */
            rel=>{
                let childTable = ds.tables[rel.childTable];
                if ( visited[rel.childTable] || Model.isSubEntityRelation(rel,childTable,table)){
                    return;
                }
                visited[rel.childTable]= childTable;
                toVisit[rel.childTable]= childTable;
                recursivelyMarkSubEntityAsVisited(childTable,visited,toVisit);
        });
}

/**
 * Gets all data of the DataSet cascate-related to the primary table.
 * The first relations considered are child of primary, then
 *  proper child / parent relations are called in cascade style.
 * @param {Context} ctx
 * @param {DataTable} primaryTable
 * @param {boolean} onlyPeripherals if true, only peripheral (not primary or secondary) tables are refilled
 * @param {DataRow} [oneRow] The (eventually) only primary table row on which get the entire sub-graph.
 *  Can be null if PrimaryDataTable already contains rows.  R is not required to belong to PrimaryDataTable.
 */
function doGet(ctx, primaryTable, onlyPeripherals, oneRow){

    const /*{{DataTable}}*/ visited = {};
    const /*{{DataTable}}*/ toVisit = {};
    let /*DataSet*/ ds = primaryTable.dataset;
    //Set Fully-Visited and Cached tables as Visited
    _.forIn(ds.tables, (t,tableName)=>{
        if (Model.cachedTable(t)||Model.visitedFully(t)|| Model.temporaryTable(t)) {
            visited[tableName] = t;
        }
    });
    toVisit[primaryTable.name]=primaryTable;
    visited[primaryTable.name]=primaryTable;

    if (onlyPeripherals){
        //Marks child tables as ToVisit+Visited
        recursivelyMarkSubEntityAsVisited(primaryTable, visited,toVisit);
        _.forIn(ds.tables,(t,tableName)=>{
           if (!Model.allowClear(t)){
               visited[tableName]=t;
               toVisit[tableName]=t;
           }
        });
    }

    //Clears all other tables
    _.forIn (ds.tables,
        (t,tableName)=>{
            if (visited[tableName]) return;
            if (Model.temporaryTable(t)) return;
            let realTable= t.realTable();
            if(realTable){
                if (visited[realTable]) return;
            }
            t.clear();
        });

    //Set as Visited all child tables linked by autoincrement fields
    if (oneRow && oneRow.state === dataRowState.added){
        _.forEach(primaryTable.childRelations(),
            /**
             * @param {DataRelation} rel
             */
            rel=>{
                let toSkip = rel.parentCols.some(c=>primaryTable.autoIncrement(c));
                if (toSkip){
                    visited[rel.childTable]= ds.tables[rel.childTable];
                }
            });
    }

    return this.scanTables(ctx, ds, toVisit, visited, oneRow)
        .then(()=>{
            if (onlyPeripherals){
                _.forEach(primaryTable.childRelations(),
                    rel=>{
                        let /*DataTable*/ childTable = ds.tables[rel.childTable];
                        if (Model.allowClear(childTable) &&
                            !Model.isSubEntityRelation(rel,childTable,primaryTable)
                        ){
                            return;
                        }
                       Model.getTemporaryValues(childTable);
                    });

                if (oneRow){
                    Model.getRowTemporaryValues(oneRow);
                }
                else {
                    Model.getTemporaryValues(primaryTable);
                }
            }
        });

}

/**
 * Assuming that primaryTable has ALREADY been filled with data, read all childs and parents starting from
 *  rows present in primaryTable.
 * @method getStartingFrom
 * @param {Context} ctx
 * @param {DataTable} primaryTable
 * @return {promise}
 */
function getStartingFrom(ctx, primaryTable) {
    const visited = {},
        that = this,
        ds = primaryTable.dataset,
        toVisit = {};
    let opened = false;
    const def = Deferred();
    visited[primaryTable.name] = primaryTable;
    toVisit[primaryTable.name] = primaryTable;
    ctx.dataAccess.open()
        .then(function () {
            opened = true;
            return that.scanTables(ctx, ds, toVisit, visited);
        })
        .then(function () {
            if (opened) {
                return ctx.dataAccess.close().then(()=>def.resolve());
            }
            def.resolve();
            return def;
        })
        .fail(function (err) {
            if (opened) {
                return ctx.dataAccess.close().then(()=>def.reject(err));
            }
            def.reject(err);
            return def;
        });


    return def.promise();


}

/**
 * @method scanTables
 * @private
 * @param {Context} ctx
 * @param {DataSet} ds
 * @param {hash} toVisit
 * @param {hash} visited
 * @param {DataRow} oneRow
 */
function scanTables(ctx, ds, toVisit, visited, oneRow) {
    const def = Deferred(),
        that = this,
        /*{{DataTable}}*/ nextVisit = {},//table to visit in the next step, i.e. this will be passed recursively as toVisit
        selList = []; //{Select[]}
    if (_.keys(toVisit).length === 0) {
        def.resolve();
        return;
    }

    //Every child and parent tables of toVisit that aren't yet visited or toVisit become visited and nextVisit
    _.forIn(toVisit, function (table, tableName) {
        if (Model.temporaryTable(table)) {
            return;
        }

        //searches child tables of T & pre-set them to visited
        _.forEach(ds.relationsByParent[tableName],
            /**
             * @param {DataRelation} rel
             */
            function (rel) {
                if (visited[rel.childTable] || toVisit[rel.childTable]) {
                    return;
                }
                const /*DataTable*/ childTable = ds.tables[rel.childTable];
                visited[rel.childTable] = childTable;
                nextVisit[rel.childTable] = childTable;
            });

        //searches parent tables of T & pre-set them to visited + NextVisit
        _.forEach(ds.relationsByChild[tableName],
            /**
             * @param {DataRelation} rel
             */
            function (rel) {
                if (visited[rel.parentTable] || toVisit[rel.parentTable]) {
                    return;
                }
                const /*DataTable*/ parentTable = ds.tables[rel.parentTable];
                visited[rel.parentTable] = parentTable;
                nextVisit[rel.parentTable] = parentTable;
            });
    });

    //load all rows in nextVisit
    _.forIn(toVisit,
        /**
         * @param {DataTable }table
         */
        function (table) {
            if (table.rows.length === 0) {
                return;
            }
            //get parents of table row
            if (!oneRow || oneRow.table.name !== table.name) {
                _.forEach(table.rows, function (r) {
                    that.getParentRows(ds, r, nextVisit, selList);
                });

                that.getAllChildRows(
                    ds,
                    table,
                    nextVisit,
                    selList);
                return;
            }
            //(OneRow!=null) && (OneRow.Table == T)
            if (oneRow.state=== dataRowState.deleted){
                return;
            }
            that.getParentRows(ds, oneRow.current,nextVisit,selList);
            that.getChildRows(ds, oneRow.current, nextVisit,selList);
        });

    if (selList.length === 0) {
        def.resolve();
    }
    else {
        ctx.dataAccess.multiSelect({
                selectList: selList,
                environment: ctx.environment
            })
            .progress(function (data) { //data.tableName and data.rows are the read data
                if (data.rows) {
                    ds.tables[data.tableName].mergeArray(data.rows, true);
                }
            })
            .done(function () {
                //Recursion with new parameters
                that.scanTables(ctx, ds, nextVisit, visited)
                    .done(function () {
                        _.forIn(toVisit, function (table) {
                            Model.getTemporaryValues(table);
                        });
                        def.resolve();
                    })
                    .fail(function (err) {
                        def.reject(err);
                    });
                }
            )
            .fail(function (err) {
                def.reject(err);
            });


    }
    return def.promise();
}

/**
 * Adds select to parent rows
 * @private
 * @method getParentRows
 * @param {DataSet} ds
 * @param {ObjectRow} row
 * @param {object} allowed
 * @param {Array.<Select>} selList
 */
function getParentRows(ds, row, allowed, selList) {
    const childTable = row.getRow().table,
        that = this;
    if (row.getRow().state === dataRowState.deleted) {
        return;
    }
    _.forEach(ds.relationsByChild[childTable.name],
        /**
         * @param {DataRelation} parentRel
         */
        function (parentRel) {
            if (!allowed[parentRel.parentTable]) {
                return;
            }
            let parentTable = ds.tables[parentRel.parentTable];

            let parentFilter = parentRel.getParentsFilter(row);
            if (parentFilter.isFalse) {
                return;
            }
            const multiComp = new multiSelect.MultiCompare(parentRel.parentCols,
                _.map(parentRel.childCols, function (field) {
                    return row[field];
                })
            );

            that.getRowsByFilter(multiComp, parentTable, selList);

        });

}



/**
 * Adds select to child rows
 * @private
 * @method getChildRows
 * @param {DataSet} ds
 * @param {ObjectRow} row
 * @param {object} allowed
 * @param {Array<Select>} selList
 */
function getChildRows(ds, row, allowed, selList) {
    const parentTable = row.getRow().table,
        that = this;
    if (row.getRow().state === dataRowState.deleted) {
        return;
    }
    _.forEach(ds.relationsByParent[parentTable.name],
        /**
         * @param {DataRelation} childRel
         */
        function (childRel) {
            if (!allowed[childRel.childTable]) {
                return;
            }
            let childTable = ds.tables[childRel.childTable];
            let childFilter = childRel.getChildFilter(row);
            if (childFilter.isFalse) {
                return;
            }
            const multiComp = new multiSelect.MultiCompare(childRel.childCols,
                _.map(childRel.parentCols, function (field) {
                    return row[field];
                })
            );
            that.getRowsByFilter(multiComp, childTable, selList);

        });

}

/**
 * Adds select to parent rows
 * @method getAllChildRows
 * @private
 * @param {DataSet} ds
 * @param {DataTable} parentTable
 * @param {object} allowed
 * @param {Arrray.<Select>} selList
 */
function getAllChildRows(ds, parentTable, allowed, selList) {
    const that = this;
    _.forEach(ds.relationsByParent[parentTable.name],
        /**
         * @param {DataRelation} rel
         */
        function (rel) {
            if (!allowed[rel.childTable]) {
                return;
            }
            const childTable = ds.tables[rel.childTable];

            _.forEach(parentTable.select(rel.activationFilter()),
                /**
                 * @param {ObjectRow} r
                 */
                function (r) {
                    // if (r.getRow().state === dataRowState.added) {
                    //     return;
                    // }
                    const childFilter = rel.getChildFilter(r);
                    if (childFilter.isFalse) {
                        return;
                    }
                    const multiComp = new multiSelect.MultiCompare(rel.childCols,
                        _.map(rel.parentCols, function (field) {
                            return r[field];
                        })
                    );
                    that.getRowsByFilter(multiComp, childTable, selList);
                });

        });
}

/**
 * Adds a select command to the given SelectList
 * @method getRowsByFilter
 * @private
 * @param {MultiCompare} multiComp
 * @param {DataTable} table
 * @param {Select[]} selectList
 */
function getRowsByFilter(multiComp, table, selectList) {
    //var mergedFilter = dq.and(filter, table.staticFilter());
    selectList.push(new multiSelect.Select(table.columnList())
        .from(table.tableForReading())
        .intoTable(table.name)
        .staticFilter(table.staticFilter())
        .multiCompare(multiComp)
        .orderBy(table.orderBy()));
}


// exported as an object in order to do unit tests
module.exports = new GetDataSpace();