Skip to content
Snippets Groups Projects
Select Git revision
  • e4d72b8f7725900a023195e79556f5bed9d040b6
  • main default protected
  • dev protected
  • f-render-html-properties
  • f-vishesh0932-ext-cosmetics
  • f-table-references
  • f-update-legacy-adapter
  • f-refactor-refs
  • f-fix-caosadvancedtools-refs
  • f-linkahead-rename
  • f-citation-cff
  • f-map-resolve-reference
  • dev-bmpg
  • f-form-select
  • f-doc-extention
  • f-geo-position-records
  • f-data-analysis
  • f-area-folder-drop
  • f-fix-get-parents
  • f-fix-110
  • f-entity-state
  • v0.16.0
  • v0.15.2
  • v0.15.1
  • v0.15.0
  • v0.14.0
  • v0.13.3
  • v0.13.2
  • v0.13.1
  • v0.13.0
  • v0.12.0
  • v0.11.1
  • v0.11.0
  • v0.10.1
  • v0.10.0
  • v0.9.0
  • v0.8.0
  • v0.7.0
  • v0.6.0
  • v0.5.0
  • v0.4.2
41 results

caosdb.js

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    caosdb.js 39.80 KiB
    /*
     * ** header v3.0
     * This file is a part of the LinkAhead Project.
     *
     * Copyright (C) 2018-2020 Alexander Schlemmer
     * Copyright (C) 2018 Research Group Biomedical Physics,
     * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
     * Copyright (C) 2019-2022 IndiScale GmbH (info@indiscale.com)
     * Copyright (C) 2019-2022 Timm Fitschen (t.fitschen@indiscale.com)
     *
     * This program is free software: you can redistribute it and/or modify
     * it under the terms of the GNU Affero General Public License as
     * published by the Free Software Foundation, either version 3 of the
     * License, or (at your option) any later version.
     *
     * This program is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     * GNU Affero General Public License for more details.
     *
     * You should have received a copy of the GNU Affero General Public License
     * along with this program. If not, see <https://www.gnu.org/licenses/>.
     *
     * ** end header
     */
    'use strict';
    
    /**
     * JavaScript client for LinkAhead
     *
     * Dependency: jquery
     * Dependency: webcaosdb
     *
     * TODO:
     * - Check whether everything works when previews are opened.
     */
    
    /**
     * Return true iff the current session has an authenticated user. This is the
     * case, iff there is a `user` attribute in the server's response and
     * subsequently a DIV.caosdb-user-name somewhere in the dom tree.
     *
     * @return {boolean}
     */
    function isAuthenticated() {
        return document.getElementsByClassName("caosdb-user-name").length > 0 && document.getElementsByClassName("caosdb-user-name")[0].innerText.trim().length > 0;
    }
    
    
    /**
     * Return the name of the user currently logged in.
     *
     * @return Name of the user.
     */
    function getUserName() {
        const userElement = document.getElementsByClassName("caosdb-user-name")[0];
        if (userElement) {
            return userElement.innerText;
        } else {
            throw new Error("No user is logged in.");
        }
    }
    
    
    /**
     * Return the realm of the user currently logged in.
     *
     * @return Realm of the user.
     */
    function getUserRealm() {
        return document.getElementsByClassName("caosdb-user-realm")[0].innerText;
    }
    
    /**
     * Return the roles of the user currently logged in.
     *
     * @return Array containing the roles of the user.
     */
    function getUserRoles() {
        return Array.from(document.getElementsByClassName("caosdb-user-role")).map(el => el.innerText);
    }
    
    /**
     * Find out, whether the user that is currently logged in is an administrator.
     *
     * @return true if the role administration is present.
     */
    function userIsAdministrator() {
        return userHasRole("administration");
    }
    
    /**
     * Return true if the user has the role `anonymous`.
     */
    function userIsAnonymous() {
        return userHasRole("anonymous");
    }
    
    
    /**
     * Return true if the user has the role `role`.
     *
     * @param role
     */
    function userHasRole(role) {
        return getUserRoles().filter(el => el == role).length > 0;
    }
    
    
    /**
     * Return all entities in the current document.
     *
     * @return A list of document elements.
     */
    function getEntities() {
        return document.getElementsByClassName("caosdb-entity-panel");
    }
    
    /**
     * Return the role (Record, RecordType, ...) of element.
     * @return A String holding the role or undefined if the role could not be found.
     */
    function getEntityRole(element) {
        if ($(element).is("[data-entity-role]")) {
            return $(element).attr("data-entity-role");
        }
        if ($(element).is(".caosdb-f-entity-role")) {
            return $(element).text();
        }
    
    
        let res = $(element).find("[data-entity-role]");
        if (res.length == 1) {
            return res.attr("data-entity-role");
        }
    
        res = $(element).find(".caosdb-f-entity-role");
        if (res.length == 1) {
            return res.text();
        }
        return undefined;
    }
    
    /**
     * Return the unit of entity or undefined.
     *
     * @param {HTMLElement} entity
     * @return {String}
     */
    function getEntityUnit(entity) {
        var res = $(entity).find("input.caosdb-f-entity-unit");
        if (res.length == 1) {
            var x = res.val();
            return (x == '' ? undefined : x);
        }
    
        return getEntityHeadingAttribute(entity, "unit");
    }
    
    /**
     * Return the datatype of element.
     * If the corresponding data-attribute can not be found undefined is returned.
     * @return A string containing the datatype of the element.
     */
    function getEntityDatatype(element) {
        var is_list = ($(element).find("input.caosdb-f-entity-is-list:checked").length > 0);
        var res = $(element).find("select.caosdb-f-entity-datatype");
        if (res.length == 1) {
            var x = res.val();
            if (typeof x !== 'undefined' && x != '' && is_list) {
                return "LIST<" + x + ">";
            }
            return (x == '' ? undefined : x);
        }
    
        res = $(element).find(".caosdb-entity-panel-heading[data-entity-datatype]");
        if (res.length == 1) {
            var x = res.attr("data-entity-datatype");
            return (x == '' ? undefined : x);
        }
        return undefined;
    }
    
    
    /**
     * Return the data type of a entity-property element or undefined.
     *
     * @param {HTMLElement} element - the entity-property element.
     *
     * @throws {Error} if the data type could not uniquely be determined.
     *
     * @return {string} the data type
     */
    function getPropertyDatatype(element) {
        var dt_elem = findElementByConditions(element,
            x => x.classList.contains("caosdb-property-datatype"),
            x => x.classList.contains("caosdb-preview-container"));
    
        if (dt_elem.length == 1) {
            return $(dt_elem[0]).text();
        } else if (dt_elem.length > 1) {
            throw new Error("The datatype of this property could not uniquely be determined.");
        }
    
        // no datatype
        return undefined;
    }
    
    /**
     * Return the name of element.
     * If the corresponding label can not be found or the label is ambigious undefined is returned.
     * @return A string containing the name of the element.
     */
    function getEntityName(element) {
        // TODO deprecated class name
        if ($(element).find('[data-entity-name]').length == 1) {
            return $(element).find('[data-entity-name]')[0].dataset.entityName;
        } else if (typeof $(element).find('.caosdb-f-entity-name').val() !== 'undefined') {
            return $(element).find('.caosdb-f-entity-name').val();
        }
        var res = element.getElementsByClassName("caosdb-label-name");
        if (res.length == 1) {
            return res[0].textContent;
        }
        return undefined;
    }
    
    /**
     * Return the path of an entity.
     *
     * This attribute is always set for file entities.
     *
     * If the corresponding label can not be found or the label is ambigious undefined is returned.
     *
     * @param {HTMLElement} entity - entity in HTML representation.
     * @return A string containing the path of the entity.
     */
    function getEntityPath(element) {
        if ($(element).find('[data-entity-path]').length == 1) {
            return $(element).find('[data-entity-path]')[0].dataset.entityPath;
        }
        const path = $(element).find('.caosdb-f-entity-path').val();
        if (typeof path !== 'undefined') {
            return path;
        }
    
        return getEntityHeadingAttribute(element, "path");
    }
    
    /**
     * Return the checksum of an entity.
     *
     * This attribute is always set for file entities.
     *
     * If the corresponding label can not be found or the label is ambigious undefined is returned.
     *
     * @param {HTMLElement} entity - entity in HTML representation.
     * @return A string containing the checksum of the entity.
     */
    function getEntityChecksum(element) {
        const checksum = $(element).find('.caosdb-f-entity-checksum').val();
        if (typeof checksum !== 'undefined') {
            return checksum;
        }
    
        return getEntityHeadingAttribute(element, "checksum");
    }
    
    /**
     * Return the size of element. This attribute is always set for file entities.
     * If the corresponding label can not be found or the label is ambigious undefined is returned.
     * @return A string containing the size of the element.
     */
    function getEntitySize(element) {
        // TODO: check if this if block is needed
        //       it is analogous to getEntityDescription
        // if ($(element).find('[data-entity-size]').length == 1) {
        //     return $(element).find('[data-entity-size]')[0].dataset.entitySize;
        // }
    
        if (typeof $(element).find('.caosdb-f-entity-size').val() !== 'undefined') {
            // This is needed for the edit mode to work properly:
            return $(element).find('.caosdb-f-entity-size').val();
        }
    
        return getEntityHeadingAttribute(element, "size");
    }
    
    /**
     * Return the id of an entity.
     * @param element The element holding the entity.
     * @return The id of the entity as a String or undefined if no ID could be found.
     * @throws An error if the ID was ambigous.
     */
    function getEntityID(element) {
        if (typeof element.dataset.entityId !== 'undefined') {
            return element.dataset.entityId;
        }
        // var res = element.getElementsByClassName("caosdb-id");
        var res = findElementByConditions(element, x => x.classList.contains("caosdb-id"),
            x => x.classList.contains("caosdb-entity-panel-body"));
        if (res.length == 1)
            return res[0].textContent;
        else if (res.length == 0)
            return undefined;
        throw new Error("id is ambigous for this element!");
    }
    
    
    /**
     * Get the version string of an entity.
     *
     * @param {HTMLElement} an Entity in HTML representation
     *     (div.caosdb-entity-panel)
     * @return {string} the version
     */
    var getEntityVersion = function (entity) {
        return entity.getAttribute("data-version-id");
    }
    
    
    /**
     * Get the id and, if present, the version of an entity.
     *
     * @param {HTMLElement} an Entity in HTML representation
     *     (div.caosdb-entity-panel)
     * @return {string} <id>[@<version>]
     */
    var getEntityIdVersion = function (entity) {
        const id = getEntityID(entity);
        const version = getEntityVersion(entity);
        if (version) {
            return `${id}@${version}`;
        }
        return id;
    }
    
    /**
     * Take a date, a time and subseconds and format it into a LinkAhead compatible representation.
     * @param date A date
     * @param time A time
     * @param subsec fractions of a seconds part of the time stamp; Must be empty
     *               string if not used
     * @return A LinkAhead compatible representation.
     */
    function input2caosdbDate(date, time, subsec) {
        let res = date + "T" + time;
        if (subsec!=""){
            res = res +"." + subsec;
        }
        return  res
    }
    
    /**
     * Return true if the current user has the given permission for the given
     * entity.
     *
     * @param {HTMLElement} entity
     * @return {boolean}
     */
    var hasEntityPermission = function (entity, permission) {
        if (userHasRole("administration")) {
            // administration is a special role. It has * permissions.
            return true;
        }
        const permissions = getAllEntityPermissions(entity);
        return permissions.indexOf(permission.toUpperCase()) > -1;
    }
    
    /**
     * Get all permissions the current user has for this entity.
     * @param {HTMLElement} entity
     * @return {string[]} array of permissions.
     */
    var getAllEntityPermissions = function (entity) {
        const permissions = $(entity).find("[data-permission]").toArray().map(x => x.getAttribute("data-permission"));
        return permissions;
    }
    
    /**
     * Take a datetime from caosdb and return a date, a time and the subseconds
     * suitable for html inputs.
     *
     * If the text contains only a date it is returned.
     *
     * @param text The string from LinkAhead.
     * @return A list of strings for the input elements.
     */
    function caosdb2InputDate(text) {
        if (text.includes("T")) {
            var spl = text.trim().split("T");
            var subsec=""
            if (spl[1].includes(".")) {
                subsec = spl[1].split('.')[1]
            }
            return [spl[0], spl[1].substring(0, 8), subsec];
        }
        return [text];
    }
    
    /**
     * Return the id of an href attribute.
     * This is needed for retrieving an ID that is contained in a link to another entity.
     *
     * Some anker tags have a data-entity-id tag instead which is returned.
     *
     * @param el The element holding the link.
     * @return A String holding the ID or undefined if no href attribute could be found.
     */
    function getIDfromHREF(el) {
        if (el.hasAttribute("href")) {
            let attr = el["href"];
            let idstr = attr.substring(attr.lastIndexOf("/") + 1);
            // Currently the XSLT returns wrong links for missing IDs.
            if (idstr.length == 0)
                return undefined;
            return idstr;
        } else if (el.hasAttribute("data-entity-id")) {
            return el.getAttribute("data-entity-id");
        }
        return undefined;
    }
    
    /**
     * Return an entity heading attribute from an element.
     * @param element The element holding the entity.
     * @return The value of the entity heading attribute or undefined if it is not present.
     */
    function getEntityHeadingAttribute(element, attribute_name) {
        var res = element.getElementsByClassName("caosdb-entity-heading-attr");
    
        for (var i = 0; i < res.length; i++) {
            var subres = res[i].getElementsByClassName("caosdb-entity-heading-attr-name");
            if (subres.length != 1) {
                throw "Entity heading attribute does not have a name.";
            }
            if (subres[0].textContent == attribute_name + ":") {
                return res[i].childNodes[1].textContent;
            }
        }
        return undefined;
    }
    
    /**
     * Return the entity attribute description from an element.
     * @param element The element holding the entity.
     * @return The value of the description or undefined if it is not present.
     */
    function getEntityDescription(element) {
        if ($(element).find('[data-entity-description]').length == 1) {
            return $(element).find('[data-entity-description]')[0].dataset.entityDescription;
        } else if (typeof $(element).find('.caosdb-f-entity-description').val() !== 'undefined') {
            // This is needed for the edit mode to work properly:
            return $(element).find('.caosdb-f-entity-description').val();
        }
    
        return getEntityHeadingAttribute(element, "description");
    }
    
    /**
     * Return the parents of an entity.
     * @param element The element holding the entity.
     * @return A list of objects with name and id of the parents.
     */
    function getParents(element) {
        var res = element.getElementsByClassName("caosdb-parent-name");
        var list = [];
        for (var i = 0; i < res.length; i++) {
            list.push({
                id: getIDfromHREF(res[i]),
                name: res[i].textContent
            });
        }
        return list;
    }
    
    /**
     * Find all elements that fulfill a condition.
     * Don't traverse elements if except condition is matched.
     *
     * @param {HTMLElement} element - The start node.
     * @param {function} condition - A filter callback which returns `true` for
     *     each element which should be included.
     * @param {function} except - A filter callback which returns `true` for an
     *     element when the traversing should stop.
     *
     * @returns {HTMLElement[]} an array of HTMLElements
     */
    function findElementByConditions(element, condition, except) {
        let found = [];
        let echild = element.children;
    
        for (var i = 0; i < echild.length; i++) {
            if (condition(echild[i])) {
                found.push(echild[i]);
            }
    
            if (!except(echild[i])) {
                found.push.apply(found, findElementByConditions(echild[i], condition, except));
            }
        }
        return found;
    }
    
    function getEntityXML(ent_element) {
        var xml = createEntityXML(
            getEntityRole(ent_element),
            getEntityName(ent_element),
            getEntityID(ent_element),
            getProperties(ent_element),
            getParents(ent_element),
            true,
            getEntityDatatype(ent_element),
            getEntityDescription(ent_element),
            getEntityUnit(ent_element),
        );
        return xml;
    }
    
    function getPropertyName(element) {
        var name_element = element.getElementsByClassName("caosdb-property-name");
        if (name_element.length > 0) {
            return name_element[0].textContent;
        } else if ($(element).is("[data-property-name]")) {
            return $(element).attr("data-property-name");
        }
    
        var ret = $(element).find("[data-property-name]").attr("data-property-name");
        if (ret && ret.length > 0) {
            return ret;
        } else {
            return undefined;
        }
    }
    
    /**
     * A JSON representation of an entities property.
     *
     * TODO description, importance
     *
     * @type {EntityProperty}
     * @property {string} id
     * @property {string} name
     * @property {number} duplicateIndex - Number of properties in the entity with
     *     the same name.
     * @property {HTMLElement} html - An HTML representation of the property.
     * @property {string} datatype
     * @property {boolean} reference - has a reference datatype?
     * @property {string|string[]} value
     * @property {string} unit
     * @property {boolean} list - has a list datatype?
     * @property {string} listDatatype - the datatype of the list elements
     */
    
    /**
     * Return a json property object.
     *
     * @param {HTMLElement} propertyelement - A HTMLElement representing the property.
     *     property element.
     * @param {object} [names] - a map of names tracking the count of usage for
     *     each name (Default: undefined)
     *
     * @return {Property}
     **/
    function getPropertyFromElement(propertyelement, names = undefined) {
    
        let property = {};
        let valel = propertyelement.getElementsByClassName("caosdb-f-property-value")[0];
        let dtel = propertyelement.getElementsByClassName("caosdb-property-datatype")[0];
        let idel = propertyelement.getElementsByClassName("caosdb-property-id")[0];
        let unitel = valel.getElementsByClassName("caosdb-unit")[0];
        const descel = propertyelement.getElementsByClassName("caosdb-property-description")[0];
    
        property.html = propertyelement;
        // name
        property.name = getPropertyName(propertyelement);
        // description
        property.description = descel ? descel.textContent : undefined;
    
    
        // id
        if (idel === undefined) {
            property.id = undefined;
        } else {
            property.id = idel.textContent;
        }
    
        // unit
        if (typeof unitel == "undefined") {
            property.unit = undefined;
        } else {
            property.unit = unitel.textContent.trim();
        }
    
        if (dtel === undefined) {
            property.datatype = undefined;
            property.list = undefined;
            property.reference = undefined;
            property.value = "";
        } else {
            property.datatype = dtel.textContent;
    
            var base_datatypes = ["TEXT", "INTEGER", "DOUBLE", "DATETIME", "BOOLEAN"];
            if (property.datatype.substring(0, 4) == "LIST") {
                property.list = true;
                property.value = [];
                var list_datatype = property.datatype.substring(5, property.datatype.length - 1);
                property.reference = base_datatypes.filter(el => el == list_datatype).length == 0;
                property.listDatatype = list_datatype;
            } else {
                property.list = false;
                property.value = "";
                property.reference = base_datatypes.filter(el => el == property.datatype).length == 0;
            }
    
        }
    
        if (!property.list && valel.getElementsByClassName("caosdb-f-property-single-raw-value").length == 1) {
            valel = valel.getElementsByClassName("caosdb-f-property-single-raw-value")[0];
        }
    
        var value_string = undefined;
        if (valel && valel.textContent.length > 0) {
            value_string = valel.textContent;
        } else if (valel && valel.value && valel.value.length > 0) {
            // this is the case when the valel is an input coming from edit_mode.
            value_string = valel.value;
        }
    
        // Needed for multiple properties with the same name:
        // It is not set when names is undefined.
        if (!(names === undefined)) {
            if (names.has(property.name)) {
                names.set(property.name, names.get(property.name) + 1);
            } else {
                names.set(property.name, 0);
            }
            property.duplicateIndex = names.get(property.name);
        }
    
        if (typeof value_string !== "undefined") {
            // This is set to true, when there is a reference or a list of references:
            if (typeof property.reference === "undefined") {
                property.reference = (valel.getElementsByClassName("caosdb-id").length > 0);
            }
    
            if (property.list) {
                // list datatypes
                let listel;
                // list of anything but references
                listel = findElementByConditions(valel, x => x.classList.contains("caosdb-f-property-single-raw-value"),
                    x => x.classList.contains("caosdb-preview-container"));
                for (var j = 0; j < listel.length; j++) {
                    property.value.push(listel[j].textContent);
                }
            } else if (property.reference && valel.getElementsByTagName("a")[0]) {
                // reference datatypes
                property.value = getIDfromHREF(valel.getElementsByTagName("a")[0]);
            } else {
                // all other datatypes
                property.value = value_string;
            }
        }
    
        return property;
    }
    
    /**
     * Get all the properties from an entity.
     *
     * @param {HTMLElement} element The element holding the entity.
     *
     * @return {HTMLElement[]} a list of properties in HTML representation.
     */
    function getPropertyElements(element) {
        return findElementByConditions(element,
            x => x.classList.contains("caosdb-f-entity-property"),
            x => x.classList.contains("caosdb-preview-container"));
    }
    
    /**
     * Return JavaScript object representations of the properties of an entity.
     *
     * @param {HTMLElement} element - The element holding the entity.
     * @return {object[]} A list of objects for the properties according to the
     *     following specification.
     *
     *
     * Specification of properties:
     * prop = {
     *     name: ... // String, name of the property
     *     id: ... // String, id of the property
     *     value: ... // value of the property
     *     datatype: ... // full datatype, e.g. INTEGER or LIST<Uboot>
     *     duplicateIndex: ... // Integer starting from 0 and increasing for multiple properties
     *     reference: ... // Is this holding an ID of a reference? (boolean)
     *     list: ... // Is this a list? (boolean)
     *   }
     *
     *
     */
    function getProperties(element) {
        var res = getPropertyElements(element);
        var list = [];
    
        var names = new Map();
        for (var i = 0; i < res.length; i++) {
            let property = getPropertyFromElement(res[i], names);
            list.push(property);
        }
        return list;
    }
    
    
    /**
     * Construct XPath expression from selectors.
     *
     * Used by getPropertyValues.
     *
     * @param {String[][]} selectors
     * @return {String[]} XPath expressions.
     */
    var _constructXpaths = function (selectors) {
        const xpaths = [];
        for (let sel of selectors) {
            var expr = "Property";
            if (sel[0] == "id") {
                expr = "";
            }
            for (let i = 0; i < sel.length; i++) {
                const segment = sel[i];
                if (segment == "id") {
                    expr += `@id`;
                } else if (segment) {
                    expr += `[@name='${segment}']`;
                }
    
                if (i+1 < sel.length) {
                    expr += "//Property"
                }
            }
            xpaths.push(expr);
        }
        return xpaths;
    }
    
    /**
     * Return a table where each row represents an entity and each column a property.
     *
     * This also works for entities from select queries, where the properties
     * are deeply nested, e.g. when each entity references a "Geo Location"
     * record which have latitude and longitude properties:
     *
     * `getPropertyValues(entities, [["Geo Location", "latitude"], ["Geo Location", "longitude"]])`
     *
     * When the entitieshave normal non-list references to the "Geo Location" the
     * result looks like this:
     *
     * `[[ "50", "-39"], ...]`
     *
     * When the entities have a LIST of thre Geo Locations the result looks like
     * this:
     *
     * `[[[ "50", "51", "52"], [ "-39", "-38", "-37" ]], ...]`.
     *
     * Use empty strings for selector elements when the property name is irrelevant:
     *
     * `getPropertyValues(entities, [["", "latitude"], ["", "longitude"]])`
     *
     * Limitations:
     *
     * 1. Currently, this implementation assumes that properties (and subproperties
     *    for that matter) have unique names, entity-wide and do have a LIST
     *    datatype.
     *
     * 2. It only handles one of the many special cases, which is "id". Other
     *    special cases ("name", "description", "unit", etc.) are to be added when
     *    needed.
     *
     * @param {XMLElement[]} entities
     * @param {String[][]} selectors
     * @return {String[][]} A table of the property values for each entity (index
     *     order is `[row][column]`). Each row is an entity, each column is a value
     *     (or an array of values, when the entity has list properties).
     */
    var getPropertyValues = function (entities, selectors) {
        // @review Florian Spreckelsen 2022-05-06
        const entity_iter = entities.evaluate("/Response/Record", entities);
    
        const table = [];
        const xpaths = _constructXpaths(selectors)
    
        var current_entity = entity_iter.iterateNext();
        while (current_entity) {
            const row = [];
            for (let expr of xpaths) {
                const property_iter = entities.evaluate(expr, current_entity);
                var property = property_iter.iterateNext();
                if (typeof property !== "undefined" && property !== null) {
                    // handle lists and single values
                    var values = [];
                    while (property !== null) {
                      values.push(property.textContent.trim());
                      property = property_iter.iterateNext();
                    }
                    if(values.length < 2) {
                      // wasn't a list
                      values = values[0];
                    }
                    row.push(values);
                } else {
                    row.push(undefined)
                }
    
            }
            table.push(row);
            current_entity = entity_iter.iterateNext();
        }
    
        return table;
    }
    
    /**
     * Sets a property with some basic type checking.
     *
     * @param valueelement The dom element where the text content is to be set.
     * @param property The new property.
     * @param propold The old property belonging to valueelement.
     */
    function setPropertySafe(valueelement, property, propold) {
        const serverstring = connection.getBasePath() + "Entity/";
        if (propold.list) {
            if (property.value.length === undefined) {
                throw ("New property must be a list.");
            }
    
            // Currently problems, when list is set to [] and afterwards to something different.
    
            if (propold.reference) {
                var finalstring;
                if (property.value.length == 0) {
                    finalstring = "";
                } else {
                    finalstring = '';
                    for (var i = 0; i < property.value.length; i++) {
                        finalstring += '<a class="btn btn-secondary btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value[i] + '"><span class="caosdb-id">' + property.value[i] + '</span><span class="caosdb-resolve-reference-target" /></a>';
                    }
                }
                valueelement.getElementsByClassName("caosdb-value-list")[0].getElementsByClassName("caosdb-overflow-content")[0].innerHTML = finalstring;
            } else {
                throw ("Not Implemented: Lists with no references.");
            }
        } else if (propold.reference) {
            var llist = valueelement.getElementsByTagName("a");
            if (llist.length > 0) {
                var ael = llist[0];
                ael.setAttribute("href", serverstring + property.value);
                ael.innerHTML = '<span class="caosdb-id">' + property.value + '</span><span class="caosdb-resolve-reference-target" />';
            } else {
                finalstring = '<a class="btn btn-secondary btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value + '"><span class="caosdb-id">' + property.value + '</span><span class="caosdb-resolve-reference-target" /></a>';
                valueelement.innerHTML = finalstring;
                preview.init();
            }
        } else {
            valueelement.innerHTML = "<span class='caosdb-f-property-single-raw-value caosdb-f-property-text-value caosdb-v-property-text-value'>" + property.value + "</span>";
        }
    }
    
    /**
     * Set a property in the dom model.
     * @author Alexander Schlemmer
     * @param element The document element of the record.
     * @param property The new property as an object.
     * @return The number of properties that were set.
     *
     * If multiple properties with the same name exist and the property
     * to be set has not duplicateIndex, all properties will be set to that value.
     */
    function setProperty(element, property) {
        var elementp = element.getElementsByClassName("caosdb-properties")[0];
        var res = elementp.getElementsByClassName("list-group-item");
        var dindex = property.duplicateIndex;
        var counter = 0;
        for (var i = 0; i < res.length; i++) {
            if (res[i].classList.contains("caosdb-properties-heading")) continue;
            let propold = getPropertyFromElement(res[i]);
            if (propold.name === property.name) {
                if (dindex > 0) {
                    dindex--;
                    continue;
                }
                setPropertySafe(res[i].getElementsByClassName("caosdb-f-property-value")[0],
                    property,
                    propold);
                counter++;
            }
        }
        return counter;
    }
    
    /**
     * Get a property value by name from an Entity in HTML representation.
     *
     * @param {HTMLElement} element - The element holding the entity.
     * @param {string} property_name - The name of the property.
     * @param {boolean} [case_sensitive=True] - If true search for property names
     *     case-sensitively. Otherwise neglect the case ("A" and "a" are
     *     equivalent).
     * @returns {string} The value of the the property with property_name or `undefined` when this property is not available for this entity.
     */
    function getProperty(element, property_name, case_sensitive = true) {
        var props;
        if (case_sensitive) {
            props = getProperties(element).filter(el => el.name == property_name);
        } else {
            props = getProperties(element).filter(el => el.name.toLowerCase() == property_name.toLowerCase());
        }
        if (props.length == 0) {
            return undefined;
        }
        return props[0].value;
    }
    
    /**
     * Helper function for setting an id and or a name if contained in parent.
     * @param parentElement The element which is to recieve the attributes.
     * @param parent The object possibly containing an id and or a name.
     */
    function _setDescriptionNameID(parentElement, parent) {
        if (typeof parent.id !== 'undefined' && parent.id !== '') {
            parentElement.setAttribute("id", parent.id);
        }
        if (typeof parent.name !== 'undefined' && parent.name !== '') {
            parentElement.setAttribute("name", parent.name);
        }
        if (typeof parent.description !== 'undefined' && parent.description !== '') {
            parentElement.setAttribute("description", parent.description);
        }
    }
    
    /**
     * Append a parent node to an XML document.
     * @see getParents
     * @param doc A document for the XML.
     * @param element The element to append to.
     * @param parent An object containing a name and or an id.
     */
    function appendParent(doc, element, parent) {
        var parentElement = document.createElementNS(undefined, "Parent");
        _setDescriptionNameID(parentElement, parent);
        element.appendChild(parentElement);
    }
    
    /**
     * Append a text node with name name and value value to element element.
     * @param doc A document for the XML.
     * @param element
     * @param name
     * @param value
     */
    function appendValueNode(doc, element, name, value) {
        let el = document.createElementNS(undefined, name);
        let valel = document.createTextNode(value);
        el.appendChild(valel);
        element.appendChild(el);
    }
    
    /**
     * Append a property node to an XML document.
     * @see getProperties
     * @param doc An document for the XML.
     * @param element The element to append to.
     * @param property An object specifying a property.
     */
    function appendProperty(doc, element, property, append_datatype = false) {
        var propertyElement = document.createElementNS(undefined, "Property");
        _setDescriptionNameID(propertyElement, property);
        if (append_datatype && typeof property.datatype !== "undefined") {
            propertyElement.setAttribute("datatype", property.datatype);
        }
        if (typeof property.unit !== 'undefined') {
            propertyElement.setAttribute("unit", property.unit);
        }
    
        if (!(property.value === undefined)) {
            if (("list" in property && property.list) || property.value instanceof Array) {
                if (property.value instanceof Array) {
                    for (var i = 0; i < property.value.length; i++) {
                        appendValueNode(doc, propertyElement, "Value", property.value[i]);
                    }
                } else {
                    appendValueNode(doc, propertyElement, "Value", property.value);
                }
            } else {
                let valel = document.createTextNode(property.value);
                propertyElement.appendChild(valel);
            }
        }
    
        element.appendChild(propertyElement);
    }
    
    
    /**
     * Return a new Document.
     *
     * Helper function.
     *
     * @param {string} root - the new root element.
     * @returns {Document} the new document.
     */
    function _createDocument(root) {
        return document.implementation.createDocument(null, root, null);
    }
    
    
    /**
     * Create an XML for an entity.
     * This function uses the object notation.
     * @see getProperties
     * @see getParents
     * @param role Record, RecordType, Property or File (in case of files the three file arguments must be used!)
     * @param name The name of the entity. Can be undefined.
     * @param id The id of the entity. Can be undefined.
     * @param properties A list of properties.
     * @param parents A list of parents.
     * @param description A description for this entity.
     * @return {Document|DocumentFragment} - An xml document holding the newly
     *         created entity.
     *
     */
    function createEntityXML(role, name, id, properties, parents,
        append_datatypes = false, datatype = undefined, description = undefined,
        unit = undefined,
        file_path = undefined, file_checksum = undefined, file_size = undefined) {
    
        var doc = _createDocument(role);
        var nelnode = doc.children[0];
        _setDescriptionNameID(nelnode, {
            name: name,
            id: id,
            description: description,
        });
    
        if (typeof datatype !== 'undefined' && datatype.length > 0) {
            $(nelnode).attr("datatype", datatype);
        }
    
        if (typeof unit !== 'undefined' && unit.length > 0) {
            $(nelnode).attr("unit", unit);
        }
    
        if (!(parents === undefined)) {
            for (var i = 0; i < parents.length; i++) {
                appendParent(doc, nelnode, parents[i]);
            }
        }
    
        if (!(properties === undefined)) {
            for (var i = 0; i < properties.length; i++) {
                appendProperty(doc, nelnode, properties[i], append_datatypes);
            }
        }
    
        if (role.toLowerCase() == "file") {
            /*
              File path, checksum and size are needed for File entities.
    
              An error is raised when these arguments are not set.
            */
            if (file_path === undefined || file_checksum === undefined || file_size === undefined) {
                throw "Path, checksum and size must not be undefined in case of file entities.";
            }
    
            $(nelnode).attr("path", file_path);
            $(nelnode).attr("checksum", file_checksum);
            $(nelnode).attr("size", file_size);
        }
        return doc;
    }
    
    /**
     * Create an XML for a file entity.
     * This is a convenience function for creating XML from file entities.
     * This function uses the object notation.
     * @see getProperties
     * @see getParents
     * @param name The name of the entity. Can be undefined.
     * @param id The id of the entity. Can be undefined.
     * @param parents A list of parents.
     * @param file_path The path of the file in the LinkAhead file system.
     * @param file_checksum The checksum of the file.
     * @param file_size The size of the file in bytes.
     * @param description A description for this entity.
     * @return {Document|DocumentFragment} - An xml document holding the newly
     *         created entity.
     *
     */
    function createFileXML(name, id, parents,
        file_path, file_checksum, file_size,
        description = undefined) {
        return createEntityXML("File", name, id, {}, parents,
            false, undefined, description, undefined,
            file_path, file_checksum, file_size);
    }
    
    /**
     * Helper function to wrap xml documents into another node which could e.g. be
     * Update, Response, Delete.
     *
     * @param {string} root - The name of the newly created document root node.
     * @param {Document[]|XMLDocumentFragment[]} xmls The xml documents.
     * @return {Document} A new xml document.
     */
    function wrapXML(root, xmls) {
        xmls = caosdb_utils.assert_array(xmls, "param `xmls`", true);
        caosdb_utils.assert_string(root, "param `root`");
    
        var doc = _createDocument(root);
        for (var i = 0; i < xmls.length; i++) {
            doc.firstElementChild.appendChild(xmls[i].firstElementChild);
        }
    
        return doc;
    }
    
    /**
     * Convert this xml document into an update.
     * @param {(Document[]|DocumentFragment[])} xmls - Array of xml documents.
     * @return {(Document|DocumentFragment)} A new xml document.
     */
    function createUpdate(xmls) {
        return wrapXML("Request", xmls);
    }
    
    /**
     * Concat and convert these xml documents into an insert request body for LinkAhead server.
     *
     * @param {(Document[]|DocumentFragment[])} xmls - Array of xml documents.
     * @return {(Document|DocumentFragment)} A new xml document.
     */
    function createInsert(xmls) {
        return wrapXML("Request", xmls);
    }
    
    /**
     * Convert this xml document into a response.
     *
     * @param {(Document[]|DocumentFragment[])} xmls - Array of xml documents.
     * @return {(Document|DocumentFragment)} A new xml document.
     */
    function createResponse(xmls) {
        return wrapXML("Response", xmls);
    }
    
    
    /**
     * Retrieve an entity by using an id or a name.
     *
     * TODO merge with connection.retrieveEntityById
     *
     * @param id The id of the entity. Can be undefined when name is used.
     * @param name The name of the entity. Can be undefined when id is used.
     * @return The element holding that entity.
     */
    async function retrieve(id = undefined, name = undefined) {
        var retstr = id;
        if (id === undefined) {
            retstr = name;
        }
        let entities = await connection.get("Entity/" + retstr);
        return transformation.transformEntities(entities);
    }
    
    /**
     * Query the database for querytext.
     * @param querytext The search query.
     * @return An array of entities.
     */
    async function query(querytext) {
        let entities = await connection.get("Entity/?query=" + querytext);
        return transformation.transformEntities(entities);
    }
    
    /**
     * Retrive one entity given by ID id
     * and transform them into single entity objects.
     * @param id The id.
     * @return An array of entities.
     */
    async function retrieve_dragged_property(id) {
        let entities = await connection.get("Entity/" + id);
        return transformation.transformProperty(entities);
    }
    
    /**
     * Update an entity using its xml representation.
     * @param xml The xml of the entity which will be automatically wrapped with an Update.
     */
    async function update(xml) {
        var wrapped = createUpdate(xml);
        return await transaction.updateEntitiesXml(wrapped);
    }
    
    
    /**
     * Restore an old version of an entity using an xml representation.
     * First, the old version is retrieved and the current version is set to the
     * old one.
     * @param versionid The version id (e.g. 123@abbabbaeff23322) of the version of
     * the entity which shall be restored.
     */
    async function restore_old_version(versionid){
        // retrieve entity
        var ent = await transaction.retrieveEntityById(versionid);
        if (ent === undefined){
            throw new Error(`Entity with version id ${versionid} could not be retrieved.`);
        }
        // remove unwanted tags (Version and Permissions)
        ent.getElementsByTagName("Version")[0].remove();
        var permissions = ent.getElementsByTagName("Permissions");
        for (let i = permissions.length-1; i >=0 ; i--) {
            permissions[i].remove();
        }
    
        // use XML to update entity/restore old version
        const doc = _createDocument("Request");
        doc.firstElementChild.appendChild(ent);
        reps = await transaction.updateEntitiesXml(doc);
        if (reps.getElementsByTagName("Error").length>0) {
            throw new Error(`Could not restore the Entity to the version ${versionid}.`);
        }
    }
    
    /**
     * Insert an entity in xml representation.
     *
     * @param {(Document[]|DocumentFragment[])} xml - xml of the entities which
     *        will be automatically wrapped with an Insert.
     * @returns {Document} server response.
     */
    async function insert(xml) {
        var wrapped = createInsert(xml);
        return await transaction.insertEntitiesXml(wrapped);
    }