Skip to content
Snippets Groups Projects
process.js 11.91 KiB
import("system.indexsearch");
import("system.neon");
import("system.text");

/**
 * provides static methods for special handling of entities in JDito-Processes
 * do not create an instance of this
 * 
 * @class
 */
function IndexsearchUtils() {}

/**
 * Searches within an affectedInfoContainer for an id value <br/>
 * This id value can be used in the affectedIds-process of an index-recordContainer for example <br/>
 * 
 * Depending on the provided action the given ID-field is searched in: <br/>
 * <ul>
 * <li>the newValues (action: I), no other values are yet present</li>
 * <li>the oldValues (action: D), other values are not anymore present</li>
 * <li>another source by calling a function (action: U), in the update case only values are provided that acutally changed so 
 *      another source (usually the dattabase) is needed to perform the lookup</li>
 * <li>Otherwise an empty array is given (action X for example)</li>
 * </ul>
 * 
 * @param {String} fieldname                <p> Name of the field that shall be searched within the infocontainer
 * @param {Object} affectedInfoContainer    <p> affectedInfoContainer like it is provided by the IndexsearchUtils.createAffectedInfoContainer-function
 *
 * @param {Function} updateFn               <p> a callback function that is called when an update-action is provided in the affectedInfoContianer 
 *                                          (which means there are no [useful] newvalues/oldvalues) 
 *
 * @return {Array}                          <p> 1D-array of Strings with all affectedIds that where found depeding on the information in the 
 *                                          affectedInfoContainer
 * @example 
 * //example for an affectedIds-procces for a indexer that is built on top of the database table OFFER
 * //explanations of the example are written as code-comments
 * ...imports, variables and more code here...
 * //this is always the uid of the record that has been changed, so if an OFFER-entry has been changed the OFFERID is returned, if an OFFERITEM-entry 
 * //has been changed the OFFERITEMID is returned and so on
 * var idValue = vars.get("$local.idvalue");
 * var infoContainer =  ...code for creating a infoContainer...
 * 
 * switch (tableName)//the tableName is always the name of the DB table that has been changed (changed = inserted, updated or deleted)
 * {
 *     //if the OFFER itself has been changed it's very easy because it's possible to return the OFFERID directly
 *     case "OFFER": 
 *         res = [idValue];
 *         break;    
 *     //for other items (like the OFFERITEM in this exmaple) however it is not that easy, because the index-UID is needed as result in the 
 *     //affectedIdsProcess, so we have find the correct OFFERID for the OFFERITEM that has been changed
 *     // therefore we have to define the columnname of the index-UID we are searching within our table (here: OFFER_ID) and we have to define a 
 *     //callback function that returns the index-UID with for the given ID of the record that has been changed [to see why we need a function see the
 *     //descriptiontext of this function]
 *     case "OFFERITEM": 
 *         res = IndexsearchUtils.getAffectedIdValues("OFFER_ID", infoContainer, function (offerItemId){
 *             return newSelect("OFFERITEM.OFFER_ID")
 *             .from("OFFERITEM")
 *             .where("OFFERITEM.OFFERITEMID", offerItemId)
 *             .arrayColumn();
 *         });
 *         break;
 * }
 * 
 * //this needed as a savegate, because the Index is being rebuilt by the indexer if nothing gets returned => do a final check here!
 * if (res)
 *     result.object(res);
 * else
 *     result.object([]);
 */
IndexsearchUtils.getAffectedIdValues = function(fieldname, affectedInfoContainer, updateFn) {
    var affectedIds;
    switch (affectedInfoContainer.action)
    {
        case "I":
            affectedIds = affectedInfoContainer.newValues[affectedInfoContainer.columns.indexOf(fieldname)];
            affectedIds = affectedIds ? [affectedIds] : [];//check if affectedIds are present because otherwise we may would return [null]
            break;
        case "U":
            affectedIds = updateFn.call(null, affectedInfoContainer.id);
            break;
        case "D":
            affectedIds = affectedInfoContainer.oldValues[affectedInfoContainer.columns.indexOf(fieldname)];
            affectedIds = affectedIds ? [affectedIds] : [];//check if affectedIds are present because otherwise we may would return [null]
            break;
    }
    if(affectedIds)
        return affectedIds;
    else
        return [];
}

/**
 * Builds an object with various properties and returns the newly created object.
 * The object that is retruned is so called affectedInfoContainer, but the type and prototype is still object.<br/>
 * <br/>
 * Each object represents one audited change in a db table with a sepcific type (insert, update, delete, etc.) for one row. 
 * The object is basically a unified blueprint for such a change.<br/>
 * <br/>
 * Main purpose of the object is to make the handling of given values within a affectedIdProcess in an entity-indexRecordContainer easier.
 * However it could be used in other cases too, for example in the auditProcess.
 * 
 * @param {String} changedIdValue   <p> UID of the row where the change has been audited
 * @param {String} changedTable     <p> name of the db table where the change has been audited
 * @param {String} action           <p> action type of the audited change in short form, for exmaple: I, U, D, ...
 * @param {Function} columnsFn      <p> callbackFunction that has to return an array with the column names of the audited table
 * @param {Function} oldValueFn     <p> callbackFunction that has to return an array with the new values that exist after the audited change
 * @param {Function} newValueFn     <p> callbackFunction that has to return an array with the old values that have existed before the audited change
 * 
 * @return {Object} object with the following properties (values depend on the parameters):
 * <ul>
 * <li>id: uid of the changed record</li>
 * <li>table: name of the database table of the changed record</li>
 * <li>action: type of the change, this is usually "I" for insert, "U" for update, "D" for delete and "X" for unknown</li>
 * <li>columns: array of column names of the table that have been audited (if present)</li>
 * <li>oldValues: detemined values before the change (if present)</li>
 * <li>newValues: detemined values after the change (if present)</li>
 * </ul>
 * <br/>
 * Note that this only what the object SHOULD contain, the actual values depend on the input parameters.
 *
 */
IndexsearchUtils.createAffectedInfoContainer = function(changedIdValue, changedTable, action, columnsFn, oldValueFn, newValueFn) {
    var res, internalStorage;
    internalStorage = {};
    res = {
         id: changedIdValue
        ,table: changedTable
        ,action: action
        ,columns: null //null for autocomplete in the ADITO-designer
        ,oldValues: null 
        ,newValues: null
    };
    Object.defineProperty(res, "columns", {
        get: function(){
            if (internalStorage["columns"] == undefined)
                internalStorage["columns"] = columnsFn.call(null);
            return internalStorage["columns"];
        }
        ,set: function (v){
            internalStorage["columns"] = v;
        }
    });
    Object.defineProperty(res, "oldValues", {
        get: function(){
            if (internalStorage["oldValues"] == undefined)
                internalStorage["oldValues"] = oldValueFn.call(null);
            return internalStorage["oldValues"];
        }
        ,set: function (v){
            internalStorage["oldValues"] = v;
        }
    });
    Object.defineProperty(res, "newValues", {
        get: function(){
            if (internalStorage["newValues"] == undefined)
                internalStorage["newValues"] = newValueFn.call(null);
            return internalStorage["newValues"];
        }
        ,set: function (v){
            internalStorage["newValues"] = v;
        }
    });
    return res;
}


/**
 * Static utility class for constructing an indexsearch pattern
 */
function IndexsearchFilterUtils() {}

/**
 * Creates an indexsearch pattern from a filter
 * 
 * @param {object} pFilter the filter in json representation
 * 
 * @returns {IndexsearchFilterGroup|IndexsearchFilterRow} the IndexSearchFilter object
 */
IndexsearchFilterUtils.fromFilter = function(pFilter)
{
    if(pFilter["type"] == "group")
    {
        return IndexsearchFilterGroup.fromFilter(pFilter["childs"], pFilter["operator"]);
    }
    else if(pFilter["type"] == "row")
    {
        return IndexsearchFilterRow.fromFilter(pFilter["name"], pFilter["operator"], pFilter["value"]);
    }
    throw new Error("Unknown filter node type: " + pFilter["type"]);
}


/**
 * The IndexsearchFilterGroup object represents the a filter group
 * and is able to generate the corrosponding indexsearch pattern
 * 
 * @param {(IndexsearchFilterGroup|IndexsearchFilterRow)[]} pChilds the child filterrows/filtergroups
 * @param {string} pOperator the operator for the group can either be 'AND' or 'OR'
 */
function IndexsearchFilterGroup(pChilds, pOperator)
{
    this.childs = pChilds;
    this.operator = pOperator;
}

/**
 * Returns all fiels as Set witch are required for this filter
 * 
 * @returns {Set<string>} The fields as Set
 */
IndexsearchFilterGroup.prototype.getFields = function()
{
    var fields = new Set();
    for(let i = 0; i < this.childs.length; i++)
    {
        this.childs[i].getFields().forEach(fields.add, fields);
    }
    return fields;
}

/**
 * Builds the index pattern and returns it as string
 * 
 * @param {Record<string, string>} pFieldValues the field name value pair
 */
IndexsearchFilterGroup.prototype.buildQuery = function(pFieldValues)
{
    return this.childs.map(function(curr) { return curr.buildQuery(pFieldValues); })
    .filter(function(curr) { return curr != null; })
    .join(" " + this.operator + " ");
}

/**
 * Creates a new IndexsearchFilterGroup object
 * 
 * @param {(IndexsearchFilterGroup|IndexsearchFilterRow)[]} pChilds the child filterrows/filtergroups
 * @param {string} pOperator the operator for the group can either be 'AND' or 'OR'
 */
IndexsearchFilterGroup.fromFilter = function(pChilds, pOperator)
{
    return new IndexsearchFilterGroup(
        pChilds.map(function(curr)
        {
            return IndexsearchFilterUtils.fromFilter(curr);
        }),
        pOperator
    );
}


function IndexsearchFilterRow(pName, pEmpty, pExclude)
{
    this.name = pName;
    this.empty = pEmpty;
    this.exclude = pExclude;
}

/**
 * Returns all fiels as Set witch are required for this filter
 * 
 * @returns {Set<string>} The fields as Set
 */
IndexsearchFilterRow.prototype.getFields = function()
{
    return new Set([this.name]);
}

/**
 * Builds the index pattern and returns it as string
 * 
 * @param {Record<string, string>} pFieldValues the field name value pair
 */
IndexsearchFilterRow.prototype.buildQuery = function(pFieldValues)
{
    var fieldValue = pFieldValues[this.name] ? pFieldValues[this.name] : "";
    if(!this.empty && fieldValue == "")
    {
        return null;
    }
    var valueStr = fieldValue;
    for(let i = 0; i < this.exclude.length; i++)
    {
        valueStr = valueStr.replace(new RegExp(this.exclude[i], "gi"), "");
    }
    return this.name.toLowerCase() + ':("' + indexsearch.escapeString(valueStr.trim().replace(/\s+/g, "")) + '")';
}

/**
 * Creates a new IndexsearchFilterGroup object
 * 
 * @param {string} pName the row field name
 * @param {string} pOperator the operator for the group can either be 'AND' or 'OR'
 * @param {string} pValue the field valze
 */
IndexsearchFilterRow.fromFilter = function(pName, pOperator, pValue)
{
    if(pOperator == "NOT_EQUAL" || pOperator == "CONTAINSNOT")
    {
        return new IndexsearchFilterRow(pName, true, JSON.parse(pValue));
    }
    else if(pOperator == "ISNOTNULL")
    {
        return new IndexsearchFilterRow(pName, false, []);
    }
    throw new Error("Unknown filterrow operator: " + pOperator);
}