Skip to content
Snippets Groups Projects
process.js 81.48 KiB
import("Util_lib");
import("Employee_lib");
import("KeywordData_lib");
import("Context_lib");
import("system.util");
import("system.datetime");
import("system.translate");
import("system.neon");
import("system.vars");
import("system.db");
import("system.project");
import("system.entities");
import("Sql_lib");
import("Keyword_lib");

/**
 * Provides functions for the work with attributes,<br>
 * like listing the available attributes for a context.<br>
 * <b><u>Don't instanciate this!</u></b>
 * 
 * @class
 */
function AttributeUtil () {}

/**
 * Returns all possible usageable contexts for attributes.
 *                              <p>
 * @return {Array}              Array with the useable contexts.
 */
AttributeUtil.getPossibleUsageContexts = function() 
{
    return [
        "Organisation",
        "Person",
        "Contract",
        "Product",
        "Activity",
        "Offer",
        "Order",
        "Employee",
        "Salesproject",
        "Campaign",
        "DocumentTemplate",
        "SupportTicket",
        "Leadimport",
        "ImportField"
    ];
}

/**
 * Gives an array of all available attributes for a context.<br>
 * This is used in the possibleItems process for the <br>
 * attribute id in AttributeRelation.<br>
 * 
 * @param {String} pObjectType                              <p>
 *                                                          The object type (context).<br>
 * @param {Boolean} pIncludeGroups=false (optional)         <p>
 *                                                          Description.<br>
 * @param {String[]} pFilteredAttributeIds=[] (optional)    <p>
 *                                                          Whitleist of attribute ids.<br>
 * @param {Object} pAttributeCount=null (optional)          <p>
 *                                                          Object with attribute ids and their count.<br>
 * @return {String[]}                                       <p>
 *                                                          Array of attribute ids.
 */
AttributeUtil.getPossibleAttributes = function (pObjectType, pIncludeGroups, pFilteredAttributeIds, pAttributeCount)
{
    if (pObjectType == null || (pFilteredAttributeIds && pFilteredAttributeIds.length == 0))
        return [];
    
    var attrSelect = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_PARENT_ID, ATTRIBUTE_TYPE")
                        .from("AB_ATTRIBUTE")
                        .join("AB_ATTRIBUTEUSAGE", "AB_ATTRIBUTEID = AB_ATTRIBUTE_ID")
                        .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)
                        .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE(), SqlBuilder.NOT_EQUAL())
                        .and("ATTRIBUTE_ACTIVE = 1");

    if (pAttributeCount)
    {
        for (let attributeId in pAttributeCount)
        {
            attrSelect.and(newWhere()
                            .or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId, SqlBuilder.NOT_EQUAL())
                            .or("AB_ATTRIBUTEUSAGE.MAX_COUNT", pAttributeCount[attributeId], SqlBuilder.GREATER())
                            .or("AB_ATTRIBUTEUSAGE.MAX_COUNT is null")
            );
        }
    }
        
    if (pFilteredAttributeIds)
    {
        var filteredIdChildren = AttributeUtil.getAllChildren(pFilteredAttributeIds);
        var allFilteredIds = pFilteredAttributeIds.concat(filteredIdChildren);
        
        attrSelect.andIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", allFilteredIds, SqlBuilder.IN())
    }

    if (!pIncludeGroups)
        attrSelect.and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.GROUP(), SqlBuilder.NOT_EQUAL());
    
    var attributes = attrSelect.table();
    
    //filter out groups without usable children
    if (pIncludeGroups && pAttributeCount)
    {
        var parentIds = {};
        attributes.forEach(function (attribute)
        {
            this[attribute[1]] = true;
        }, parentIds);
        attributes = attributes.filter(function (attribute)
        {
            return attribute[2].trim() != AttributeTypes.GROUP() || this[attribute[0]];
        }, parentIds);
    }
    
    return attributes.map(function (attribute)
    {
        return attribute[0];
    });
}

/**
 * Searches for possible values for a atttribute<br> 
 * and returns these. The values depend on<br>
 * the attributeType.<br>
 * 
 * @param {String} pAttributeId                         <p>
 *                                                      The id of the attribute.<br>
 * @param {Boolean} pAttributeType                      <p>
 *                                                      Type of the attribute that is <br>
 *                                                      specified with pAttributeId. The type <br>
 *                                                      needs to be passed to the function <br>
 *                                                      for better performance. (loading the<br>
 *                                                      type via attribute several times would<br>
 *                                                      be too slow)<br>
 * @param {Boolean} pIncludeInactives=false (optional)  <p>
 *                                                      Specifies if only active attributevalues <br>
 *                                                      or actives and inactives shall be returned,<br>
 *                                                      this is important when you want <br>
 *                                                      to search for attribute values.<br>
 * @return {Array}                                      <p>
 *                                                      2D-array with [id, value] as elements if<br>
 *                                                      the given attributeType has possible items,<br>
 *                                                      if not null is returned.<br>
 */
AttributeUtil.getPossibleListValues = function (pAttributeId, pAttributeType, pIncludeInactives)
{
    var attributeId = pAttributeId;
    var attrType = pAttributeType.trim();
    var onlyActives = !pIncludeInactives;
    if (attrType == AttributeTypes.COMBO())
    {
        var valuesSelect = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_NAME")
                                .from("AB_ATTRIBUTE")
                                .where("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", attributeId)
                                .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE());
                                
        if (onlyActives)
            valuesSelect.and("AB_ATTRIBUTE.ATTRIBUTE_ACTIVE", "1");
        
        var valueList = valuesSelect.orderBy("SORTING asc")
                                    .table();
            
        for (let i = 0; i < valueList.length; i++)
        {
            valueList[i][1] = translate.text(valueList[i][1]);
        }
        return valueList;
    }
    else if (attrType == AttributeTypes.BOOLEAN())
    {
        return [
            ["1", translate.text("Yes")],
            ["0", translate.text("No")]
            ];
    }
    else if (attrType == AttributeTypes.KEYWORD())
    {
        var attrKeyword = newSelect("DROPDOWNDEFINITION")
                                .from("AB_ATTRIBUTE")
                                .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", attributeId)
                                .cell();
            
        var keywords = KeywordData.getSimpleData(attrKeyword, null, onlyActives);
        return keywords;
    }
    else if (attrType == AttributeTypes.OBJECTSELECTION())
    {
        var [module, filter] = newSelect("DROPDOWNDEFINITION, DROPDOWNFILTER")
                                    .from("AB_ATTRIBUTE")
                                    .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", attributeId)
                                    .arrayRow();
        var objects = [];
        if (module)
        {
            var uid = "#UID";
            if (module == "Employee_entity")
                uid = "SHORT_UID";
            
            var title = "#CONTENTTITLE";
            var config = entities.createConfigForLoadingRows()
                .entity(module)
                .fields([uid, title]);
            if (filter)
            {
                filter = JSON.parse(filter);
                if (filter.filter)
                    config.filter(JSON.stringify(filter.filter));
            }
            var rows = entities.getRows(config);
            for (let i = 0, l = rows.length; i < l; i++)
                objects.push([rows[i][uid], rows[i][title]])
        }
        return objects;
    }
    else
        return null;
}

/**
 * Returns the name of an attribute <br>
 * with all parent attribute names.<br>
 * 
 * @param {String} pAttributeId                         <p>
 *                                                      The id of the attribute.<br>
 * @param {Boolean} pSimpleName=false (optional)        <p>
 *                                                      Use only the name of the attribute <br>
 *                                                      and not the names of the parents.<br>
 * @param {Boolean} pTranslate=true (optional)          <p>
 *                                                      If the name should be translated.<br>
 * @param {Number} pStartLayer=0 (optional)             <p>
 *                                                      Group names to omit. Example: The attribute <br>
 *                                                      is "Departments / Distribution / Field staff".<br>
 *                                                      If you set this value to 1, "Departments / " will <br>
 *                                                      be removed, if set to 2, "Departments / Distribution /"<br>
 *                                                      will be removed and so on. The last name will never <br>
 *                                                      be removed to avoid an empty result <br>
 *                                                      if something is wrong.<br>
 * @return {String}                                     <p>
 *                                                      The name of the attribute.<br>
 */
AttributeUtil.getFullAttributeName = function (pAttributeId, pSimpleName, pTranslate, pStartLayer) 
{
    if (pSimpleName === undefined)
        pSimpleName = false;
    if (pTranslate === undefined)
        pTranslate = true;
    
    if (!pAttributeId)
        return "";
    if (pSimpleName)
        return AttributeUtil.getSimpleAttributeName(pAttributeId, pTranslate);
    var attributeNames = [];
    var attribute;
    do {
        attribute = newSelect("ATTRIBUTE.ATTRIBUTE_NAME, PARENT1.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_PARENT_ID")
                        .from("AB_ATTRIBUTE", "ATTRIBUTE")
                        .leftJoin("AB_ATTRIBUTE", "ATTRIBUTE.ATTRIBUTE_PARENT_ID = PARENT1.AB_ATTRIBUTEID", "PARENT1")
                        .leftJoin("AB_ATTRIBUTE", "PARENT1.ATTRIBUTE_PARENT_ID = PARENT2.AB_ATTRIBUTEID", "PARENT2")
                        .where(["AB_ATTRIBUTE", "AB_ATTRIBUTEID", "ATTRIBUTE"], pAttributeId)
                        .arrayRow();

        if (attribute.length > 0)
        {
            attributeNames.unshift(attribute[0]);
            if (attribute[1])
                attributeNames.unshift(attribute[1]);
            if (attribute[2])
                attributeNames.unshift(attribute[2]);
            pAttributeId = attribute[3];
        }
        else
            pAttributeId = "";
    } while (pAttributeId);
    
    if (pStartLayer)
        attributeNames = attributeNames.slice(pStartLayer < attributeNames.length ? pStartLayer : -1);
        
    if (pTranslate)
    {
        attributeNames = attributeNames.map(function (name)
        {
            return translate.text(name);
        });
    }
    return attributeNames.join(" / ");
}

/**
 * Returns the name of an attribute.
 * 
 * @param {String} pAttributeId                 <p>
 *                                              The id of the attribute.<br>
 * @param {boolean} pTranslate                  <p>
 *                                              If the name should be translated.<br>
 * @return {String}                             <p>
 *                                              The name of the attribute.<br>
 */
AttributeUtil.getSimpleAttributeName = function (pAttributeId, pTranslate) 
{
    var attributeName = newSelect("ATTRIBUTE_NAME")
                            .from("AB_ATTRIBUTE")
                            .whereIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId)
                            .cell(true, "");
    if (pTranslate)
        attributeName = translate.text(attributeName);
    return attributeName;
}

/**
 * Returns the ids of all subordinated attributes of an attribute.
 * 
 * @param {String|Array} pAttributeIds              <p>
 *                                                  The id(s) of the attribute(s).<br>
 * @return {String[]}                               <p>
 *                                                  Array with the ids of every subordinated attribute.<br>
 */
AttributeUtil.getAllChildren = function (pAttributeIds)
{
    var childIds = [];
    if (typeof(pAttributeIds) == "string")
        pAttributeIds = [pAttributeIds];
        
    while (pAttributeIds.length > 0)
    {
        pAttributeIds = newSelect("AB_ATTRIBUTEID")
                            .from("AB_ATTRIBUTE")
                            .where("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeIds, SqlBuilder.IN())
                            .arrayColumn();

        if (pAttributeIds.length > 0)
            childIds = childIds.concat(pAttributeIds);
    }
    return childIds;
}

/**
 * Checks if an attribute has attribute relations.
 * 
 * @param {String} pAttributeId                 <p>
 *                                              The id of the attribute.<br>
 * @return {Boolean}                            <p>
 *                                              True, if it has relations.<br>
 */
AttributeUtil.hasRelations = function (pAttributeId)
{
    if (!pAttributeId)
        return false;
    return new AttributeRelationQuery()
        .attributeId(pAttributeId)
        .getAttributeCount() != 0;
}

/**
 * Returns the type of an attribute.
 * 
 * @param {String} pAttributeId                 <p>
 *                                              The id of the attribute.<br>
 * @return {String}                             <p>
 *                                              Attribute type.<br>
 */
AttributeUtil.getAttributeType = function (pAttributeId)
{
    if (!pAttributeId)
        return "";

    return newSelect("ATTRIBUTE_TYPE")
                .from("AB_ATTRIBUTE")
                .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId)
                .cell()
                .trim();
}

/**
 * Checks whether the given object type <br>
 * has attribute in usage.<br>
 * 
 * @param {String} pObjectType                  <p>
 *                                              The object type.<br>
 * @return {String}                             <p>
 *                                              Returns false whether the given object<br>
 *                                              type is not filled correctly and true<br>
 *                                              if the given object type has attributes<br>
 *                                              in usage.<b>
 */
AttributeUtil.hasAttributes = function (pObjectType)
{
    if (!pObjectType)
        return false;
    return newSelect("count(*)")
                .from("AB_ATTRIBUTEUSAGE")
                .join("AB_ATTRIBUTE", "AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)
                .and("AB_ATTRIBUTE.ATTRIBUTE_ACTIVE", "1")
                .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE(), SqlBuilder.NOT_EQUAL())
                .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.GROUP(), SqlBuilder.NOT_EQUAL())
                .cell() != "0"; //TODO: is there a way exists could be used?
}

/*********************************************************************************************************************/

/**
 * Provides functions for the work with attributeRelations,<br>
 * getting the value of an attributeRelation for an object.<br>
 * <b>Don't instanciate this!</b>
 * 
 * @class
 */
function AttributeRelationUtils () {}

/**
 * @deprecated use AttributeRelationQuery
 * 
 * gets the value of an attributeRelation for one dataset (e. g. a person)
 * 
 * @param {String} pAttributeId             <p>
 *                                          Attribute id.<br>
 * @param {String} pObjectRowId             <p>
 *                                          Row id of the dataset.<br>
 * @param {String} pObjectType=null         <p>
 *                                          Object type.<br>
 * @param {String} pGetViewValue=false      <p>
 *                                          If true, the values are resolved and formatted.<br>
 * @param {String} pGetAttrname=false       <p>
 *                                          If true, the attributename is also returned.<br>
 * @return {String|String[]|null}           <p>
 *                                          The value of the attribute or an array<br>
 *                                          of attrname and value [attrname, value]<br>
 *                                          (if pGetAttrname is true)<br>
 */
AttributeRelationUtils.getAttribute = function (pAttributeId, pObjectRowId, pObjectType, pGetViewValue, pGetAttrname)
{
    var attributeQuery = new AttributeRelationQuery(pObjectRowId, pAttributeId, pObjectType);
    
    if (pGetViewValue)
        attributeQuery.includeDisplayValue();
    if (pGetAttrname)
        attributeQuery.includeFullAttributeName();
    
    var attribute = attributeQuery.getSingleAttribute();
    var value = pGetViewValue ? attribute.displayValue : attribute.value;
    
    return pGetAttrName ? [attribute.fullAttributeName, value] : value;
}

/**
 * Get a SqlBuilder already containing <br>
 * the full select for attributes.<br>
 * 
 * @param {String[]} pFields                <p>
 *                                          Array of all fields which should be selected.<br>
 * @param {String} pObjectRowId             <p>
 *                                          Object row id.<br>
 * @param {String} [pObjectType=null]       <p>
 *                                          Object type.<br>
 * @return {SqlBuilder}                     <p>
 *                                          A already filled SqlBuilder.<br>
 */
AttributeRelationUtils.getAttributeSqlBuilder = function (pFields, pObjectRowId, pObjectType)
{
    return newSelect(pFields)
                .from("AB_ATTRIBUTERELATION")
                .join("AB_ATTRIBUTE", "AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                .leftJoin("AB_ATTRIBUTE COMBOVAL", AttributeTypes.COMBO.databaseField + " = COMBOVAL.AB_ATTRIBUTEID")
                .whereIfSet("AB_ATTRIBUTERELATION.OBJECT_ROWID", pObjectRowId)
                .andIfSet("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType);
}

/**
 * @deprecated use AttributeRelationQuery
 * Gets all attributes for a dataset.
 * 
 * @param {String} pObjectRowId                 <p>
 *                                              Object row id.<br>
 * @param {String} pObjectType=null             <p>
 *                                              Object type.<br>
 * @param {String} pUseAttributeIds=0           <p>
 *                                              <ul>
 *                                              <li>0: The full attribute names are returned.<br></li>
 *                                              <li>1: The ids are used instead of the full attribute names.<br></li>
 *                                              <li>2: The ids and the full attribute name is returned.<br></li>
 *                                              </ul>
 * @param {String} pUseIdValues=false           If true the values are not resolved or formatted<br>
 *                                              [attributeId, attributeName, value].<br>
 * @return {String[][]}                         <p>
 *                                              Two-dimensional array a row is [attributeId|attributeName, value]<br>
 *                                              (or [attributeId, attributeName, value]).<br>
 */
AttributeRelationUtils.getAllAttributes = function (pObjectRowId, pObjectType, pUseAttributeIds, pUseIdValues)
{    
    var attributeQuery = new AttributeRelationQuery(pObjectRowId, pObjectType);
    
    if (!pUseAttributeIds || pUseAttributeIds == 2)
        attributeQuery.includeFullAttributeName();
    if (!pUseIdValues)
        attributeQuery.includeDisplayValue();
    
    return attributeQuery.getAttributes().map(function (row)
    {
        var value = pUseIdValues ? row.value : row.displayValue;
        switch (pUseAttributeIds)
        {
            case 1:
                return [row.attributeId, value];
            case 2:
                return [row.attributeId, row.fullAttributeName, value];
            case 0:
            default:
                return [row.fullAttributeName, value];
        }
    });
}

/**
 * Gets the correct attribute value from <br>
 * a map with values depending on the attribute id.<br>
 * 
 * @param {String} pAttributeId                 <p>
 *                                              The attribute id.
 * @param {Object} pValueMap                    <p>
 *                                              A map with the attribute values <br>
 *                                              and the db fields as keys.<br>
 * @param {Boolean} pGetViewValue=false         <p>
 *                                              If true, get the view value.<br>
 * @return {String|null}                        <p>
 *                                              The value of the attribute or null <br>
 *                                              if the attribute doesn't exist.<br>
 */
AttributeRelationUtils.selectAttributeValue = function (pAttributeId, pValueMap, pGetViewValue)
{
    var type = newSelect("ATTRIBUTE_TYPE, DROPDOWNDEFINITION")
                    .from("AB_ATTRIBUTE")
                    .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId)
                    .arrayRow();
                    
    if (!type.length)
        return null;
    
    type[0] = type[0].trim();
    var field = AttributeTypeUtil.getDatabaseField(type[0]);
    var value = pValueMap[field];
    if(value == undefined)
        return "";
    if (pGetViewValue && type[0] == AttributeTypes.COMBO())
    {
        value = newSelect("ATTRIBUTE_NAME")
                    .from("AB_ATTRIBUTE")
                    .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", value)
                    .cell();
    }
    else if (pGetViewValue)
        value = AttributeTypeUtil.getAttributeViewValue(type[0], value, type[1]);
    
    return value;
}

/**
 * @deprecated use AttributeRelationQuery.prototype.insertAttribute
 * 
 * Inserts an attribute relation and validates if it can be used for the context and 
 * it also heeds the max usage count.
 * 
 * @return {boolean} true, if the attribute relation was inserted
 */
AttributeRelationUtils.setAttribute = function (pRowId, pObjectType, pAttributeId, pValue)
{
    return new AttributeRelationQuery(pRowId, pAttributeId, pObjectType)
        .insertAttribute(pValue, false);
}

/**
 * @deprecated use AttributeRelationQuery.prototype.insertAttribute
 * 
 * inserts an attribute without validating the count
 */
AttributeRelationUtils.insertAttribute = function (pRowId, pObjectType, pAttributeId, pValue)
{
    return new AttributeRelationQuery(pRowId, pAttributeId, pObjectType)
        .insertAttribute(pValue, true);
}

/**
 * Adds rows for attributes with min_count > 0.
 * 
 * @param {String} pObjectType              <p>
 *                                          The object type.<br>
 * @param {String} pConsumer                <p>
 *                                          The name of the attribute relation consumer.<br>
 * @param {String[]} pFiltered              <p>
 *                                          Array of attributeId's which act as a whitelist.<br>
 *                                          (groups are resolves to the childid's)<br>
 */
AttributeRelationUtils.presetMandatoryAttributes = function (pObjectType, pConsumer, pFiltered)
{
    var mandatoryAttributesSelect = newSelect("AB_ATTRIBUTE_ID, MIN_COUNT")
                                    .from("AB_ATTRIBUTEUSAGE")
                                    .join("AB_ATTRIBUTE", "AB_ATTRIBUTE_ID = AB_ATTRIBUTEID")
                                    .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)
                                    .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE(), SqlBuilder.NOT_EQUAL())
                                    .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.GROUP(), SqlBuilder.NOT_EQUAL())
                                    .and("ATTRIBUTE_ACTIVE = 1")
                                    .and("MIN_COUNT > 0");
                                    
    if (pFiltered)
    {
        var possibleIds = AttributeUtil.getPossibleAttributes(pObjectType, false, pFiltered);
        if (possibleIds.length > 0)
            mandatoryAttributesSelect.and("AB_ATTRIBUTE.AB_ATTRIBUTEID", possibleIds, SqlBuilder.IN())
        else
            return;
    }

    var mandatoryAttributes = mandatoryAttributesSelect.table();
    mandatoryAttributes.forEach(function (usage)
    {
        //adding an attribute more than 20 times would be too much (having a min_count > 20 is very unlikely)
        for (let i = 0; i < usage[1] && i < 20; i++)
            neon.addRecord(pConsumer, {
                "AB_ATTRIBUTE_ID" : usage[0]
            });
    });
}

/**
 * Clears rows of attribute.
 * 
 * @param {String} pConsumer                <p>
 *                                          The name of the attribute relation consumer.<br>
 */
AttributeRelationUtils.clearAttributes = function (pConsumer)
{
    var insertedLinks = vars.get("$field." + pConsumer + ".insertedRows");
    var updatedLinks = vars.get("$field." + pConsumer + ".changedRows");
        
    insertedLinks.concat(updatedLinks).forEach(function (link)
    {
        if (link["AB_ATTRIBUTE_ID"])
            neon.deleteRecord(pConsumer, link["#UID"]);
    });
}

/**
 * Checks if the count of the used attributes <br>
 * is valid and returns a message if it's not.<br>
 * 
 * @param {String} pRowId                   <p>
 *                                          The row id of the entity.<br>
 * @param {String} pObjectType=null         <p>
 *                                          The object type.<br>
 * @param {String} pConsumerField           <p>
 *                                          The name of the attribute relation consumer.<br>
 * @param {String} pFilteredAttributeIds    <p>
 *                                          Filters the attributes that are, validated<br>
 *                                          this should be the same as the<br>
 *                                          FilteredAttributeIds_param.<br>
 * @return {String}                         <p> 
 *                                          The validation message or an empty<br>
 *                                          string if everything is ok.<br>
 */
AttributeRelationUtils.validateAttributeCount = function (pRowId, pObjectType, pConsumerField, pFilteredAttributeIds)
{
    var attributeChanges = {};
    var deletedRows = vars.get("$field." + pConsumerField + ".deletedRows");
    var changedRows = vars.get("$field." + pConsumerField + ".changedRows");
    var insertedRows = vars.get("$field." + pConsumerField + ".insertedRows");

   if (deletedRows)
    {
        deletedRows.forEach(function (row)
        {
            this[row.UID] = "";
        }, attributeChanges);
    }
    
    if (changedRows)
    {
        changedRows.forEach(function (row)
        {
            this[row.UID] = row.AB_ATTRIBUTE_ID;
        }, attributeChanges);
    }
    
    //get the current count of usages considering the changes
    //this will merge the counts of attributeChanges and the already stored attributerelations
    var countObj = AttributeRelationUtils.countAttributeRelations(pRowId, pObjectType, attributeChanges);
    
    if (insertedRows) //append the new rows
    {
        insertedRows.forEach(function (row)
        {
            this[row.AB_ATTRIBUTE_ID] = (this[row.AB_ATTRIBUTE_ID] || 0) + 1;
        }, countObj);
    }
    if (changedRows) //append the new rows (if they are added by default but not filled with a value)
    {
        changedRows.forEach(function (row)
        {
            if(!row.DATE_NEW && !row.USER_NEW)
            {
                this[row.AB_ATTRIBUTE_ID] = (this[row.AB_ATTRIBUTE_ID] || 0) + 1;
            }
        }, countObj);
    }
    
    var possibleAttributes = AttributeUtil.getPossibleAttributes(pObjectType, undefined, pFilteredAttributeIds);
    var minMaxCounts = [];
    
    if (possibleAttributes.length > 0)
    {
        var minMaxCountsSelect = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_NAME, MIN_COUNT, MAX_COUNT")
                                    .from("AB_ATTRIBUTEUSAGE")
                                    .join("AB_ATTRIBUTE", "AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                                    .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", possibleAttributes, SqlBuilder.IN())
                                    .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)

        //retrieve all min/max counts of the possible attributes
        minMaxCounts = minMaxCountsSelect.table();
    }
        
    var validationMessage = [];
    minMaxCounts.forEach(function ([attributeId, name, minCount, maxCount])
    {
        let count = this[attributeId] || 0;
        //compares the actual usage with the min and max count and generates a message if the usage is too low or too high
        if (count < minCount)
            validationMessage.push(translate.withArguments("Attribute \"%0\" has to be used at least %1.", [name, _getTranslatedCount(minCount)]));
        if (maxCount && count > maxCount)
            validationMessage.push(translate.withArguments("Attribute \"%0\" can't be used more than %1.", [name, _getTranslatedCount(maxCount)]));
    }, countObj);
    
    return validationMessage.join("\n");
    
    //returns the correct count expression by choosing either singular (1 time) or plural (2 times)
    function _getTranslatedCount (pCount)
    {
        if (pCount == 1)
            return pCount + " " + translate.text("${COUNT_PREPOSITION_SINGLE}");
        return pCount + " " + translate.text("${COUNT_PREPOSITION_MULTIPLE}");
    }
}

/**
 * Counts attribute relations.
 * 
 * @param {String} pRowId                   <p>
 *                                          The row id of the entity.<br>
 * @param {String} pObjectType=null         <p>
 *                                          The object type.<br>
 * @param {Object} pAttributeChanges=null   <p>
 *                                          Object containing changes and deletions <br>
 *                                          of attributes structure = {attributeRelationId <br>
 *                                          : new attributeId or "" when deleted}<br>
 * @return {Object}                         <p>
 *                                          Object with attribute ids and the count of <br>
 *                                          the usage (without new rows).<br>
 */
AttributeRelationUtils.countAttributeRelations = function (pRowId, pObjectType, pAttributeChanges)
{
    //use cases:
    //complete new row ==> increase count by 1 for that attribute [this is done in another function]
    //row removed ==> decrease count by 1
    //row edit: replace one attribute by another attribute ==> increase the new attribute count and decrease the old attribute count
    //row edit: replace attribute with no new value ==> decrease count for the old attribute
    //unchanged (already stored) row ==> increase count 
    var countObj = {};

    storedAttributeRelations = new AttributeRelationQuery()
        .objectRowId(pRowId)
        .objectType(pObjectType)
        .getAttributes();
                        
    storedAttributeRelations.forEach(function (storedRow) {
        var storedAttributeId = storedRow.attributeId;
        var storedAttrRelationId = storedRow.attributeRelationId;
        var currentAttributeId = storedAttributeId;
        //merging the data that is stored in the DB and the provided changes
        if (pAttributeChanges && storedAttrRelationId in pAttributeChanges)
            currentAttributeId = pAttributeChanges[storedAttrRelationId];
        
        // it doesn't matter if a row has been deleted or if the attribute has been set to "nothing"
        if (currentAttributeId == "")
            _decrCount(storedAttributeId);
        else
        {
            _incrCount(currentAttributeId);
            if (currentAttributeId != storedAttributeId)
                _decrCount(storedAttributeId);
        }
    });
    
    function _incrCount(pAttributeId)
    {
        if (countObj[pAttributeId])
            countObj[pAttributeId]++;
        else
            countObj[pAttributeId] = 1;
    }
    
    function _decrCount(pAttributeId)
    {
        if (countObj[pAttributeId])
            countObj[pAttributeId]--;
        else
            countObj[pAttributeId] = 0;
    }
    
    return countObj;
}


/*********************************************************************************************************************/


/**
 * Object for the enumeration and management of all attribute types.
 * This Object is only for the general definition of attribute types and for getting
 * data about every type, anything that has to do with a specific attribute (= the function requires an attribute id)
 * should be done in AttributeUtils.
 * 
 * Every AttributeType also needs a keyword entry for the keyword container 'AttributeType'
 */
function AttributeTypes () {}

AttributeTypes.TEXT = function () {return "TEXT";}
AttributeTypes.DATE = function () {return "DATE";}
AttributeTypes.NUMBER = function () {return "NUMBER";}
AttributeTypes.BOOLEAN = function () {return "BOOLEAN";}
AttributeTypes.COMBO = function () {return "COMBO";}
AttributeTypes.COMBOVALUE = function () {return "COMBOVALUE";}
AttributeTypes.GROUP = function () {return "GROUP";} 
AttributeTypes.KEYWORD = function () {return "KEYWORD";}
AttributeTypes.VOID = function () {return "VOID";}
AttributeTypes.MEMO = function () {return "MEMO";}
AttributeTypes.OBJECTSELECTION = function () {return "OBJECTSELECTION";}
AttributeTypes.THEME = function () {return "THEME";}
AttributeTypes.INTEGER = function () {return "INTEGER";}

/**
 * Object for representing a single AttributeType. Note: The entries in AttributeTypes are not actually of this type, so "instanceof" will not
 * work for them. The default properties of an attribute type are still initialized by calling this constructor, to make at least code
 * completion possible.
 */
function AttributeType ()
{
    /**
     * Returns a String representation of the AttributeType
     * 
     * @return {String} String that matches the keyId of the AttributeType Keyword
     */
    this.toString = function () {return this();};
    /**
     * ContentType of the value field
     */
    this.contentType = null;
    /**
     * Database field that should hold the value
     */
    this.databaseField = null;
    /**
     * If set to true, max usage count is always 1 (optional)
     */
    this.singleSelection = false;
    /**
     * Array of attribute types that can have this attribute as parent (optional)
     */
    this.possibleChildren = null;
    /**
     * Name of the dropdowndefinition field (optional)
     */
    this.dropDownDefinitionTitle = "";
    /**
     * Use the lookup field
     */
    this.useLookup = false;
    /**
     * Function that returns an sub-sql to resolve the attribute display value (optional)
     * 
     * @param {Object} pAttributeData   Object with the attribute properties
     * @return {String} sql expression that resolves the display value
     */
    this.getDisplayValueSql = function (pAttributeData)
    {
        return this.databaseField;
    }
    /**
     * Function to resolve the display value (optional)
     * 
     * @param {String} pValue   raw value
     * @return {String} the display value
     */
    this.getViewValue = function (pValue)
    {
        return pValue;
    }
    /**
     * Function to load all possible values for "dropdowndefinition" (optional)
     * 
     * @return {Array|null} array that can be used in the dropDownProcess (if defined)
     */
    this.getDropDownDefinitions = function () 
    {
        return null;
    }
    /**
     * Function to define validation parameters that can be set for an attribute (optional)
     * 
     * @return {Object[]} dynamicForm definition for the parameter fields
     */
    this.getValidationParameters = function () {},
    /**
     * Function to validate the entered attribute value (optional)
     * 
     * @param {String} pValue               the value to be validated
     * @param {Object} pValidationParams    validation parameters defined for the attribute
     * @return {String|Boolean} A validation message string if the validation failed, true if the value is valid
     */
    this.validateValue = function (pValue, pValidationParams)
    {
        return true;
    }
}

{   //block for encapsulation
    for (let typeName in AttributeTypes)
    {
        AttributeType.call(AttributeTypes[typeName]);
    }
}

//the "get" function is defined like this so it does not show up in for..in loops (enumerable: false)
Object.defineProperty(AttributeTypes, "get", {
    enumerable: false, 
    writable: true
});
/**
 * Get the AttributeType object with the given name. 
 * 
 * @param {String} pAttributeTypeName name of the attribute type
 * @return {AttributeType} the attribute type, or null if the given name was invalid
 */
AttributeTypes.get = function (pAttributeTypeName)
{
    if (!pAttributeTypeName)
        return null;
    return AttributeTypes[pAttributeTypeName.toString().trim()] || null;
}

/*** In the following section, custom properties are defined for every AttributeType ***/

Object.assign(AttributeTypes.TEXT, {
    contentType: "TEXT",
    databaseField: "CHAR_VALUE",
    getValidationParameters: function ()
    {
        return [{
            id: "regExp",
            name: translate.text("Regular expression"),
            contentType: "TEXT",
            isReadable: true,
            isWritable: true,
            isRequired: false,
            value: null
        }];
    },
    validateValue: function (pValue, pValidationParams)
    {
        if (pValidationParams && pValidationParams.regExp)
        {
            var regExParts = pValidationParams.regExp.match(new RegExp('^/(.*?)/([gimy]*)$'));
            var regEx;
            if (regExParts)
                regEx = new RegExp(regExParts[1], regExParts[2]);
            else
                regEx = new RegExp(pValidationParams.regExp);
            if (!regEx.test(pValue))
                return translate.text("Invalid value");
        }
        return true;
    }
});
Object.assign(AttributeTypes.DATE, {
    contentType: "DATE", 
    databaseField: "DATE_VALUE",
    getViewValue: function (pValue)
    {
        return datetime.toDate(pValue, translate.text("dd.MM.yyyy"));
    }
});
Object.assign(AttributeTypes.NUMBER, {
    contentType: "NUMBER", 
    databaseField: "NUMBER_VALUE",
    getValidationParameters: function ()
    {
        return [{
            id: "minValue",
            name: translate.text("Minimum"),
            contentType: "NUMBER",
            isReadable: true,
            isWritable: true,
            isRequired: false,
            value: null
        },{
            id: "maxValue",
            name: translate.text("Maximum"),
            contentType: "NUMBER",
            isReadable: true,
            isWritable: true,
            isRequired: false,
            value: null
        }];
    },
    validateValue: function (pValue, pValidationParams)
    {
        if (pValidationParams)
        {   
            pValue = Number(pValue);
            if (!Utils.isNullOrEmptyString(pValidationParams.minValue) && pValue < pValidationParams.minValue)
                return translate.withArguments("Value is too small, the minimum is %0", [pValidationParams.minValue]);
            if (!Utils.isNullOrEmptyString(pValidationParams.maxValue) && pValue > pValidationParams.maxValue)
                return translate.withArguments("Value is too big, the maximum is %0", [pValidationParams.maxValue]);
        }
        return true;
    }
});
Object.assign(AttributeTypes.BOOLEAN, {
    contentType: "BOOLEAN", 
    databaseField: "INT_VALUE",
    singleSelection: true,
    getDisplayValueSql: function (pAttributeData)
    {
        var valueField = "AB_ATTRIBUTERELATION." + this.databaseField;
        return SqlBuilder.caseWhen(valueField, "1").thenString(translate.text("Yes"))
            .when(valueField, "0").thenString(translate.text("No"));
    },
    getViewValue: function (pValue)
    {
        return Utils.toBoolean(pValue) ? translate.text("Yes") : translate.text("No");
    }
});
Object.assign(AttributeTypes.COMBO, {
    contentType: "UNKNOWN",
    databaseField: "ID_VALUE",
    possibleChildren: [AttributeTypes.COMBOVALUE()],
    //in most cases the view value of this attribute type is loaded via a direct sql join for less queries and better performance
    getDisplayValueSql: function (pAttributeData)
    {
        var valueField = "AB_ATTRIBUTERELATION." + this.databaseField;
        var values = newSelect(["AB_ATTRIBUTEID", "ATTRIBUTE_NAME"])
            .from("AB_ATTRIBUTE")
            .where("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeData.attributeId)
            .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE())
            .table();
        var sql = SqlBuilder.caseStatement(values, valueField);
        values.forEach(function ([key, value])
        {
            sql.when(valueField, key).thenString(translate.text(value));
        });
        return sql.toString();
    },
    getViewValue: function (pValue)
    {
        var viewValue = newSelect("AB_ATTRIBUTE.ATTRIBUTE_NAME")
            .from("AB_ATTRIBUTE")
            .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", pValue)
            .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE())
            .cell();
        return viewValue ? translate.text(viewValue) : viewValue;
    }
});
Object.assign(AttributeTypes.GROUP, {
    possibleChildren: [
        AttributeTypes.GROUP(), 
        AttributeTypes.TEXT(), 
        AttributeTypes.NUMBER(), 
        AttributeTypes.COMBO(), 
        AttributeTypes.VOID(), 
        AttributeTypes.THEME(), 
        AttributeTypes.KEYWORD(), 
        AttributeTypes.OBJECTSELECTION(), 
        AttributeTypes.MEMO(), 
        AttributeTypes.DATE(), 
        AttributeTypes.BOOLEAN()
    ]
});
Object.assign(AttributeTypes.KEYWORD, {
    contentType: "UNKNOWN", 
    databaseField: "ID_VALUE", 
    getDisplayValueSql: function (pAttributeData)
    {
        var valueField = "AB_ATTRIBUTERELATION." + this.databaseField;
        return KeywordUtils.getResolvedTitleSqlPart(pAttributeData.dropDownDefinition, valueField);
    },
    getViewValue: function (pValue, pKeyword)
    {
        return KeywordUtils.getViewValue(pKeyword, pValue);
    },
    dropDownDefinitionTitle: "Keyword",
    getDropDownDefinitions: function ()
    {
        return KeywordUtils.getContainerNames().map(function (e)
        {
            return [e, e];//currently the first column is ID, second view value - which is the same because there is no ID for keyword-containers
        });
    }
});
Object.assign(AttributeTypes.VOID, {
    possibleChildren: [AttributeTypes.VOID()],
    singleSelection: true
});
Object.assign(AttributeTypes.MEMO, {
    contentType: "LONG_TEXT", 
    databaseField: "CHAR_VALUE"
});
Object.assign(AttributeTypes.OBJECTSELECTION, {
    contentType: "UNKNOWN",
    databaseField: "ID_VALUE",
    useLookup: true,
    getViewValue: function (pValue, pModule)
        {
            if (pValue)
            {
                if (pModule == "Employee_entity")
                    pValue = EmployeeUtils.prefixUserId(pValue);
                
                var title = "#CONTENTTITLE";
                var config = entities.createConfigForLoadingRows()
                    .entity(pModule)
                    .fields([title])
                    .uid(pValue);
                // first check count to avoid errors at getRow (make sure entry exists)
                var count = entities.getRowCount(config);
                if (count && count > 0) {
                    var rows = entities.getRow(config);
                    pValue = rows ? rows[title] : pValue;
                } else {
                    // return null if entry does not exist 
                    // -> react accordingly when calling this function
                    pValue = null;
                }
            }
            return pValue;
        },
    dropDownDefinitionTitle: "Module",
    getDropDownDefinitions: function ()
        {
            // TODO: use loadEntity from context_entity
            var dropDownList = [];
            project.getDataModels(project.DATAMODEL_KIND_ENTITY).forEach(
                function (entity)
                {
                    if (entity[1] && AttributeTypes.OBJECTSELECTION._selectableEntities.has(entity[0]))
                        dropDownList.push([entity[0], translate.text(entity[1])]);
                }
            );
            return dropDownList;
        },
    /** @private */
    _selectableEntities: new Set([
        "ObjectRelationType_entity",
        "DocumentTemplate_entity",
        "SupportTicket_entity",
        "Organisation_entity",
        "Salesproject_entity",
        "Productprice_entity",
        "SerialLetter_entity",
        "AnyContact_entity",
        "Salutation_entity",
        "Attribute_entity",
        "Activity_entity",
        "Contract_entity",
        "Campaign_entity",
        "BulkMail_entity",
        "Employee_entity",
        "Language_entity",
        "Product_entity",
        "Person_entity",
        "Offer_entity",
        "Order_entity",
        "Task_entity",
        "Role_entity"
    ])
});
Object.assign(AttributeTypes.THEME, {
    contentType: "LONG_TEXT",
    databaseField: "CHAR_VALUE",
    possibleChildren: [AttributeTypes.THEME()]
});
Object.assign(AttributeTypes.INTEGER, {
    contentType: "NUMBER",
    databaseField: "INT_VALUE",
    getValidationParameters: function ()
    {
        return [{
            id: "minValue",
            name: translate.text("Minimum"),
            contentType: "NUMBER",
            isReadable: true,
            isWritable: true,
            isRequired: false,
            value: null
        },{
            id: "maxValue",
            name: translate.text("Maximum"),
            contentType: "NUMBER",
            isReadable: true,
            isWritable: true,
            isRequired: false,
            value: null
        }];
    },
    validateValue: function (pValue, pValidationParams)
    {
        if (!Utils.isInteger(pValue))
            return translate.text("Value must be an integer");
        if (pValidationParams)
        {   
            pValue = Number(pValue);
            if (!Utils.isNullOrEmptyString(pValidationParams.minValue) && pValue < pValidationParams.minValue)
                return translate.withArguments("Value is too small, the minimum is %0", [pValidationParams.minValue]);
            if (!Utils.isNullOrEmptyString(pValidationParams.maxValue) && pValue > pValidationParams.maxValue)
                return translate.withArguments("Value is too big, the maximum is %0", [pValidationParams.maxValue]);
        }
        return true;
    }
})

//reference for compatibility with old name
var $AttributeTypes = AttributeTypes;

function AttributeTypeUtil () {}

/**
 * Returns the required contentType for the given attribute type.
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type (use the values of the AttributeTypes<br>
 *                                              object, e. g. AttributeTypes.TEXT)<br>
 * @return {String}                             <p>
 *                                              The contentType for the attribute.<br>
 */
AttributeTypeUtil.getContentType = function (pAttributeType)
{
    var type = AttributeTypes.get(pAttributeType);
    return type ? type.contentType : null;
}

/**
 * Returns if the type is a group type.
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type (use the values of the <br>
 *                                              AttributeTypes object, e. g. AttributeTypes.TEXT)
 * @return {Boolean}                            <p>
 *                                              If the type is a group type it returns true.
 */
AttributeTypeUtil.isGroupType = function (pAttributeType)
{
    var type = AttributeTypes.get(pAttributeType);
    return type && !Utils.isNullOrEmpty(type.possibleChildren);
}

/**
 * Returns the database field for the given<br>
 * attribute type that holds the value of the<br>
 * attribute.<br>
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type (use the values of<br>
 *                                              the AttributeTypes object, e.g.<br>
 *                                              AttributeTypes.TEXT)<br>
 * @return {String}                             <p>
 *                                              The database field for the attribute.<br>
 */
AttributeTypeUtil.getDatabaseField = function (pAttributeType)
{
    var type = AttributeTypes.get(pAttributeType);
    return type ? type.databaseField : null;
}

/**
 * Returns the possible children types for the given attribute type.<br>
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type (use the values <br>
 *                                              of the AttributeTypes object, e. g.<br>
 *                                              AttributeTypes.TEXT)<br>
 * @return {String[]|null}                      <p>
 *                                              The possible children types, can be null.<br>
 */
AttributeTypeUtil.getPossibleChildren = function (pAttributeType)
{
    var type = AttributeTypes.get(pAttributeType);
    return type ? type.possibleChildren : null;
}

/**
 * Checks whether the given attribute type is<br>
 * is a single selection attribute type.<br>
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type (use the values<br>
 *                                              of the AttributeTypes object, e. g.<br>
 *                                              AttributeTypes.TEXT)<br>
 * @return {Boolean}                            <p>
 *                                              if the attribute can only be used once<br>
 */
AttributeTypeUtil.isSingleSelection = function (pAttributeType)
{
    var type = AttributeTypes.get(pAttributeType);
    return type ? type.singleSelection : null;
}

/**
 * Returns the title of the "dropDownDefinition"
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type (use the values<br>
 *                                              of the AttributeTypes object, e. g.<br>
 *                                              AttributeTypes.TEXT)<br>
 * @return {String}                           <p>
 *                                              .<br>
 */
AttributeTypeUtil.getDropDownDefinitionTitle = function (pAttributeType)
{
    var type = AttributeTypes.get(pAttributeType);
    return type ? type.dropDownDefinitionTitle : "";
}

/**
 * Returns a function to resolve the displayValue depending on the attribute type.
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type (use the values<br>
 *                                              of the AttributeTypes object, e. g.<br>
 *                                              AttributeTypes.TEXT)<br>
 * @return {Function}                           <p>
 *                                              A function that resolves the displayValue or null if the type is invalid<br>
 */
AttributeTypeUtil.getDisplayValueSqlFn = function (pAttributeType)
{
    var attributeType = AttributeTypes.get(pAttributeType);
    if (!attributeType)
        return null;
    return attributeType.getDisplayValueSql.bind(attributeType);
}

/**
 * Compare the given pAttributeType with the attribute type string
 * "OBJECTSELECTION".
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type which shall be comapred.
 * @return {Boolean}                            <p>
 *                                              Returns true, if the given attribute type is equal<br>
 *                                              with the attribute string "OBJECTSELECTION" and <br>
 *                                              false, if not.<br>                                             
 */
AttributeTypeUtil.useLookup = function (pAttributeType)
{
    var type = AttributeTypes.get(pAttributeType);
    return type ? type.useLookup : false;
}

/**
 * Compare the given attribute type with every other<br>
 * existing attribute type and returns every compared <br>
 * type which is a possible parent type.<br>
 * 
 * @param {String} pChildType               <p>
 *                                          The attribute type which shall be used to compare.
 * @return {Array}                          <p>
 *                                          Returns all possible parent attribute types.<br>                                         
 */
AttributeTypeUtil.getGroupTypes = function (pChildType)
{
    var groupTypes = [];
    for (let type in AttributeTypes)
    {
        if (AttributeTypeUtil.isGroupType(type) && (!pChildType || (!AttributeTypeUtil.getPossibleChildren(type) || AttributeTypeUtil.getPossibleChildren(type).indexOf(pChildType) !== -1)) )
            groupTypes.push(type);
    }
    return groupTypes;
}

/**
 * If the given attribute type is a <br>
 * valid type and it has a getViewValue<br>
 * function it will return the matching<br>
 * view value to the given pValue.<br>
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type.<br>
 * @param {String} pValue                       <p>
 *                                              The value.<br>
 * @param {String} pKeyword                     <p>
 *                                              The keyword.<br>
 * @return {String}                             <p>
 *                                              Returns the view value.                                                                                                                                       
 */
AttributeTypeUtil.getAttributeViewValue = function (pAttributeType, pValue, pKeyword)
{
    var type = AttributeTypes.get(pAttributeType);
    return type ? type.getViewValue(pValue, pKeyword) : pValue;
}

/**
 * Initializes the type columns.
 */
AttributeTypeUtil._initTypeColumnData = function ()
{
    var columns = [];
    var typeColumnMap = {};
    for (let type in AttributeTypes)
    {
        type = AttributeTypes[type];
        if (type.databaseField)
        {
            var typeKey = type.toString();
            var colIndex = columns.indexOf(type.databaseField);
            if (colIndex == -1)
            {
                colIndex = columns.length;
                columns.push(type.databaseField);
            }
            typeColumnMap[typeKey] = colIndex;
        }
    }
    this._allDBColumns = columns;
    this._typeColumnMap = typeColumnMap;
}

/**
 * Return the all database fields/columns of the every attribute type.
 * 
 * @return {String}             <p>
 *                              All database fields/columns.
 */
AttributeTypeUtil.getAllDatabaseFields = function ()
{
    if (this._allDBColumns == undefined)
        AttributeTypeUtil._initTypeColumnData();
    return this._allDBColumns;
}

/**
 * Returns the type column index.
 * 
 * @param {String} pAttributeType               <p>
 *                                              The attribute type of you want the column<br>
 *                                              type index back.<br>
 * @return {String}                                                                           
 */
AttributeTypeUtil.getTypeColumnIndex = function (pAttributeType)
{
    if (this._typeColumnMap == undefined)
        AttributeTypeUtil._initTypeColumnData();
    return this._typeColumnMap[pAttributeType.trim()];
}

/*********************************************************************************************************************/

/**
 * Functions for AttributeUsages.<br>
 * <b><i>Do not instanciate this!</i></b>
 */
function AttributeUsageUtil () {}

/**
 * Creates AttributeUsages for all subordinate attributes <br>
 * of an attribute.This is required when an usage is added <br>
 * to a superordinate attribute.<br>
 * 
 * @param {String} pAttributeId                 <p>
 *                                              The id of the superordinate attribute.<br>
 * @param {String} pObjectType                  <p>
 *                                              The context.<br>
 */
AttributeUsageUtil.insertChildrenUsages = function (pAttributeId, pObjectType)
{
    if (!pAttributeId)
        return;
    var table = "AB_ATTRIBUTEUSAGE";
    var columns = ["AB_ATTRIBUTEUSAGEID", "AB_ATTRIBUTE_ID", "OBJECT_TYPE", "MAX_COUNT"];
    
    var inserts = [];
    _addInserts(pAttributeId, pObjectType);
    db.inserts(inserts);
    
    function _addInserts (pAttributeId, pObjectType)
    {
        var attributes = newSelect(["AB_ATTRIBUTEID", "ATTRIBUTE_TYPE",
                                newSelect("count(*)")
                                    .from("AB_ATTRIBUTEUSAGE")
                                    .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                                    .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)])
                            .from("AB_ATTRIBUTE")
                            .where("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE(), SqlBuilder.NOT_EQUAL())
                            .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId)
                            .table();

        attributes.forEach(function (row)
        {
            if (row[2] == "0")
            {
                let maxCount = AttributeTypeUtil.isSingleSelection(row[1])
                    ? "1"
                    : "";
                let values = [util.getNewUUID(), row[0], pObjectType, maxCount];
                inserts.push([table, columns, null, values]);
            }
            _addInserts(row[0], pObjectType);
        });
    }
}

/**
 * Updates AttributeUsages for all subordinate attributes <br>
 * of an attribute. This is required when an usage of a <br>
 * superordinate attribute is changed.<br>
 * 
 * @param {String} pAttributeId                 <p>
 *                                              The id of the superordinate attribute.<br>
 * @param {String} pOldObjectType               <p>
 *                                              The old context.<br>
 * @param {String} pNewObjectType               <p>
 *                                              The new context.<br>
 */
AttributeUsageUtil.updateChildrenUsages = function (pAttributeId, pOldObjectType, pNewObjectType)
{
    if (!pNewObjectType || !pAttributeId || !pOldObjectType)
        return;
    
    var table = "AB_ATTRIBUTEUSAGE";
    
    var countSubQuery = newSelect("count(*)")
                            .from("AB_ATTRIBUTEUSAGE")
                            .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pNewObjectType)
                            .and("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID");
    
    var sqlSelect = newSelect(["AB_ATTRIBUTEID", "AB_ATTRIBUTEUSAGEID", countSubQuery])
                        .from("AB_ATTRIBUTE")
                        .leftJoin("AB_ATTRIBUTEUSAGE", newWhere()
                                .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pOldObjectType)
                                .and("AB_ATTRIBUTEID = AB_ATTRIBUTE_ID"));
    
    var updateCond = newWhere();
    
    //it is possible that the new objectType is already in a subordinate attribute 
    //and an update could cause a duplicate entry so one has to be deleted
    var deleteCond = newWhere();
    
    _addUpdateIds(pAttributeId, pOldObjectType);
        
    updateCond.updateData(true, table, ["OBJECT_TYPE"], null, [pNewObjectType]);
    deleteCond.deleteData(true, table);
    
    function _addUpdateIds (pAttributeId)
    {
        sqlSelect.clearWhere()
                 .where("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE(), SqlBuilder.NOT_EQUAL())
                 .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId);

        var attributes = sqlSelect.table();
        
        attributes.forEach(function (row)
        {
            if (row[1] && row[2] != "0")
                deleteCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1]);
            else if (row[1])
                updateCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1]);
            _addUpdateIds(row[0]);
        });
    }
}

/**
 * Deletes AttributeUsages for all subordinate attributes of an attribute.
 * This is required when an usage is removed from a superordinate attribute.
 * 
 * @param {String} pAttributeId                 <p>
 *                                              The id of the superordinate attribute.<br>
 * @param {String} pObjectType                  <p>
 *                                              The context.
 */
AttributeUsageUtil.deleteChildrenUsages = function (pAttributeId, pObjectType)
{
    var attributeSelect = newSelect("AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID")
                            .from("AB_ATTRIBUTE")
                            .leftJoin("AB_ATTRIBUTEUSAGE", newWhere("AB_ATTRIBUTEID = AB_ATTRIBUTE_ID")
                                                               .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType));
    
    var deleteCond = newWhere().from("AB_ATTRIBUTEUSAGE");
    _addDeleteIds(pAttributeId, pObjectType);
    
    deleteCond.deleteData();
    
    function _addDeleteIds (pAttributeId)
    {
        attributeSelect.clearWhere()
                       .where("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypes.COMBOVALUE(), SqlBuilder.NOT_EQUAL())
                       .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId);
        var attributes = attributeSelect.table();
        
        attributes.forEach(function (row)
        {
            if (row[1])
                deleteCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1])
            _addDeleteIds(row[0]);
        });
    }
}

/**
 * Deletes duplicate attribute usages.
 * 
 * @param {String} [pAttributeId=null]              <p>
 *                                                  Attribute id, if omitted, all duplicates will be deleted.<br>
 */
AttributeUsageUtil.removeDuplicates = function (pAttributeId)
{
    
    
    var attributeSelect = newSelect("AB_ATTRIBUTEUSAGEID, AB_ATTRIBUTE_ID, OBJECT_TYPE")
                                .from("AB_ATTRIBUTEUSAGE")
                                .where(null, newSelect("AB_ATTRIBUTEUSAGEID")
                                                .from("AB_ATTRIBUTEUSAGE", "USAGEDUP")
                                                .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = USAGEDUP.AB_ATTRIBUTE_ID")
                                                .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE = USAGEDUP.OBJECT_TYPE")
                                                .and("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID != USAGEDUP.AB_ATTRIBUTEUSAGEID"), 
                                        SqlBuilder.EXISTS());
                                        
    attributeSelect.andIfSet("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", pAttributeId);
    
    var duplicates = attributeSelect.table();
    var usageObj = {};
    var deleteCond = newWhere().from("AB_ATTRIBUTEUSAGE");
    
    duplicates.forEach(function (row)
    {
        if (!(row[1] in this))
            this[row[1]] = {};
        if (row[2] in this[row[1]])
            deleteCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[0]);
        this[row[1]][row[2]] = true;
    }, usageObj);
    
    deleteCond.deleteData();
}

/*************************************************************************************************/

/**
 * 
 * An AttributeRelationQuery can be used for getting the 
 * value and other properties of an AttributeRelation. <br>
 * <i><u>You have to instanciate it with "new".</u></i>
 * <p>
 * This is built like this because there are several different scenarios for
 * loading the values or other properties of one or more attribute relations. Because of this,
 * the constructor takes in the common parameters for loading attribute relations and you can
 * use methods of the constructed object to configure the query and get the desired result.
 * 
 * @param {String} pObjectRowId             <p>
 *                                          The object row id. (e.g.: contact id)<br>
 * @param {String} pAttributeId             <p>
 *                                          The attribute id.<br>
 * @param {String} pObjectType              <p>
 *                                          The object type. (e.g.: "Organisation")
 * @class
 */
function AttributeRelationQuery (pObjectRowId, pAttributeId, pObjectType)
{
    this._rowId = pObjectRowId || null;
    this._objectType = pObjectType || null;
    this._attributeIds = pAttributeId ? [pAttributeId] : null;
    this._attributeTypes = null;
    this._includeFullAttributeName = false;
    this._includeDisplayValue = false;
}

/**
 * Sets the object row id for the query.
 * 
 * @param {String} pObjectRowId             <p>
 *                                          The row id of the object. (e.g.: contact id)<br>
 * @return {Object}                         <p>
 *                                          Returns AttributeRelationQuery object<br>
 *                                          with the object row id set.<br>                                         
 */
AttributeRelationQuery.prototype.objectRowId = function (pObjectRowId)
{
    this._rowId = pObjectRowId;
    return this;
}

/**
 * Sets the object-type for the query.<br>
 * 
 * @param {String} pObjectType              <p>
 *                                          The object type. (e.g.: "Organisation")<br>
 * @return {Object}                         <p>
 *                                          Returns AttributeRelationQuery object<br>
 *                                          with the object type set.<br>                                         
 */ 
AttributeRelationQuery.prototype.objectType = function (pObjectType)
{
    this._objectType = pObjectType;
    return this;
}

/**
 * Sets only one attribute id for the query.<br>
 * 
 * @param {Array} pAttributeId              <p>
 *                                          The attribute id.<br>
 * @return {Object}                         <p>
 *                                          Returns AttributeRelationQuery object<br>
 *                                          with the attribute id set.<br>                                         
 */
AttributeRelationQuery.prototype.attributeId = function (pAttributeId)
{
    this._attributeIds = [pAttributeId];
    return this;
}

/**
 * Sets the attribute ids for the query.
 * 
 * @param {Array} pAttributeIds                 <p>
 *                                              The attribute ids in a array.<br>
 * @return {Object}                             <p>
 *                                              Returns AttributeRelationQuery object<br>                                             
 *                                              with the attribute ids set.<br>
 */
AttributeRelationQuery.prototype.attributeIds = function (pAttributeIds)
{
    this._attributeIds = pAttributeIds;
    return this;
}

/**
 * Sets the attribute type for the query.
 * 
 * @param {Array} pAttributeTypes               <p>
 *                                              The attribute types.<br>
 * @return {Object}                             <p>
 *                                              Returns the AttributeRelationQuery object<br>
 *                                              with the attributeTypes set.<br>                                               
 */
AttributeRelationQuery.prototype.attributeTypes = function (pAttributeTypes)
{
    this._attributeTypes = pAttributeTypes;
    return this;
}

/**
 * If this method was called, the query result will contain the fullAttributeName.
 * 
 * @return {Object}             <p>
 *                              Return the AttributeRelationQuery object<br>
 *                              with the option includeFullAttributeName enabled.<br> 
 */
AttributeRelationQuery.prototype.includeFullAttributeName = function ()
{
    this._includeFullAttributeName = true;
    return this;
}

/**
 * If this method was called, the query result will contain the displayValue.
 * 
 * @return {Object}             <p>
 *                              Return the AttributeRelationQuery object<br>
 *                              with the option includeDisplayValue enabled.<br>
 */
AttributeRelationQuery.prototype.includeDisplayValue = function ()
{
    this._includeDisplayValue = true;
    return this;
}

/**
 * Executes the query and returns the result, depending on the properties of the AttributeRelationQuery object.
 * 
 * @return {AttributeRelation[]}    <p>
 *                                  Array of objects. By default, the objects contain the properties:
 *                                  <ul>
 *                                  <li>attributeId</li>
 *                                  <li>value</li>
 *                                  <li>attributeRelationId</li>
 *                                  <li>attributeName</li>
 *                                  <li>attributeType</li>
 *                                  </ul>
 *                                  If includeDisplayValue is true, the object also contains<br>
 *                                  the property 'displayValue' and if includeFullAttributeName <br>
 *                                  is true, there is also the property 'fullAttributeName'.<br>
 */
AttributeRelationQuery.prototype.getAttributes = function ()
{
    var defaultFields = [
        "AB_ATTRIBUTE.ATTRIBUTE_TYPE", 
        "AB_ATTRIBUTE.DROPDOWNDEFINITION", 
        "AB_ATTRIBUTE.ATTRIBUTE_NAME",
        "COMBOVAL.ATTRIBUTE_NAME",
        "AB_ATTRIBUTE.AB_ATTRIBUTEID",
        "AB_ATTRIBUTERELATION.AB_ATTRIBUTERELATIONID",
        "AB_ATTRIBUTERELATION.OBJECT_ROWID",
        "AB_ATTRIBUTERELATION.OBJECT_TYPE"
    ];

    
    var valueFields = AttributeTypeUtil.getAllDatabaseFields();

    var attributeValues = AttributeRelationUtils.getAttributeSqlBuilder(defaultFields.concat(valueFields), this._rowId, this._objectType)
        .andIfSet("AB_ATTRIBUTERELATION.AB_ATTRIBUTE_ID", this._attributeIds, SqlBuilder.IN())
        .andIfSet("AB_ATTRIBUTE.ATTRIBUTE_TYPE", this._attributeTypes, SqlBuilder.IN())
        .table();
        
    if (attributeValues.length == 0)
        return [];
    
    var mappingFn = function (row)
    {
        var attrObj = new AttributeRelation(row[5], row[4], row[AttributeTypeUtil.getTypeColumnIndex(row[0]) + defaultFields.length], 
            row[2], row[0], row[6], row[7]);
        
        if (this._includeDisplayValue)
        {
            if (row[0].trim() == AttributeTypes.COMBO())
                attrObj.displayValue = translate.text(row[3]);
            else
                attrObj.displayValue = AttributeTypeUtil.getAttributeViewValue(row[0].trim(), attrObj.value, row[1]);
        }
        if (this._includeFullAttributeName)
        {
            attrObj.fullAttributeName = AttributeUtil.getFullAttributeName(row[4]);
        }
        
        return attrObj;
    }
    
    return attributeValues.map(mappingFn, this);
}

/**
 * If this method is executed on your AttributeRelationQuery<br>
 * object it will return only one attribute.
 * 
 * @return {AttributeRelation}  <p>
 *                              Returns the AttributeRelationQuery object<br>
 *                              with only a sinlge attribute.<br>
 */
AttributeRelationQuery.prototype.getSingleAttribute = function ()
{
    if (!this._attributeIds || this._attributeIds.length !== 1)
        throw new Error("You have to specify a single attribute id");
    return this.getAttributes()[0] || null;
}

/**
 * Executes the query and returns a single value.<br>
 * For this, there must be a attribute id set.
 * 
 * @return {String}             <p>
 *                              The single value.
 */
AttributeRelationQuery.prototype.getSingleAttributeValue = function ()
{
    var attribute = this.getSingleAttribute();
    return attribute ? attribute.value : null;
}

/**
 * Executes the query and returns the count of datasets.
 * 
 * @return {Number}                 <p>
 *                                  The number of attribute relations.
 */
AttributeRelationQuery.prototype.getAttributeCount = function ()
{
    return parseInt(AttributeRelationUtils.getAttributeSqlBuilder("count(*)", this._rowId, this._objectType)
        .and(newWhereIfSet( "AB_ATTRIBUTERELATION.AB_ATTRIBUTE_ID", this._attributeIds, SqlBuilder.IN())
            .orIfSet("AB_ATTRIBUTERELATION.ID_VALUE", this._attributeIds, SqlBuilder.IN()))
        .cell() || 0);
}

AttributeRelationQuery.prototype.getMaxCount = function ()
{
    if (!this._objectType || !this._rowId)
        throw new Error("AttributeRelationQuery: Object type and row id are required");
    if (!this._attributeIds || this._attributeIds.length !== 1)
        throw new Error("AttributeRelationQuery: You have to specify a single attribute id");
    
    var attributeId = this._attributeIds[0];
    
    var maxCount = newSelect("MAX_COUNT")
        .from("AB_ATTRIBUTEUSAGE")
        .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId)
        .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", this._objectType)
        .cell();
    if (maxCount)
        return Number(maxCount) || null;
    return null;
}

AttributeRelationQuery.prototype.getMinCount = function ()
{
    if (!this._objectType || !this._rowId)
        throw new Error("AttributeRelationQuery: Object type and row id are required");
    if (!this._attributeIds || this._attributeIds.length !== 1)
        throw new Error("AttributeRelationQuery: You have to specify a single attribute id");
    
    var attributeId = this._attributeIds[0];
    
    return Number(newSelect("MIN_COUNT")
        .from("AB_ATTRIBUTEUSAGE")
        .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId)
        .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", this._objectType)
        .cell());
}

/**
 * Inserts a new attribute relation.
 * 
 * @param {String} pValue                       <p>
 *                                              Value of the attribute relation.
 * @param {Boolean} [pOmitValidation=false]     <p>
 *                                              If set to true, the current usage of the attribute and <br>
 *                                              max count won't be checked.<br>
 * @return {Boolean}                            <p>
 *                                              True, if the attribute relation was inserted, false <br>
 *                                              if the count validation failed.<br>
 */
AttributeRelationQuery.prototype.insertAttribute = function (pValue, pOmitValidation)
{
    if (!this._objectType || !this._rowId)
        throw new Error("AttributeRelationQuery: Object type and row id are required for insert");
    if (!this._attributeIds || this._attributeIds.length !== 1)
        throw new Error("AttributeRelationQuery: You have to specify a single attribute id for insert");
    
    var attributeId = this._attributeIds[0];
    
    if (!pOmitValidation)
    {
        var maxCount = this.getMaxCount();
        if (maxCount)
        {
            let timesUsed = this.getAttributeCount();
            if (timesUsed >= maxCount)
                return false;
        }
    }

    var attrData = {
        "AB_ATTRIBUTE_ID" : attributeId,
        "OBJECT_ROWID" : this._rowId,
        "OBJECT_TYPE" : this._objectType,
        "DATE_NEW" : vars.get("$sys.date"),
        "USER_NEW" : vars.get("$sys.user")
    };
    var type = AttributeUtil.getAttributeType(attributeId);
    var valueField = AttributeTypeUtil.getDatabaseField(type);
    if (valueField)
        attrData[valueField] = pValue;
    
    new SqlBuilder().insertFields(attrData, "AB_ATTRIBUTERELATION", "AB_ATTRIBUTERELATIONID");
    return true;
}

/**
 * deletes all attribute relations with the given rowId and objectType
 * 
 * @return {Number} count of deleted rows
 */
AttributeRelationQuery.prototype.deleteAllAttributes = function ()
{
    if (!this._rowId)
        throw new Error("AttributeRelationQuery: Row id is required for delete");
    
    return newWhere("AB_ATTRIBUTERELATION.OBJECT_ROWID", this._rowId)
        .andIfSet("AB_ATTRIBUTERELATION.OBJECT_TYPE", this._objectType)
        .deleteData();
}

/**
 * Object representing one attribute relation in the database. Don't use this constructor in you own code!
 * Instances of this should only be created by functions in this library.
 * 
 * @param {String} pAttributeRelationId attribute relation id
 * @param {String} pAttributeId attribute id
 * @param {String} pValue value of the attribute
 * @param {String} pAttributeName name of the attribute
 * @param {String} pAttributeType type of the attribute
 * @param {String} pObjectRowId rowId of the linked object
 * @param {String} pObjectType context of the linked object
 */
function AttributeRelation (pAttributeRelationId, pAttributeId, pValue, pAttributeName, pAttributeType, pObjectRowId, pObjectType)
{
    if (!pAttributeRelationId)
        throw new Error("AttributeRelation: pAttributeRelationId must be provided");
        
    this.attributeRelationId = pAttributeRelationId;
    this.attributeId = pAttributeId;
    this.value = pValue;
    this.attributeName = pAttributeName;
    this.attributeType = pAttributeType;
    this.objectRowId = pObjectRowId;
    this.objectType = pObjectType;
    this.displayValue = undefined;
    this.fullAttributeName = undefined;
}

/**
 * updates the value of the attribute in the database
 * 
 * @param {String} pValue the new value of the attribute relation
 * @return {Boolean} currently the function always returns true (if some kind of validation is implemented in the future, 
 *      it will return false if the validation fails)
 */
AttributeRelation.prototype.updateAttribute = function (pValue)
{
    if (pValue == undefined || pValue == "")
        throw new Error("AttributeRelation: no value provided for update");
        
    var attrData = {
        "DATE_EDIT" : vars.get("$sys.date"),
        "USER_EDIT" : vars.get("$sys.user")
    };
    var valueField = AttributeTypeUtil.getDatabaseField(this.attributeType);
    if (valueField)
        attrData[valueField] = pValue;

    newWhere("AB_ATTRIBUTERELATION.AB_ATTRIBUTERELATIONID", this.attributeRelationId)
        .updateFields(attrData);
    return true;
}

/**
 * deletes the attribute relation from the database
 * 
 * @param {Boolean} [pOmitValidation=false] if set to true, the function won't check if the min count prohibits the deletion
 * @retun {Boolean} true if it was deleted and false if the min count doesn't allow the deletion
 */
AttributeRelation.prototype.deleteAttribute = function (pOmitValidation)
{
    if (!pOmitValidation)
    {
        var attributeQuery = new AttributeRelationQuery(this.objectRowId, this.attributeId, this.objectType);
        var minCount = attributeQuery.getMinCount();
        
        if (minCount)
        {
            let timesUsed = attributeQuery.getAttributeCount();
            if (timesUsed <= minCount)
                return false;
        }
    }
    
    newWhere("AB_ATTRIBUTERELATION.AB_ATTRIBUTERELATIONID", this.attributeRelationId)
        .deleteData();
    return true;
}