org.structr.graphql.GraphQLTest.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.graphql.GraphQLTest.java

Source

/**
 * Copyright (C) 2010-2018 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.graphql;

import com.jayway.restassured.RestAssured;
import com.jayway.restassured.filter.log.ResponseLoggingFilter;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.config.Settings;
import org.structr.common.error.FrameworkException;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.Group;
import org.structr.core.entity.MailTemplate;
import org.structr.core.entity.Principal;
import org.structr.core.entity.Relation;
import org.structr.core.entity.SchemaNode;
import org.structr.core.entity.SchemaProperty;
import org.structr.core.entity.SchemaRelationshipNode;
import org.structr.core.graph.NodeAttribute;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.Tx;
import org.structr.core.property.EnumProperty;
import org.structr.core.property.PropertyKey;
import org.structr.core.property.PropertyMap;
import org.structr.rest.common.StructrGraphQLTest;
import org.structr.schema.export.StructrSchema;
import org.structr.schema.json.JsonEnumProperty;
import org.structr.schema.json.JsonFunctionProperty;
import org.structr.schema.json.JsonObjectType;
import org.structr.schema.json.JsonSchema;

/**
 *
 *
 */
public class GraphQLTest extends StructrGraphQLTest {

    private static final Logger logger = LoggerFactory.getLogger(GraphQLTest.class.getName());

    @Test
    public void testBasics() {

        RestAssured.basePath = "/structr/graphql";

        Group group = null;
        Principal tester = null;

        try (final Tx tx = app.tx()) {

            final PropertyKey<List> membersKey = StructrApp.key(Group.class, "members");

            tester = app.create(Principal.class, new NodeAttribute<>(Principal.name, "tester"));
            group = app.create(Group.class, new NodeAttribute<>(Group.name, "TestGroup"),
                    new NodeAttribute<>(membersKey, Arrays.asList(tester)));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        final String query1 = "{ Group { id, type, name, members { id, type, name } }, Principal(_pageSize: 1) { id, type name }}";
        final String query2 = "{ Group { id, type, name, members { } }}";
        final String query3 = "{ Group(id: \"" + group.getUuid() + "\") { id, type, name }}";

        RestAssured

                .given().contentType("application/json; charset=UTF-8")
                .filter(ResponseLoggingFilter.logResponseTo(System.out)).body(query1)

                .expect().statusCode(200).body("Group", hasSize(1)).body("Principal", hasSize(1))
                .body("Group[0].id", equalTo(group.getUuid())).body("Group[0].type", equalTo("Group"))
                .body("Group[0].name", equalTo("TestGroup"))
                .body("Group[0].members[0].id", equalTo(tester.getUuid()))
                .body("Group[0].members[0].type", equalTo("Principal"))
                .body("Group[0].members[0].name", equalTo("tester"))
                .body("Principal[0].id", equalTo(group.getUuid())).body("Principal[0].type", equalTo("Group"))
                .body("Principal[0].name", equalTo("TestGroup"))

                .when().post("/");

        RestAssured

                .given().contentType("application/json; charset=UTF-8")
                .filter(ResponseLoggingFilter.logResponseTo(System.out)).body(query2)

                .expect().statusCode(422).body("message", equalTo("Parse error at } in line 1, column 36"))
                .body("code", equalTo(422)).body("query", equalTo(query2))

                .when().post("/");

        RestAssured

                .given().contentType("application/json; charset=UTF-8")
                .filter(ResponseLoggingFilter.logResponseTo(System.out)).body(query3)

                .expect().statusCode(200).body("Group", hasSize(1)).body("Group[0].id", equalTo(group.getUuid()))
                .body("Group[0].type", equalTo("Group")).body("Group[0].name", equalTo("TestGroup"))

                .when().post("/");
    }

    @Test
    public void testAdvancedQueries() {

        final List<MailTemplate> templates = new LinkedList<>();
        final List<Principal> team = new LinkedList<>();
        Group group = null;

        try (final Tx tx = app.tx()) {

            final PropertyKey<List> membersKey = StructrApp.key(Group.class, "members");

            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Axel")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Christian")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Christian")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Ins")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Kai")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Lukas")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Michael")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Susanne")));
            team.add(app.create(Principal.class, new NodeAttribute<>(Principal.name, "Tobias")));

            group = app.create(Group.class, new NodeAttribute<>(Group.name, "Structr Team"),
                    new NodeAttribute<>(membersKey, team));

            app.create(Group.class, new NodeAttribute<>(Group.name, "All teams"),
                    new NodeAttribute<>(membersKey, Arrays.asList(group)));

            templates.add(app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "MailTemplate4"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "zrtsga"),
                    new NodeAttribute<>(AbstractNode.owner, team.get(2))));

            templates.add(app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "MailTemplate2"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "lertdf"),
                    new NodeAttribute<>(AbstractNode.owner, team.get(0))));

            templates.add(app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "MailTemplate5"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "tzegsg"),
                    new NodeAttribute<>(AbstractNode.owner, team.get(3))));

            templates.add(app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "MailTemplate3"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "asgw"),
                    new NodeAttribute<>(AbstractNode.owner, team.get(1))));

            templates.add(app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "MailTemplate6"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "dfjgr"),
                    new NodeAttribute<>(AbstractNode.owner, team.get(4))));

            templates.add(app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "MailTemplate1"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "abcdef"),
                    new NodeAttribute<>(AbstractNode.owner, team.get(0))));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Principal(id: \"" + team.get(0).getUuid() + "\") { id, type, name } }");
            assertMapPathValueIs(result, "Principal.#", 1);
            assertMapPathValueIs(result, "Principal.0.id", team.get(0).getUuid());
            assertMapPathValueIs(result, "Principal.0.type", "Principal");
            assertMapPathValueIs(result, "Principal.0.name", "Axel");
        }

        {
            final Map<String, Object> result = fetchGraphQL("{ Principal(name: \"Axel\") { name } }");
            assertMapPathValueIs(result, "Principal.#", 1);
            assertMapPathValueIs(result, "Principal.0.name", "Axel");
            assertMapPathValueIs(result, "Principal.0.type", null);
            assertMapPathValueIs(result, "Principal.0.id", null);
        }

        {
            final Map<String, Object> result = fetchGraphQL("{ Principal { name(_equals: \"Axel\") } }");
            assertMapPathValueIs(result, "Principal.#", 1);
            assertMapPathValueIs(result, "Principal.0.name", "Axel");
            assertMapPathValueIs(result, "Principal.0.type", null);
            assertMapPathValueIs(result, "Principal.0.id", null);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Principal { name(_contains: \"a\", _contains: \"l\", _conj: \"and\") } }");
            assertMapPathValueIs(result, "Principal.#", 4);
            assertMapPathValueIs(result, "Principal.0.name", "All teams");
            assertMapPathValueIs(result, "Principal.1.name", "Axel");
            assertMapPathValueIs(result, "Principal.2.name", "Lukas");
            assertMapPathValueIs(result, "Principal.3.name", "Michael");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Group(_pageSize: 1) { name }, Principal(_pageSize: 2, _page: 2) { name(_contains: \"i\") } }");
            assertMapPathValueIs(result, "Group.#", 1);
            assertMapPathValueIs(result, "Group.0.name", "All teams");
            assertMapPathValueIs(result, "Principal.#", 2);
            assertMapPathValueIs(result, "Principal.0.name", "Ins");
            assertMapPathValueIs(result, "Principal.1.name", "Kai");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Group { name, members(_pageSize: 2, _page: 2) { name }}}");
            assertMapPathValueIs(result, "Group.#", 2);
            assertMapPathValueIs(result, "Group.0.name", "All teams");
            assertMapPathValueIs(result, "Group.0.members", new LinkedList<>());
            assertMapPathValueIs(result, "Group.1.name", "Structr Team");
            assertMapPathValueIs(result, "Group.1.members.#", 2);
            assertMapPathValueIs(result, "Group.1.members.0.name", "Christian");
            assertMapPathValueIs(result, "Group.1.members.1.name", "Ins");
        }

        {
            final Map<String, Object> result = fetchGraphQL("{ Group { name, members(_sort: \"name\") { name }}}");
            assertMapPathValueIs(result, "Group.#", 2);
            assertMapPathValueIs(result, "Group.0.name", "All teams");
            assertMapPathValueIs(result, "Group.0.members.#", 1);
            assertMapPathValueIs(result, "Group.1.name", "Structr Team");
            assertMapPathValueIs(result, "Group.1.members.#", 9);
            assertMapPathValueIs(result, "Group.1.members.0.name", "Axel");
            assertMapPathValueIs(result, "Group.1.members.1.name", "Christian");
            assertMapPathValueIs(result, "Group.1.members.2.name", "Christian");
            assertMapPathValueIs(result, "Group.1.members.3.name", "Ins");
            assertMapPathValueIs(result, "Group.1.members.4.name", "Kai");
            assertMapPathValueIs(result, "Group.1.members.5.name", "Lukas");
            assertMapPathValueIs(result, "Group.1.members.6.name", "Michael");
            assertMapPathValueIs(result, "Group.1.members.7.name", "Susanne");
            assertMapPathValueIs(result, "Group.1.members.8.name", "Tobias");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Group { name, members(_sort: \"name\", _desc: true) { name }}}");
            assertMapPathValueIs(result, "Group.#", 2);
            assertMapPathValueIs(result, "Group.0.name", "All teams");
            assertMapPathValueIs(result, "Group.0.members.#", 1);
            assertMapPathValueIs(result, "Group.1.name", "Structr Team");
            assertMapPathValueIs(result, "Group.1.members.#", 9);
            assertMapPathValueIs(result, "Group.1.members.8.name", "Axel");
            assertMapPathValueIs(result, "Group.1.members.7.name", "Christian");
            assertMapPathValueIs(result, "Group.1.members.6.name", "Christian");
            assertMapPathValueIs(result, "Group.1.members.5.name", "Ins");
            assertMapPathValueIs(result, "Group.1.members.4.name", "Kai");
            assertMapPathValueIs(result, "Group.1.members.3.name", "Lukas");
            assertMapPathValueIs(result, "Group.1.members.2.name", "Michael");
            assertMapPathValueIs(result, "Group.1.members.1.name", "Susanne");
            assertMapPathValueIs(result, "Group.1.members.0.name", "Tobias");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Group { members { name(_contains: \"k\", _contains: \"l\", _conj: \"and\") }}}");
            assertMapPathValueIs(result, "Group.#", 2);
            assertMapPathValueIs(result, "Group.0.name", null);
            assertMapPathValueIs(result, "Group.0.members", new LinkedList<>());
            assertMapPathValueIs(result, "Group.1.members.0.name", "Lukas");
            assertMapPathValueIs(result, "Group.1.members.1", null);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Group { members { name(_contains: \"k\", _contains: \"l\", _conj: \"or\") }}}");
            assertMapPathValueIs(result, "Group.#", 2);
            assertMapPathValueIs(result, "Group.0.name", null);
            assertMapPathValueIs(result, "Group.0.members", new LinkedList<>());
            assertMapPathValueIs(result, "Group.1.members.#", 4);
            assertMapPathValueIs(result, "Group.1.members.0.name", "Axel");
            assertMapPathValueIs(result, "Group.1.members.1.name", "Kai");
            assertMapPathValueIs(result, "Group.1.members.2.name", "Lukas");
            assertMapPathValueIs(result, "Group.1.members.3.name", "Michael");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate { id, type, text(_contains: \"2\"), owner(_equals: { name: \"Axel\"}) { name } }}");
            assertMapPathValueIs(result, "MailTemplate.#", 1);
            assertMapPathValueIs(result, "MailTemplate.0.id", templates.get(1).getUuid());
            assertMapPathValueIs(result, "MailTemplate.0.type", "MailTemplate");
            assertMapPathValueIs(result, "MailTemplate.0.text", "MailTemplate2");
            assertMapPathValueIs(result, "MailTemplate.0.name", null);
            assertMapPathValueIs(result, "MailTemplate.0.owner.name", "Axel");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(_sort: \"owner.name\") { name, owner { name }}}");
            assertMapPathValueIs(result, "MailTemplate.#", 6);
            assertMapPathValueIs(result, "MailTemplate.0.name", "abcdef");
            assertMapPathValueIs(result, "MailTemplate.0.owner.name", "Axel");
            assertMapPathValueIs(result, "MailTemplate.1.name", "lertdf");
            assertMapPathValueIs(result, "MailTemplate.1.owner.name", "Axel");
            assertMapPathValueIs(result, "MailTemplate.2.name", "asgw");
            assertMapPathValueIs(result, "MailTemplate.2.owner.name", "Christian");
            assertMapPathValueIs(result, "MailTemplate.3.name", "zrtsga");
            assertMapPathValueIs(result, "MailTemplate.3.owner.name", "Christian");
            assertMapPathValueIs(result, "MailTemplate.4.name", "tzegsg");
            assertMapPathValueIs(result, "MailTemplate.4.owner.name", "Ins");
            assertMapPathValueIs(result, "MailTemplate.5.name", "dfjgr");
            assertMapPathValueIs(result, "MailTemplate.5.owner.name", "Kai");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Group { name, members(_sort: \"name\", _desc: true) { name }}}");
            assertMapPathValueIs(result, "Group.#", 2);
            assertMapPathValueIs(result, "Group.0.name", "All teams");
            assertMapPathValueIs(result, "Group.0.members.#", 1);
            assertMapPathValueIs(result, "Group.1.name", "Structr Team");
            assertMapPathValueIs(result, "Group.1.members.#", 9);
            assertMapPathValueIs(result, "Group.1.members.8.name", "Axel");
            assertMapPathValueIs(result, "Group.1.members.7.name", "Christian");
            assertMapPathValueIs(result, "Group.1.members.6.name", "Christian");
            assertMapPathValueIs(result, "Group.1.members.5.name", "Ins");
            assertMapPathValueIs(result, "Group.1.members.4.name", "Kai");
            assertMapPathValueIs(result, "Group.1.members.3.name", "Lukas");
            assertMapPathValueIs(result, "Group.1.members.2.name", "Michael");
            assertMapPathValueIs(result, "Group.1.members.1.name", "Susanne");
            assertMapPathValueIs(result, "Group.1.members.0.name", "Tobias");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(_pageSize: 2, _sort: \"name\", owner: { name: { _contains: \"x\" }} ) { id, type, name, owner { name }}}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
            assertMapPathValueIs(result, "MailTemplate.0.id", templates.get(5).getUuid());
            assertMapPathValueIs(result, "MailTemplate.0.type", "MailTemplate");
            assertMapPathValueIs(result, "MailTemplate.0.name", "abcdef");
            assertMapPathValueIs(result, "MailTemplate.0.owner.name", "Axel");
            assertMapPathValueIs(result, "MailTemplate.1.id", templates.get(1).getUuid());
            assertMapPathValueIs(result, "MailTemplate.1.type", "MailTemplate");
            assertMapPathValueIs(result, "MailTemplate.1.name", "lertdf");
            assertMapPathValueIs(result, "MailTemplate.1.owner.name", "Axel");
        }
    }

    @Test
    public void testAdvancedQueriesManyToMany() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType task = schema.addType("Task");

            project.relate(task, "HAS", Relation.Cardinality.ManyToMany, "projects", "tasks");

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        final List<NodeInterface> projects = new LinkedList<>();
        final List<NodeInterface> tasks = new LinkedList<>();
        final Class project = StructrApp.getConfiguration().getNodeEntityClass("Project");
        final Class task = StructrApp.getConfiguration().getNodeEntityClass("Task");
        final PropertyKey tasksKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(project, "tasks");

        try (final Tx tx = app.tx()) {

            tasks.add(app.create(task, "task1"));
            tasks.add(app.create(task, "task2"));
            tasks.add(app.create(task, "task3"));
            tasks.add(app.create(task, "task4"));
            tasks.add(app.create(task, "task5"));

            projects.add(app.create(project, "project1"));
            projects.add(app.create(project, "project2"));
            projects.add(app.create(project, "project3"));
            projects.add(app.create(project, "project4"));
            projects.add(app.create(project, "project5"));

            for (int i = 0; i < 5; i++) {
                projects.get(i).setProperty(tasksKey, tasks.subList(i, 5));
            }

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task1\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task2\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 2);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task3\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 3);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task4\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 4);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task5\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 5);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _contains: \"task\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 5);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(projects: { name: { _equals: \"project1\"}}) { name, projects { name }}}");
            assertMapPathValueIs(result, "Task.#", 5);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(projects: { name: { _equals: \"project2\"}}) { name, projects { name }}}");
            assertMapPathValueIs(result, "Task.#", 4);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(projects: { name: { _equals: \"project3\"}}) { name, projects { name }}}");
            assertMapPathValueIs(result, "Task.#", 3);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(projects: { name: { _equals: \"project4\"}}) { name, projects { name }}}");
            assertMapPathValueIs(result, "Task.#", 2);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(projects: { name: { _equals: \"project5\"}}) { name, projects { name }}}");
            assertMapPathValueIs(result, "Task.#", 1);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(projects: { name: { _contains: \"project\"}}) { name, projects { name }}}");
            assertMapPathValueIs(result, "Task.#", 5);
        }
    }

    @Test
    public void testAdvancedQueriesOneToMany() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType task = schema.addType("Task");

            project.relate(task, "HAS", Relation.Cardinality.OneToMany, "project", "tasks");

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        final List<NodeInterface> projects = new LinkedList<>();
        final List<NodeInterface> tasks = new LinkedList<>();
        final Class project = StructrApp.getConfiguration().getNodeEntityClass("Project");
        final Class task = StructrApp.getConfiguration().getNodeEntityClass("Task");
        final PropertyKey tasksKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(project, "tasks");

        try (final Tx tx = app.tx()) {

            tasks.add(app.create(task, "task0"));
            tasks.add(app.create(task, "task1"));
            tasks.add(app.create(task, "task2"));
            tasks.add(app.create(task, "task3"));
            tasks.add(app.create(task, "task4"));
            tasks.add(app.create(task, "task5"));
            tasks.add(app.create(task, "task6"));
            tasks.add(app.create(task, "task7"));
            tasks.add(app.create(task, "task8"));
            tasks.add(app.create(task, "task9"));

            projects.add(app.create(project, "project1"));
            projects.add(app.create(project, "project2"));
            projects.add(app.create(project, "project3"));
            projects.add(app.create(project, "project4"));
            projects.add(app.create(project, "project5"));

            projects.get(0).setProperty(tasksKey, tasks.subList(0, 2));
            projects.get(1).setProperty(tasksKey, tasks.subList(2, 4));
            projects.get(2).setProperty(tasksKey, tasks.subList(4, 6));
            projects.get(3).setProperty(tasksKey, tasks.subList(6, 8));
            projects.get(4).setProperty(tasksKey, tasks.subList(8, 10));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task1\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task3\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task5\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task7\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task9\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(project: { name: { _equals: \"project1\"}}) { name, project { name }}}");
            assertMapPathValueIs(result, "Task.#", 2);
            assertMapPathValueIs(result, "Task.0.name", "task0");
            assertMapPathValueIs(result, "Task.1.name", "task1");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(project: { name: { _equals: \"project2\"}}) { name, project { name }}}");
            assertMapPathValueIs(result, "Task.#", 2);
            assertMapPathValueIs(result, "Task.0.name", "task2");
            assertMapPathValueIs(result, "Task.1.name", "task3");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(project: { name: { _equals: \"project3\"}}) { name, project { name }}}");
            assertMapPathValueIs(result, "Task.#", 2);
            assertMapPathValueIs(result, "Task.0.name", "task4");
            assertMapPathValueIs(result, "Task.1.name", "task5");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(project: { name: { _equals: \"project4\"}}) { name, project { name }}}");
            assertMapPathValueIs(result, "Task.#", 2);
            assertMapPathValueIs(result, "Task.0.name", "task6");
            assertMapPathValueIs(result, "Task.1.name", "task7");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(project: { name: { _equals: \"project5\"}}) { name, project { name }}}");
            assertMapPathValueIs(result, "Task.#", 2);
            assertMapPathValueIs(result, "Task.0.name", "task8");
            assertMapPathValueIs(result, "Task.1.name", "task9");
        }
    }

    @Test
    public void testAdvancedQueriesManyToOne() {

        // test data setup
        try (final Tx tx = app.tx()) {

            final Principal p1 = app.create(Principal.class, "p1");
            final Principal p2 = app.create(Principal.class, "p2");
            final MailTemplate m1 = app.create(MailTemplate.class, "m1");
            final MailTemplate m2 = app.create(MailTemplate.class, "m2");
            final MailTemplate m3 = app.create(MailTemplate.class, "m3");
            final MailTemplate m4 = app.create(MailTemplate.class, "m4");

            m1.setProperty(MailTemplate.owner, p1);
            m2.setProperty(MailTemplate.owner, p1);
            m3.setProperty(MailTemplate.owner, p2);
            m4.setProperty(MailTemplate.owner, p2);

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _equals: \"p2\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
            assertMapPathValueIs(result, "MailTemplate.0.name", "m3");
            assertMapPathValueIs(result, "MailTemplate.1.name", "m4");
        }

    }

    @Test
    public void testAdvancedQueriesManyToOneWithEdgeCases() {

        // test data setup
        try (final Tx tx = app.tx()) {

            final Principal p1 = app.create(Principal.class, "First Tester");
            final Principal p2 = app.create(Principal.class, "Second Tester");
            final MailTemplate m1 = app.create(MailTemplate.class, "First Template");
            final MailTemplate m2 = app.create(MailTemplate.class, "Second Template");
            final MailTemplate m3 = app.create(MailTemplate.class, "Third Template");
            final MailTemplate m4 = app.create(MailTemplate.class, "Fourth Template");
            final MailTemplate m5 = app.create(MailTemplate.class, "Fifth Template");
            final MailTemplate m6 = app.create(MailTemplate.class, "Sixth Template");

            m1.setProperty(MailTemplate.owner, p1);
            m2.setProperty(MailTemplate.owner, p1);
            m3.setProperty(MailTemplate.owner, p2);
            m4.setProperty(MailTemplate.owner, p2);

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        {
            // expect no results because no owner name matches the given name filter
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _equals: \"none\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 0);
        }

        {
            // expect two results because one owner with two templates matches the given name filter
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _equals: \"First Tester\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
            assertMapPathValueIs(result, "MailTemplate.0.name", "First Template");
            assertMapPathValueIs(result, "MailTemplate.1.name", "Second Template");
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"f\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"fi\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"fir\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"firs\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"first\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 2);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"t\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 4);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"te\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 4);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"tes\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 4);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"test\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 4);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ MailTemplate(owner: { name: { _contains: \"tester\"}}) { name }}");
            assertMapPathValueIs(result, "MailTemplate.#", 4);
        }

    }

    @Test
    public void testFunctionPropertyQueries() {

        // schema setup
        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType type = schema.addType("FunctionTest");
            final JsonFunctionProperty stringTest = type.addFunctionProperty("stringTest");
            final JsonFunctionProperty boolTest = type.addFunctionProperty("boolTest");

            stringTest.setReadFunction("if(eq(this.name, 'test1'), 'true', 'false')");
            stringTest.setIndexed(true);
            stringTest.setTypeHint("String");

            boolTest.setReadFunction("if(eq(this.name, 'test2'), true, false)");
            boolTest.setIndexed(true);
            boolTest.setTypeHint("Boolean");

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        // test data setup
        try (final Tx tx = app.tx()) {

            final Class type = StructrApp.getConfiguration().getNodeEntityClass("FunctionTest");

            app.create(type, "test1");
            app.create(type, "test2");

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ FunctionTest(stringTest: {_equals: \"true\"}) { id, type, name, stringTest, boolTest }}");
            assertMapPathValueIs(result, "FunctionTest.#", 1);
            assertMapPathValueIs(result, "FunctionTest.0.name", "test1");
            assertMapPathValueIs(result, "FunctionTest.0.stringTest", "true");
            assertMapPathValueIs(result, "FunctionTest.0.boolTest", false);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ FunctionTest(stringTest: {_equals: \"false\"}) { id, type, name, stringTest, boolTest }}");
            assertMapPathValueIs(result, "FunctionTest.#", 1);
            assertMapPathValueIs(result, "FunctionTest.0.name", "test2");
            assertMapPathValueIs(result, "FunctionTest.0.stringTest", "false");
            assertMapPathValueIs(result, "FunctionTest.0.boolTest", true);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ FunctionTest(stringTest: {_contains: \"e\"}) { id, type, name, stringTest, boolTest }}");
            assertMapPathValueIs(result, "FunctionTest.#", 2);
            assertMapPathValueIs(result, "FunctionTest.0.name", "test1");
            assertMapPathValueIs(result, "FunctionTest.0.stringTest", "true");
            assertMapPathValueIs(result, "FunctionTest.0.boolTest", false);
            assertMapPathValueIs(result, "FunctionTest.1.name", "test2");
            assertMapPathValueIs(result, "FunctionTest.1.stringTest", "false");
            assertMapPathValueIs(result, "FunctionTest.1.boolTest", true);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ FunctionTest(boolTest: {_equals: true}) { id, type, name, stringTest, boolTest }}");
            assertMapPathValueIs(result, "FunctionTest.#", 1);
            assertMapPathValueIs(result, "FunctionTest.0.name", "test2");
            assertMapPathValueIs(result, "FunctionTest.0.stringTest", "false");
            assertMapPathValueIs(result, "FunctionTest.0.boolTest", true);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ FunctionTest(boolTest: {_equals: false}) { id, type, name, stringTest, boolTest }}");
            assertMapPathValueIs(result, "FunctionTest.#", 1);
            assertMapPathValueIs(result, "FunctionTest.0.name", "test1");
            assertMapPathValueIs(result, "FunctionTest.0.stringTest", "true");
            assertMapPathValueIs(result, "FunctionTest.0.boolTest", false);
        }
    }

    @Test
    public void testSchema() {

        RestAssured.basePath = "/structr/graphql";

        RestAssured.given()

                .contentType("application/json; charset=UTF-8")
                .filter(ResponseLoggingFilter.logResponseTo(System.out)).body("{ __schema { types { name } }}")

                .expect().statusCode(200)

                .when().post("/");
    }

    @Test
    public void testMixedSchemaError() {

        RestAssured.basePath = "/structr/graphql";

        /*
         * Structr uses two different methods of creating a GraphQL result, depending on the type of query. If a __schema query is
         * sent, Structr delegates query execution to graphql-java, otherwise Structr uses its own query methods and only uses the
         * structure information from graphql to control the output views on each level. Because of that, Structr cannot support
         * GraphQL queries that request both schema and data information, and throws an exception.
         */

        final String query1 = "{ __schema { types { name }}, Group { name }}";
        final String query2 = "{ Group { name }, __schema { types { name }} }";

        RestAssured.given()

                .contentType("application/json; charset=UTF-8").body(query1)

                .expect().statusCode(422)
                .body("message", equalTo("Unsupported query type, schema and data queries cannot be mixed."))
                .body("code", equalTo(422)).body("query", equalTo(query1))

                .when().post("/");

        RestAssured.given()

                .contentType("application/json; charset=UTF-8").body(query2)

                .expect().statusCode(422)
                .body("message", equalTo("Unsupported query type, schema and data queries cannot be mixed."))
                .body("code", equalTo(422)).body("query", equalTo(query2))

                .when().post("/");
    }

    @Test
    public void testGraphQLErrorMessages() {

        RestAssured.basePath = "/structr/graphql";

        final String query1 = "{ Group { id. type, name, members } }";
        final String query2 = "{ Group { id. type, name, owner } }";

        RestAssured.given()

                .filter(ResponseLoggingFilter.logResponseTo(System.out))
                .contentType("application/json; charset=UTF-8").body(query1)

                .expect().statusCode(422)
                .body("errors[0].message", equalTo(
                        "Validation error of type SubSelectionRequired: Sub selection required for type Principal of field members"))
                .body("errors[0].locations[0].line", equalTo(1)).body("errors[0].locations[0].column", equalTo(27))
                .body("errors[0].description",
                        equalTo("Sub selection required for type Principal of field members"))
                .body("errors[0].validationErrorType", equalTo("SubSelectionRequired"))

                .when().post("/");

        RestAssured.given()

                .filter(ResponseLoggingFilter.logResponseTo(System.out))
                .contentType("application/json; charset=UTF-8").body(query2)

                .expect().statusCode(422)
                .body("errors[0].message", equalTo(
                        "Validation error of type SubSelectionRequired: Sub selection required for type Principal of field owner"))
                .body("errors[0].locations[0].line", equalTo(1)).body("errors[0].locations[0].column", equalTo(27))
                .body("errors[0].description", equalTo("Sub selection required for type Principal of field owner"))
                .body("errors[0].validationErrorType", equalTo("SubSelectionRequired"))

                .when().post("/");
    }

    @Test
    public void testFunctionPropertyTypeHint() {

        RestAssured.basePath = "/structr/graphql";

        List<NodeInterface> children = null;
        Principal user = null;

        try (final Tx tx = app.tx()) {

            final JsonSchema schema = StructrSchema.createFromDatabase(app);
            final JsonObjectType type = schema.addType("Test");
            final JsonObjectType tmpType = schema.addType("Tmp");

            type.relate(tmpType, "TMP", Relation.Cardinality.OneToMany, "parent", "children");

            type.addFunctionProperty("test1").setReadFunction("'test'").setTypeHint("String");
            type.addFunctionProperty("test2").setReadFunction("false").setTypeHint("Boolean");
            type.addFunctionProperty("test3").setReadFunction("int(42)").setTypeHint("Int");
            type.addFunctionProperty("test4").setReadFunction("12.34").setTypeHint("Double");
            type.addFunctionProperty("test5").setReadFunction("7465423674522").setTypeHint("Long");
            type.addFunctionProperty("test6").setReadFunction("this.owner").setTypeHint("Principal");
            type.addFunctionProperty("test7").setReadFunction("this.children").setTypeHint("Tmp[]");

            StructrSchema.replaceDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
            fail("Unexpected exception.");
        }

        // create test node
        try (final Tx tx = app.tx()) {

            user = app.create(Principal.class, "tester");

            final Class tmpType = StructrApp.getConfiguration().getNodeEntityClass("Tmp");
            final Class testType = StructrApp.getConfiguration().getNodeEntityClass("Test");

            final PropertyKey nameKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(testType, "name");
            final PropertyKey ownerKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(testType, "owner");
            final PropertyKey childrenKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(testType,
                    "children");

            children = createTestNodes(tmpType, 10);

            app.create(testType, new NodeAttribute<>(nameKey, "Test"), new NodeAttribute<>(ownerKey, user),
                    new NodeAttribute<>(childrenKey, children));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
            fail("Unexpected exception.");
        }

        final Map<String, Object> result = fetchGraphQL(
                "{ Test { test1, test2, test3, test4, test5, test6 { id, type, name }, test7 { id, type } }}");
        assertMapPathValueIs(result, "Test.0.test1", "test");
        assertMapPathValueIs(result, "Test.0.test2", false);
        assertMapPathValueIs(result, "Test.0.test3", 42.0);
        assertMapPathValueIs(result, "Test.0.test4", 12.34);
        assertMapPathValueIs(result, "Test.0.test5", 7.465423674522E12);
        assertMapPathValueIs(result, "Test.0.test6.id", user.getUuid());
        assertMapPathValueIs(result, "Test.0.test6.type", "Principal");
        assertMapPathValueIs(result, "Test.0.test6.name", "tester");
        assertMapPathValueIs(result, "Test.0.test7.#", 10);

        for (int i = 0; i < 10; i++) {
            assertMapPathValueIs(result, "Test.0.test7." + i + ".id", children.get(i).getUuid());
            assertMapPathValueIs(result, "Test.0.test7." + i + ".type", "Tmp");
        }

        final String query = "{ Test { test6, test7 } }";

        RestAssured.given()

                .filter(ResponseLoggingFilter.logResponseTo(System.out))
                .contentType("application/json; charset=UTF-8").body(query)

                .expect().statusCode(422)
                .body("errors[0].message", equalTo(
                        "Validation error of type SubSelectionRequired: Sub selection required for type Principal of field test6"))
                .body("errors[0].locations[0].line", equalTo(1)).body("errors[0].locations[0].column", equalTo(10))
                .body("errors[0].description", equalTo("Sub selection required for type Principal of field test6"))
                .body("errors[0].validationErrorType", equalTo("SubSelectionRequired"))
                .body("errors[1].message", equalTo(
                        "Validation error of type SubSelectionRequired: Sub selection required for type Tmp of field test7"))
                .body("errors[1].locations[0].line", equalTo(1)).body("errors[1].locations[0].column", equalTo(17))
                .body("errors[1].description", equalTo("Sub selection required for type Tmp of field test7"))
                .body("errors[1].validationErrorType", equalTo("SubSelectionRequired"))

                .when().post("/");
    }

    @Test
    public void testInheritedRelationshipProperties() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType task = schema.addType("Task");
            final JsonObjectType extProject1 = schema.addType("ExtendedProject1");
            final JsonObjectType extProject2 = schema.addType("ExtendedProject2");

            extProject1.setExtends(project);
            extProject2.setExtends(project);

            project.relate(task, "HAS", Relation.Cardinality.OneToMany, "project", "tasks");

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        final List<NodeInterface> projects = new LinkedList<>();
        final List<NodeInterface> tasks = new LinkedList<>();
        final Class extProject = StructrApp.getConfiguration().getNodeEntityClass("ExtendedProject1");
        final Class task = StructrApp.getConfiguration().getNodeEntityClass("Task");
        final PropertyKey tasksKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(extProject, "tasks");

        try (final Tx tx = app.tx()) {

            tasks.add(app.create(task, "task0"));
            tasks.add(app.create(task, "task1"));
            tasks.add(app.create(task, "task2"));
            tasks.add(app.create(task, "task3"));
            tasks.add(app.create(task, "task4"));
            tasks.add(app.create(task, "task5"));
            tasks.add(app.create(task, "task6"));
            tasks.add(app.create(task, "task7"));
            tasks.add(app.create(task, "task8"));
            tasks.add(app.create(task, "task9"));

            projects.add(app.create(extProject, "project1"));
            projects.add(app.create(extProject, "project2"));
            projects.add(app.create(extProject, "project3"));
            projects.add(app.create(extProject, "project4"));
            projects.add(app.create(extProject, "project5"));

            projects.get(0).setProperty(tasksKey, tasks.subList(0, 2));
            projects.get(1).setProperty(tasksKey, tasks.subList(2, 4));
            projects.get(2).setProperty(tasksKey, tasks.subList(4, 6));
            projects.get(3).setProperty(tasksKey, tasks.subList(6, 8));
            projects.get(4).setProperty(tasksKey, tasks.subList(8, 10));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { name: { _equals: \"task1\"}}) { name, tasks { name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
        }
    }

    @Test
    public void testCountPropertyType() {

        try (final Tx tx = app.tx()) {

            final SchemaNode projectType = app.create(SchemaNode.class, "Project");
            final SchemaNode taskType = app.create(SchemaNode.class, "Task");

            final SchemaRelationshipNode rel = app.create(SchemaRelationshipNode.class,
                    new NodeAttribute<>(SchemaRelationshipNode.sourceNode, projectType),
                    new NodeAttribute<>(SchemaRelationshipNode.targetNode, taskType),
                    new NodeAttribute<>(SchemaRelationshipNode.relationshipType, "TASK"),
                    new NodeAttribute<>(SchemaRelationshipNode.sourceMultiplicity, "1"),
                    new NodeAttribute<>(SchemaRelationshipNode.targetMultiplicity, "*"),
                    new NodeAttribute<>(SchemaRelationshipNode.sourceJsonName, "project"),
                    new NodeAttribute<>(SchemaRelationshipNode.targetJsonName, "tasks"));

            app.create(SchemaProperty.class, new NodeAttribute<>(SchemaProperty.schemaNode, projectType),
                    new NodeAttribute<>(SchemaProperty.name, "taskCount"),
                    new NodeAttribute<>(SchemaProperty.propertyType, "Count"),
                    new NodeAttribute<>(SchemaProperty.format, "tasks"));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        try (final Tx tx = app.tx()) {

            final Class projectType = StructrApp.getConfiguration().getNodeEntityClass("Project");
            final Class taskType = StructrApp.getConfiguration().getNodeEntityClass("Task");
            final PropertyKey name = StructrApp.getConfiguration().getPropertyKeyForJSONName(projectType, "name");
            final PropertyKey tasks = StructrApp.getConfiguration().getPropertyKeyForJSONName(projectType, "tasks");

            final List<NodeInterface> taskList = createTestNodes(taskType, 10);

            final NodeInterface project = app.create(projectType, new NodeAttribute<>(name, "Test"),
                    new NodeAttribute<>(tasks, taskList));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project { id, type, name, taskCount, tasks { id, type, name }}}");
            assertMapPathValueIs(result, "Project.#", 1);
            assertMapPathValueIs(result, "Project.0.name", "Test");
            assertMapPathValueIs(result, "Project.0.tasks.#", 10);
            assertMapPathValueIs(result, "Project.0.taskCount", 10.0);
        }
    }

    @Test
    public void testCompositeQueries() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType task = schema.addType("Task");

            project.relate(task, "HAS", Relation.Cardinality.OneToMany, "project", "tasks");

            final JsonFunctionProperty p1 = task.addFunctionProperty("projectId");
            p1.setIndexed(true);
            p1.setTypeHint("String");
            p1.setFormat("this.project.id");

            final JsonFunctionProperty p2 = project.addFunctionProperty("taskCount");
            p2.setIndexed(true);
            p2.setTypeHint("Int");
            p2.setFormat("size(this.tasks)");

            final JsonEnumProperty p3 = task.addEnumProperty("status");
            p3.setIndexed(true);
            p3.setEnums("open", "closed", "cancelled");

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        final List<NodeInterface> projects = new LinkedList<>();
        final List<NodeInterface> tasks = new LinkedList<>();
        final Class project = StructrApp.getConfiguration().getNodeEntityClass("Project");
        final Class task = StructrApp.getConfiguration().getNodeEntityClass("Task");
        final PropertyKey tasksKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(project, "tasks");
        final EnumProperty statusKey = (EnumProperty) StructrApp.getConfiguration().getPropertyKeyForJSONName(task,
                "status");
        final PropertyKey nameKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(task, "name");

        ;

        try (final Tx tx = app.tx()) {

            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task0"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "open"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task1"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "closed"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task2"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "cancelled"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task3"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "open"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task4"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "closed"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task5"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "cancelled"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task6"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "open"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task7"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "closed"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task8"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "cancelled"))));
            tasks.add(app.create(task, new NodeAttribute<>(nameKey, "task9"),
                    new NodeAttribute<>(statusKey, Enum.valueOf(statusKey.getEnumType(), "open"))));

            projects.add(app.create(project, "project1"));
            projects.add(app.create(project, "project2"));
            projects.add(app.create(project, "project3"));
            projects.add(app.create(project, "project4"));
            projects.add(app.create(project, "project5"));

            projects.get(0).setProperty(tasksKey, tasks.subList(0, 2));
            projects.get(1).setProperty(tasksKey, tasks.subList(2, 4));
            projects.get(2).setProperty(tasksKey, tasks.subList(4, 6));
            projects.get(3).setProperty(tasksKey, tasks.subList(6, 8));
            projects.get(4).setProperty(tasksKey, tasks.subList(8, 10));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project { id, type, name, taskCount, tasks { id, type, name, projectId, status }}}");
            assertMapPathValueIs(result, "Project.#", 5);
            assertMapPathValueIs(result, "Project.0.name", "project1");
            assertMapPathValueIs(result, "Project.0.taskCount", 2.0);
            assertMapPathValueIs(result, "Project.0.tasks.#", 2);
            assertMapPathValueIs(result, "Project.0.tasks.0.name", "task0");
            assertMapPathValueIs(result, "Project.0.tasks.0.status", "open");
            assertMapPathValueIs(result, "Project.0.tasks.0.projectId", projects.get(0).getUuid());
            assertMapPathValueIs(result, "Project.0.tasks.1.name", "task1");
            assertMapPathValueIs(result, "Project.0.tasks.1.status", "closed");
            assertMapPathValueIs(result, "Project.0.tasks.1.projectId", projects.get(0).getUuid());

            assertMapPathValueIs(result, "Project.1.name", "project2");
            assertMapPathValueIs(result, "Project.1.taskCount", 2.0);
            assertMapPathValueIs(result, "Project.1.tasks.#", 2);
            assertMapPathValueIs(result, "Project.1.tasks.0.name", "task2");
            assertMapPathValueIs(result, "Project.1.tasks.0.status", "cancelled");
            assertMapPathValueIs(result, "Project.1.tasks.0.projectId", projects.get(1).getUuid());
            assertMapPathValueIs(result, "Project.1.tasks.1.name", "task3");
            assertMapPathValueIs(result, "Project.1.tasks.1.status", "open");
            assertMapPathValueIs(result, "Project.1.tasks.1.projectId", projects.get(1).getUuid());

            assertMapPathValueIs(result, "Project.2.name", "project3");
            assertMapPathValueIs(result, "Project.2.taskCount", 2.0);
            assertMapPathValueIs(result, "Project.2.tasks.#", 2);
            assertMapPathValueIs(result, "Project.2.tasks.0.name", "task4");
            assertMapPathValueIs(result, "Project.2.tasks.0.status", "closed");
            assertMapPathValueIs(result, "Project.2.tasks.0.projectId", projects.get(2).getUuid());
            assertMapPathValueIs(result, "Project.2.tasks.1.name", "task5");
            assertMapPathValueIs(result, "Project.2.tasks.1.status", "cancelled");
            assertMapPathValueIs(result, "Project.2.tasks.1.projectId", projects.get(2).getUuid());

            assertMapPathValueIs(result, "Project.3.name", "project4");
            assertMapPathValueIs(result, "Project.3.taskCount", 2.0);
            assertMapPathValueIs(result, "Project.3.tasks.#", 2);
            assertMapPathValueIs(result, "Project.3.tasks.0.name", "task6");
            assertMapPathValueIs(result, "Project.3.tasks.0.status", "open");
            assertMapPathValueIs(result, "Project.3.tasks.0.projectId", projects.get(3).getUuid());
            assertMapPathValueIs(result, "Project.3.tasks.1.name", "task7");
            assertMapPathValueIs(result, "Project.3.tasks.1.status", "closed");
            assertMapPathValueIs(result, "Project.3.tasks.1.projectId", projects.get(3).getUuid());

            assertMapPathValueIs(result, "Project.4.name", "project5");
            assertMapPathValueIs(result, "Project.4.taskCount", 2.0);
            assertMapPathValueIs(result, "Project.4.tasks.#", 2);
            assertMapPathValueIs(result, "Project.4.tasks.0.name", "task8");
            assertMapPathValueIs(result, "Project.4.tasks.0.status", "cancelled");
            assertMapPathValueIs(result, "Project.4.tasks.0.projectId", projects.get(4).getUuid());
            assertMapPathValueIs(result, "Project.4.tasks.1.name", "task9");
            assertMapPathValueIs(result, "Project.4.tasks.1.status", "open");
            assertMapPathValueIs(result, "Project.4.tasks.1.projectId", projects.get(4).getUuid());
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(id: \"" + tasks.get(0).getUuid() + "\") { name, status, projectId }}");

            assertMapPathValueIs(result, "Task.#", 1);
            assertMapPathValueIs(result, "Task.0.name", "task0");
            assertMapPathValueIs(result, "Task.0.status", "open");
            assertMapPathValueIs(result, "Task.0.projectId", projects.get(0).getUuid());
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task { id(_equals: \"" + tasks.get(0).getUuid() + "\"), name, status, projectId }}");

            assertMapPathValueIs(result, "Task.#", 1);
            assertMapPathValueIs(result, "Task.0.name", "task0");
            assertMapPathValueIs(result, "Task.0.status", "open");
            assertMapPathValueIs(result, "Task.0.projectId", projects.get(0).getUuid());
        }

        {
            final Map<String, Object> result = fetchGraphQL("{ Task(id: \"" + tasks.get(0).getUuid()
                    + "\", status: { _equals: \"open\" }) { name, status, projectId }}");

            assertMapPathValueIs(result, "Task.#", 1);
            assertMapPathValueIs(result, "Task.0.name", "task0");
            assertMapPathValueIs(result, "Task.0.status", "open");
            assertMapPathValueIs(result, "Task.0.projectId", projects.get(0).getUuid());
        }

        {
            final Map<String, Object> result = fetchGraphQL("{ Task(id: \"" + tasks.get(0).getUuid()
                    + "\", status: { _equals: \"closed\" }) { name, status, projectId }}");

            assertMapPathValueIs(result, "Task.#", 0);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(id: \"" + tasks.get(0).getUuid() + "\", projectId: { _equals: \""
                            + projects.get(0).getUuid() + "\" }) { name, status, projectId }}");

            assertMapPathValueIs(result, "Task.#", 1);
            assertMapPathValueIs(result, "Task.0.name", "task0");
            assertMapPathValueIs(result, "Task.0.status", "open");
            assertMapPathValueIs(result, "Task.0.projectId", projects.get(0).getUuid());
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task(id: \"" + tasks.get(0).getUuid() + "\", projectId: { _equals: \""
                            + projects.get(1).getUuid() + "\" }) { name, status, projectId }}");

            assertMapPathValueIs(result, "Task.#", 0);
        }
    }

    @Test
    public void testPropertiesForCorrectInputType() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");

            project.addBooleanProperty("testBoolean").setIndexed(true);
            project.addLongProperty("testLong").setIndexed(true);
            project.addNumberProperty("testDouble").setIndexed(true);
            project.addIntegerProperty("testInt").setIndexed(true);

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (Throwable fex) {
            fex.printStackTrace();
        }

        try (final Tx tx = app.tx()) {

            final Class projectType = StructrApp.getConfiguration().getNodeEntityClass("Project");

            final PropertyKey testBoolean = StructrApp.getConfiguration().getPropertyKeyForJSONName(projectType,
                    "testBoolean");
            final PropertyKey testDouble = StructrApp.getConfiguration().getPropertyKeyForJSONName(projectType,
                    "testDouble");
            final PropertyKey testLong = StructrApp.getConfiguration().getPropertyKeyForJSONName(projectType,
                    "testLong");
            final PropertyKey testInt = StructrApp.getConfiguration().getPropertyKeyForJSONName(projectType,
                    "testInt");

            app.create(projectType, new NodeAttribute<>(testBoolean, true), new NodeAttribute<>(testDouble, 252.52),
                    new NodeAttribute<>(testLong, 234532L), new NodeAttribute<>(testInt, 4563332));

            app.create(projectType, new NodeAttribute<>(testBoolean, false),
                    new NodeAttribute<>(testDouble, 124.52), new NodeAttribute<>(testLong, 563L),
                    new NodeAttribute<>(testInt, 2345));

            app.create(projectType, new NodeAttribute<>(testBoolean, true), new NodeAttribute<>(testDouble, 323.22),
                    new NodeAttribute<>(testLong, 22L), new NodeAttribute<>(testInt, 452));

            app.create(projectType, new NodeAttribute<>(testBoolean, false),
                    new NodeAttribute<>(testDouble, 334.32), new NodeAttribute<>(testLong, 5L),
                    new NodeAttribute<>(testInt, 235));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(testBoolean: { _equals: true}) { testBoolean, testDouble, testLong, testInt } }");
            assertMapPathValueIs(result, "Project.#", 2);
            assertMapPathValueIs(result, "Project.0.testBoolean", true);
            assertMapPathValueIs(result, "Project.0.testDouble", 252.52);
            assertMapPathValueIs(result, "Project.0.testLong", 234532.0);
            assertMapPathValueIs(result, "Project.0.testInt", 4563332.0);
            assertMapPathValueIs(result, "Project.1.testBoolean", true);
            assertMapPathValueIs(result, "Project.1.testDouble", 323.22);
            assertMapPathValueIs(result, "Project.1.testLong", 22.0);
            assertMapPathValueIs(result, "Project.1.testInt", 452.0);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(testDouble: { _equals: 334.32}) { testBoolean, testDouble, testLong, testInt } }");
            assertMapPathValueIs(result, "Project.#", 1);
            assertMapPathValueIs(result, "Project.0.testBoolean", false);
            assertMapPathValueIs(result, "Project.0.testDouble", 334.32);
            assertMapPathValueIs(result, "Project.0.testLong", 5.0);
            assertMapPathValueIs(result, "Project.0.testInt", 235.0);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(testLong: { _equals: 22}) { testBoolean, testDouble, testLong, testInt } }");
            assertMapPathValueIs(result, "Project.#", 1);
            assertMapPathValueIs(result, "Project.0.testBoolean", true);
            assertMapPathValueIs(result, "Project.0.testDouble", 323.22);
            assertMapPathValueIs(result, "Project.0.testLong", 22.0);
            assertMapPathValueIs(result, "Project.0.testInt", 452.0);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(testInt: { _equals: 2345}) { testBoolean, testDouble, testLong, testInt } }");
            assertMapPathValueIs(result, "Project.#", 1);
            assertMapPathValueIs(result, "Project.0.testBoolean", false);
            assertMapPathValueIs(result, "Project.0.testDouble", 124.52);
            assertMapPathValueIs(result, "Project.0.testLong", 563.0);
            assertMapPathValueIs(result, "Project.0.testInt", 2345.0);
        }

    }

    @Test
    public void testRemotePropertiesForCorrectInputType() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType task = schema.addType("Task");

            project.relate(task, "HAS", Relation.Cardinality.OneToMany, "project", "tasks");

            task.addBooleanProperty("testBoolean").setIndexed(true);
            task.addLongProperty("testLong").setIndexed(true);
            task.addNumberProperty("testDouble").setIndexed(true);
            task.addIntegerProperty("testInt").setIndexed(true);

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (Throwable fex) {
            fex.printStackTrace();
        }

        try (final Tx tx = app.tx()) {

            final Class projectType = StructrApp.getConfiguration().getNodeEntityClass("Project");
            final Class taskType = StructrApp.getConfiguration().getNodeEntityClass("Task");

            final PropertyKey projectKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(taskType,
                    "project");
            final PropertyKey testBoolean = StructrApp.getConfiguration().getPropertyKeyForJSONName(taskType,
                    "testBoolean");
            final PropertyKey testDouble = StructrApp.getConfiguration().getPropertyKeyForJSONName(taskType,
                    "testDouble");
            final PropertyKey testLong = StructrApp.getConfiguration().getPropertyKeyForJSONName(taskType,
                    "testLong");
            final PropertyKey testInt = StructrApp.getConfiguration().getPropertyKeyForJSONName(taskType,
                    "testInt");

            app.create(taskType, new NodeAttribute<>(testBoolean, true), new NodeAttribute<>(testDouble, 252.52),
                    new NodeAttribute<>(testLong, 234532L), new NodeAttribute<>(testInt, 4563332),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project1")));

            app.create(taskType, new NodeAttribute<>(testBoolean, false), new NodeAttribute<>(testDouble, 124.52),
                    new NodeAttribute<>(testLong, 563L), new NodeAttribute<>(testInt, 2345),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project2")));

            app.create(taskType, new NodeAttribute<>(testBoolean, true), new NodeAttribute<>(testDouble, 323.22),
                    new NodeAttribute<>(testLong, 22L), new NodeAttribute<>(testInt, 452),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project3")));

            app.create(taskType, new NodeAttribute<>(testBoolean, false), new NodeAttribute<>(testDouble, 334.32),
                    new NodeAttribute<>(testLong, 5L), new NodeAttribute<>(testInt, 235),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project4")));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { testBoolean: { _equals: true }}) { name, tasks { testBoolean, testDouble, testLong, testInt } } }");
            assertMapPathValueIs(result, "Project.#", 2);
            assertMapPathValueIs(result, "Project.0.name", "Project1");
            assertMapPathValueIs(result, "Project.0.tasks.0.testBoolean", true);
            assertMapPathValueIs(result, "Project.0.tasks.0.testDouble", 252.52);
            assertMapPathValueIs(result, "Project.0.tasks.0.testLong", 234532.0);
            assertMapPathValueIs(result, "Project.0.tasks.0.testInt", 4563332.0);
            assertMapPathValueIs(result, "Project.1.name", "Project3");
            assertMapPathValueIs(result, "Project.1.tasks.0.testBoolean", true);
            assertMapPathValueIs(result, "Project.1.tasks.0.testDouble", 323.22);
            assertMapPathValueIs(result, "Project.1.tasks.0.testLong", 22.0);
            assertMapPathValueIs(result, "Project.1.tasks.0.testInt", 452.0);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { testDouble: { _equals: 334.32}}) { name, tasks { testBoolean, testDouble, testLong, testInt } } }");
            assertMapPathValueIs(result, "Project.#", 1);
            assertMapPathValueIs(result, "Project.0.name", "Project4");
            assertMapPathValueIs(result, "Project.0.tasks.0.testBoolean", false);
            assertMapPathValueIs(result, "Project.0.tasks.0.testDouble", 334.32);
            assertMapPathValueIs(result, "Project.0.tasks.0.testLong", 5.0);
            assertMapPathValueIs(result, "Project.0.tasks.0.testInt", 235.0);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { testLong: { _equals: 22}}) { name, tasks { testBoolean, testDouble, testLong, testInt } } }");
            assertMapPathValueIs(result, "Project.#", 1);
            assertMapPathValueIs(result, "Project.0.name", "Project3");
            assertMapPathValueIs(result, "Project.0.tasks.0.testBoolean", true);
            assertMapPathValueIs(result, "Project.0.tasks.0.testDouble", 323.22);
            assertMapPathValueIs(result, "Project.0.tasks.0.testLong", 22.0);
            assertMapPathValueIs(result, "Project.0.tasks.0.testInt", 452.0);
        }

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(tasks: { testInt: { _equals: 2345}}) { name, tasks { testBoolean, testDouble, testLong, testInt } } }");
            assertMapPathValueIs(result, "Project.#", 1);
            assertMapPathValueIs(result, "Project.0.name", "Project2");
            assertMapPathValueIs(result, "Project.0.tasks.0.testBoolean", false);
            assertMapPathValueIs(result, "Project.0.tasks.0.testDouble", 124.52);
            assertMapPathValueIs(result, "Project.0.tasks.0.testLong", 563.0);
            assertMapPathValueIs(result, "Project.0.tasks.0.testInt", 2345.0);
        }

    }

    @Test
    public void testRemotePropertiesWithMultipleInstances() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType task = schema.addType("Task");

            project.relate(task, "HAS", Relation.Cardinality.OneToOne, "project", "task");

            task.addBooleanProperty("testBoolean").setIndexed(true);

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (Throwable fex) {
            fex.printStackTrace();
        }

        try (final Tx tx = app.tx()) {

            final Class projectType = StructrApp.getConfiguration().getNodeEntityClass("Project");
            final Class taskType = StructrApp.getConfiguration().getNodeEntityClass("Task");

            final PropertyKey projectKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(taskType,
                    "project");
            final PropertyKey testBoolean = StructrApp.getConfiguration().getPropertyKeyForJSONName(taskType,
                    "testBoolean");

            app.create(taskType, new NodeAttribute<>(testBoolean, true),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project1")));

            app.create(taskType, new NodeAttribute<>(testBoolean, false),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project2")));

            app.create(taskType, new NodeAttribute<>(testBoolean, true),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project3")));

            app.create(taskType, new NodeAttribute<>(testBoolean, false),
                    new NodeAttribute<>(projectKey, app.create(projectType, "Project4")));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Project(task: { testBoolean: { _equals: true }}) { name, task { testBoolean } } }");
            assertMapPathValueIs(result, "Project.#", 2);
            assertMapPathValueIs(result, "Project.0.name", "Project1");
            assertMapPathValueIs(result, "Project.0.task.testBoolean", true);
            assertMapPathValueIs(result, "Project.1.name", "Project3");
            assertMapPathValueIs(result, "Project.1.task.testBoolean", true);
        }
    }

    @Test
    public void testCombinedFilteringOnMultipleProperties() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType identifier = schema.addType("Identifier");

            project.relate(identifier, "HAS", Relation.Cardinality.OneToOne, "project", "identifier");

            identifier.addStringProperty("test1").setIndexed(true);
            identifier.addStringProperty("test2").setIndexed(true);
            identifier.addStringProperty("test3").setIndexed(true);

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        final List<NodeInterface> identifiers = new LinkedList<>();
        final Class project = StructrApp.getConfiguration().getNodeEntityClass("Project");
        final Class identifier = StructrApp.getConfiguration().getNodeEntityClass("Identifier");
        final PropertyKey projectNameKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(project, "name");
        final PropertyKey identifierKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(project,
                "identifier");
        final PropertyKey test1Key = StructrApp.getConfiguration().getPropertyKeyForJSONName(identifier, "test1");
        final PropertyKey test2Key = StructrApp.getConfiguration().getPropertyKeyForJSONName(identifier, "test2");
        final PropertyKey test3Key = StructrApp.getConfiguration().getPropertyKeyForJSONName(identifier, "test3");

        try (final Tx tx = app.tx()) {

            identifiers.add(app.create(identifier, new NodeAttribute<>(test1Key, "aaa"),
                    new NodeAttribute<>(test2Key, "bbb"), new NodeAttribute<>(test3Key, "ddd")));
            identifiers.add(app.create(identifier, new NodeAttribute<>(test1Key, "aaa"),
                    new NodeAttribute<>(test2Key, "bbb"), new NodeAttribute<>(test3Key, "eee")));
            identifiers.add(app.create(identifier, new NodeAttribute<>(test1Key, "aaa"),
                    new NodeAttribute<>(test2Key, "ccc"), new NodeAttribute<>(test3Key, "fff")));
            identifiers.add(app.create(identifier, new NodeAttribute<>(test1Key, "aaa"),
                    new NodeAttribute<>(test2Key, "ccc"), new NodeAttribute<>(test3Key, "ggg")));
            identifiers.add(app.create(identifier, new NodeAttribute<>(test1Key, "zzz"),
                    new NodeAttribute<>(test2Key, "zzz"), new NodeAttribute<>(test3Key, "zzz")));

            app.create(project, new NodeAttribute<>(projectNameKey, "project1"),
                    new NodeAttribute<>(identifierKey, identifiers.get(0)));
            app.create(project, new NodeAttribute<>(projectNameKey, "project2"),
                    new NodeAttribute<>(identifierKey, identifiers.get(1)));
            app.create(project, new NodeAttribute<>(projectNameKey, "project3"),
                    new NodeAttribute<>(identifierKey, identifiers.get(2)));
            app.create(project, new NodeAttribute<>(projectNameKey, "project4"),
                    new NodeAttribute<>(identifierKey, identifiers.get(3)));
            app.create(project, new NodeAttribute<>(projectNameKey, "project5"),
                    new NodeAttribute<>(identifierKey, identifiers.get(4)));
            app.create(project, new NodeAttribute<>(projectNameKey, "project6"));
            app.create(project, new NodeAttribute<>(projectNameKey, "project7"));
            app.create(project, new NodeAttribute<>(projectNameKey, "project8"));

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        final String body = "{ name, identifier { test1, test2, test3 }}";

        // check _equals
        assertCount("{ Project " + body + "}", "Project.#", 8);
        assertCount("{ Project(identifier: { test1: { _equals: \"aaa\" }}) " + body + "}", "Project.#", 4);
        assertCount("{ Project(identifier: { test1: { _equals: \"xxx\" }}) " + body + "}", "Project.#", 0);

        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }, test2: { _equals: \"bbb\" }}) " + body + "}",
                "Project.#", 2);
        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }, test2: { _equals: \"ddd\" }}) " + body + "}",
                "Project.#", 0);
        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }}, identifier: { test2: { _equals: \"bbb\" }}) "
                        + body + "}",
                "Project.#", 2);
        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }}, identifier: { test2: { _equals: \"ddd\" }}) "
                        + body + "}",
                "Project.#", 0);

        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }, test2: { _equals: \"bbb\" }, test3: { _equals: \"eee\" }}) "
                        + body + "}",
                "Project.#", 1);
        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }, test2: { _equals: \"bbb\" }, test3: { _equals: \"fff\" }}) "
                        + body + "}",
                "Project.#", 0);
        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }}, identifier: { test2: { _equals: \"bbb\" }}, identifier: {  test3: { _equals: \"eee\" }}) "
                        + body + "}",
                "Project.#", 1);
        assertCount(
                "{ Project(identifier: { test1: { _equals: \"aaa\" }}, identifier: { test2: { _equals: \"bbb\" }}, identifier: {  test3: { _equals: \"fff\" }}) "
                        + body + "}",
                "Project.#", 0);

        // same for _contains
        assertCount("{ Project(identifier: { test1: { _contains: \"a\" }}) " + body + "}", "Project.#", 4);

        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }, test2: { _contains: \"b\" }}) " + body + "}",
                "Project.#", 2);
        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }, test2: { _contains: \"d\" }}) " + body + "}",
                "Project.#", 0);
        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }}, identifier: { test2: { _contains: \"b\" }}) "
                        + body + "}",
                "Project.#", 2);
        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }}, identifier: { test2: { _contains: \"d\" }}) "
                        + body + "}",
                "Project.#", 0);
        assertCount(
                "{ Project(_page: 1, _pageSize: 100, _sort: \"name\", _desc: false, identifier: { test1: { _contains: \"a\" }, test2: { _contains: \"b\" }}) "
                        + body + "}",
                "Project.#", 2);

        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }, test2: { _contains: \"b\" }, test3: { _contains: \"e\" }}) "
                        + body + "}",
                "Project.#", 1);
        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }, test2: { _contains: \"b\" }, test3: { _contains: \"f\" }}) "
                        + body + "}",
                "Project.#", 0);
        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }}, identifier: { test2: { _contains: \"b\" }}, identifier: {  test3: { _contains: \"e\" }}) "
                        + body + "}",
                "Project.#", 1);
        assertCount(
                "{ Project(identifier: { test1: { _contains: \"a\" }}, identifier: { test2: { _contains: \"b\" }}, identifier: {  test3: { _contains: \"f\" }}) "
                        + body + "}",
                "Project.#", 0);
    }

    @Test
    public void testCombinedFilteringOnMultiplePropertiesAndCardinalities() {

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType root = schema.addType("Root");
            final JsonObjectType oneToOne = schema.addType("OneToOneTest");
            final JsonObjectType oneToMany = schema.addType("OneToManyTest");
            final JsonObjectType manyToOne = schema.addType("ManyToOneTest");
            final JsonObjectType manyToMany = schema.addType("ManyToManyTest");

            root.relate(oneToOne, "oneToOne", Relation.Cardinality.OneToOne, "rootOneToOne", "oneToOne");
            root.relate(oneToMany, "oneToMany", Relation.Cardinality.OneToMany, "rootOneToMany", "oneToMany");
            root.relate(manyToOne, "manyToOne", Relation.Cardinality.ManyToOne, "rootManyToOne", "manyToOne");
            root.relate(manyToMany, "manyToMany", Relation.Cardinality.ManyToMany, "rootManyToMany", "manyToMany");

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        final Class root = StructrApp.getConfiguration().getNodeEntityClass("Root");
        final Class oneToOne = StructrApp.getConfiguration().getNodeEntityClass("OneToOneTest");
        final Class oneToMany = StructrApp.getConfiguration().getNodeEntityClass("OneToManyTest");
        final Class manyToOne = StructrApp.getConfiguration().getNodeEntityClass("ManyToOneTest");
        final Class manyToMany = StructrApp.getConfiguration().getNodeEntityClass("ManyToManyTest");
        final PropertyKey nameKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(root, "name");
        final PropertyKey oneToOneKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(root, "oneToOne");
        final PropertyKey oneToManyKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(root, "oneToMany");
        final PropertyKey manyToOneKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(root, "manyToOne");
        final PropertyKey manyToManyKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(root,
                "manyToMany");

        try (final Tx tx = app.tx()) {

            final NodeInterface oneToOne0 = app.create(oneToOne, "oneToOne0");
            final NodeInterface oneToOne1 = app.create(oneToOne, "oneToOne1");
            final NodeInterface oneToOne2 = app.create(oneToOne, "oneToOne2");
            final NodeInterface oneToOne3 = app.create(oneToOne, "oneToOne3");
            final NodeInterface oneToOne4 = app.create(oneToOne, "oneToOne4");
            final NodeInterface oneToOne5 = app.create(oneToOne, "oneToOne5");
            final NodeInterface oneToOne6 = app.create(oneToOne, "oneToOne6");
            final NodeInterface oneToOne7 = app.create(oneToOne, "oneToOne7");

            final NodeInterface oneToMany00 = app.create(oneToMany, "oneToMany00");
            final NodeInterface oneToMany01 = app.create(oneToMany, "oneToMany01");
            final NodeInterface oneToMany02 = app.create(oneToMany, "oneToMany02");
            final NodeInterface oneToMany03 = app.create(oneToMany, "oneToMany03");
            final NodeInterface oneToMany04 = app.create(oneToMany, "oneToMany04");
            final NodeInterface oneToMany05 = app.create(oneToMany, "oneToMany05");
            final NodeInterface oneToMany06 = app.create(oneToMany, "oneToMany06");
            final NodeInterface oneToMany07 = app.create(oneToMany, "oneToMany07");
            final NodeInterface oneToMany08 = app.create(oneToMany, "oneToMany08");
            final NodeInterface oneToMany09 = app.create(oneToMany, "oneToMany09");
            final NodeInterface oneToMany10 = app.create(oneToMany, "oneToMany10");
            final NodeInterface oneToMany11 = app.create(oneToMany, "oneToMany11");
            final NodeInterface oneToMany12 = app.create(oneToMany, "oneToMany12");
            final NodeInterface oneToMany13 = app.create(oneToMany, "oneToMany13");
            final NodeInterface oneToMany14 = app.create(oneToMany, "oneToMany14");
            final NodeInterface oneToMany15 = app.create(oneToMany, "oneToMany15");

            final NodeInterface manyToOne0 = app.create(manyToOne, "manyToOne0");
            final NodeInterface manyToOne1 = app.create(manyToOne, "manyToOne1");
            final NodeInterface manyToOne2 = app.create(manyToOne, "manyToOne2");
            final NodeInterface manyToOne3 = app.create(manyToOne, "manyToOne3");
            final NodeInterface manyToOne4 = app.create(manyToOne, "manyToOne4");
            final NodeInterface manyToOne5 = app.create(manyToOne, "manyToOne5");
            final NodeInterface manyToOne6 = app.create(manyToOne, "manyToOne6");
            final NodeInterface manyToOne7 = app.create(manyToOne, "manyToOne7");
            final NodeInterface manyToOne8 = app.create(manyToOne, "manyToOne8");
            final NodeInterface manyToOne9 = app.create(manyToOne, "manyToOne9");

            final NodeInterface manyToMany0 = app.create(manyToMany, "manyToMany0");
            final NodeInterface manyToMany1 = app.create(manyToMany, "manyToMany1");
            final NodeInterface manyToMany2 = app.create(manyToMany, "manyToMany2");
            final NodeInterface manyToMany3 = app.create(manyToMany, "manyToMany3");
            final NodeInterface manyToMany4 = app.create(manyToMany, "manyToMany4");
            final NodeInterface manyToMany5 = app.create(manyToMany, "manyToMany5");
            final NodeInterface manyToMany6 = app.create(manyToMany, "manyToMany6");
            final NodeInterface manyToMany7 = app.create(manyToMany, "manyToMany7");
            final NodeInterface manyToMany8 = app.create(manyToMany, "manyToMany8");

            final KeyData keys = new KeyData(nameKey, oneToOneKey, oneToManyKey, manyToOneKey, manyToManyKey);

            createTestData(app, root, "root00", keys, null, null, null, null); // 0000
            createTestData(app, root, "root01", keys, oneToOne0, null, null, null); // 1000
            createTestData(app, root, "root02", keys, null, Arrays.asList(oneToMany00, oneToMany08), null, null); // 0100
            createTestData(app, root, "root03", keys, oneToOne1, Arrays.asList(oneToMany01, oneToMany09), null,
                    null); // 1100
            createTestData(app, root, "root04", keys, null, null, manyToOne0, null); // 0010
            createTestData(app, root, "root05", keys, null, null, manyToOne8, null); // 0010
            createTestData(app, root, "root06", keys, oneToOne2, null, manyToOne1, null); // 1010
            createTestData(app, root, "root07", keys, null, Arrays.asList(oneToMany02, oneToMany10), manyToOne2,
                    null); // 0110
            createTestData(app, root, "root08", keys, oneToOne3, Arrays.asList(oneToMany03, oneToMany11),
                    manyToOne3, null); // 1110

            createTestData(app, root, "root09", keys, null, null, null, Arrays.asList(manyToMany0, manyToMany1)); // 0001
            createTestData(app, root, "root10", keys, oneToOne4, null, null,
                    Arrays.asList(manyToMany1, manyToMany2)); // 1001
            createTestData(app, root, "root11", keys, null, Arrays.asList(oneToMany04, oneToMany12), null,
                    Arrays.asList(manyToMany2, manyToMany3)); // 0101
            createTestData(app, root, "root12", keys, oneToOne5, Arrays.asList(oneToMany05, oneToMany13), null,
                    Arrays.asList(manyToMany3, manyToMany4)); // 1101
            createTestData(app, root, "root13", keys, null, null, manyToOne4,
                    Arrays.asList(manyToMany4, manyToMany5)); // 0011
            createTestData(app, root, "root14", keys, null, null, manyToOne9,
                    Arrays.asList(manyToMany4, manyToMany5)); // 0011
            createTestData(app, root, "root15", keys, oneToOne6, null, manyToOne5,
                    Arrays.asList(manyToMany5, manyToMany6)); // 1011
            createTestData(app, root, "root16", keys, null, Arrays.asList(oneToMany06, oneToMany14), manyToOne6,
                    Arrays.asList(manyToMany6, manyToMany7)); // 0111
            createTestData(app, root, "root17", keys, oneToOne7, Arrays.asList(oneToMany07, oneToMany15),
                    manyToOne7, Arrays.asList(manyToMany7, manyToMany8)); // 1111

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        final String body = "{ name, oneToOne { name }, oneToMany { name }, manyToOne { name }, manyToMany { name } }";

        // test results for _equals
        assertCount("{ Root" + body + "}", "Root.#", 18);
        assertCount("{ Root(oneToOne:   " + eq("error") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("error") + ") " + body + "}",
                "Root.#", 0);
        assertCount(
                "{ Root(                                    oneToMany: " + eq("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + eq("oneToMany08") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne1") + ",oneToMany: " + eq("oneToMany01") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("error") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne2") + ",manyToOne: " + eq("manyToOne1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany02") + ",manyToOne: "
                + eq("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne3") + ",oneToMany: " + eq("oneToMany03") + ",manyToOne: "
                + eq("manyToOne3") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + eq("error") + ") " + body + "}",
                "Root.#", 0);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + eq("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne4") + ",manyToMany: " + eq("manyToMany1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany04") + ",manyToMany: "
                + eq("manyToMany2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne5") + ",oneToMany: " + eq("oneToMany05") + ",manyToMany: "
                + eq("manyToMany3") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne4") + ",manyToMany: " + eq("manyToMany4") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne6") + ",manyToOne: " + eq("manyToOne5") + ",manyToMany: "
                + eq("manyToMany5") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany06") + ",manyToOne: "
                + eq("manyToOne6") + ",manyToMany: " + eq("manyToMany6") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne7") + ",oneToMany: " + eq("oneToMany07") + ",manyToOne: "
                + eq("manyToOne7") + ",manyToMany: " + eq("manyToMany7") + ") " + body + "}", "Root.#", 1);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + eq("oneToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + eq("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne3") + ",oneToMany: " + eq("oneToMany01") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne4") + ",manyToOne: " + eq("manyToOne1") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany02") + ",manyToOne: "
                + eq("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne5") + ",oneToMany: " + eq("oneToMany03") + ",manyToOne: "
                + eq("manyToOne3") + ") " + body + "}", "Root.#", 0);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + eq("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne6") + ",manyToMany: " + eq("manyToMany1") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany04") + ",manyToMany: "
                + eq("manyToMany2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne7") + ",oneToMany: " + eq("oneToMany05") + ",manyToMany: "
                + eq("manyToMany3") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne4") + ",manyToMany: " + eq("manyToMany4") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne0") + ",manyToOne: " + eq("manyToOne5") + ",manyToMany: "
                + eq("manyToMany5") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany06") + ",manyToOne: "
                + eq("manyToOne6") + ",manyToMany: " + eq("manyToMany6") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne1") + ",oneToMany: " + eq("oneToMany07") + ",manyToOne: "
                + eq("manyToOne7") + ",manyToMany: " + eq("manyToMany7") + ") " + body + "}", "Root.#", 0);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + eq("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + eq("oneToMany02") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne1") + ",oneToMany: " + eq("oneToMany03") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne2") + ",manyToOne: " + eq("manyToOne1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany04") + ",manyToOne: "
                + eq("manyToOne2") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne3") + ",oneToMany: " + eq("oneToMany05") + ",manyToOne: "
                + eq("manyToOne3") + ") " + body + "}", "Root.#", 0);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + eq("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne4") + ",manyToMany: " + eq("manyToMany1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany06") + ",manyToMany: "
                + eq("manyToMany2") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne5") + ",oneToMany: " + eq("oneToMany07") + ",manyToMany: "
                + eq("manyToMany3") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne4") + ",manyToMany: " + eq("manyToMany4") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne6") + ",manyToOne: " + eq("manyToOne5") + ",manyToMany: "
                + eq("manyToMany5") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany00") + ",manyToOne: "
                + eq("manyToOne6") + ",manyToMany: " + eq("manyToMany6") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne7") + ",oneToMany: " + eq("oneToMany01") + ",manyToOne: "
                + eq("manyToOne7") + ",manyToMany: " + eq("manyToMany7") + ") " + body + "}", "Root.#", 0);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + eq("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + eq("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne1") + ",oneToMany: " + eq("oneToMany01") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne2") + ",manyToOne: " + eq("manyToOne3") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany02") + ",manyToOne: "
                + eq("manyToOne4") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne3") + ",oneToMany: " + eq("oneToMany03") + ",manyToOne: "
                + eq("manyToOne5") + ") " + body + "}", "Root.#", 0);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + eq("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne4") + ",manyToMany: " + eq("manyToMany1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany04") + ",manyToMany: "
                + eq("manyToMany2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne5") + ",oneToMany: " + eq("oneToMany05") + ",manyToMany: "
                + eq("manyToMany3") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne6") + ",manyToMany: " + eq("manyToMany4") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne6") + ",manyToOne: " + eq("manyToOne7") + ",manyToMany: "
                + eq("manyToMany5") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany06") + ",manyToOne: "
                + eq("manyToOne0") + ",manyToMany: " + eq("manyToMany6") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne7") + ",oneToMany: " + eq("oneToMany07") + ",manyToOne: "
                + eq("manyToOne1") + ",manyToMany: " + eq("manyToMany7") + ") " + body + "}", "Root.#", 0);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + eq("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + eq("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne1") + ",oneToMany: " + eq("oneToMany01") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne2") + ",manyToOne: " + eq("manyToOne1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany02") + ",manyToOne: "
                + eq("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne3") + ",oneToMany: " + eq("oneToMany03") + ",manyToOne: "
                + eq("manyToOne3") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + eq("manyToMany2") + ") " + body + "}",
                "Root.#", 2);
        assertCount(
                "{ Root(oneToOne:   " + eq("oneToOne4") + ",manyToMany: " + eq("manyToMany3") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany04") + ",manyToMany: "
                + eq("manyToMany4") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne5") + ",oneToMany: " + eq("oneToMany05") + ",manyToMany: "
                + eq("manyToMany5") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + eq("manyToOne4") + ",manyToMany: " + eq("manyToMany6") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne6") + ",manyToOne: " + eq("manyToOne5") + ",manyToMany: "
                + eq("manyToMany7") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + eq("oneToMany06") + ",manyToOne: "
                + eq("manyToOne6") + ",manyToMany: " + eq("manyToMany0") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + eq("oneToOne7") + ",oneToMany: " + eq("oneToMany07") + ",manyToOne: "
                + eq("manyToOne7") + ",manyToMany: " + eq("manyToMany1") + ") " + body + "}", "Root.#", 0);

        // test results for _contains
        assertCount("{ Root" + body + "}", "Root.#", 18);
        assertCount("{ Root(oneToOne:   " + ct("error") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("o") + ") " + body + "}", "Root.#", 8);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + ct("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne1") + ",oneToMany: " + ct("oneToMany01") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne2") + ",manyToOne: " + ct("manyToOne1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany02") + ",manyToOne: "
                + ct("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne3") + ",oneToMany: " + ct("oneToMany03") + ",manyToOne: "
                + ct("manyToOne3") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + ct("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne4") + ",manyToMany: " + ct("manyToMany1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany04") + ",manyToMany: "
                + ct("manyToMany2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne5") + ",oneToMany: " + ct("oneToMany05") + ",manyToMany: "
                + ct("manyToMany3") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne4") + ",manyToMany: " + ct("manyToMany4") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne6") + ",manyToOne: " + ct("manyToOne5") + ",manyToMany: "
                + ct("manyToMany5") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany06") + ",manyToOne: "
                + ct("manyToOne6") + ",manyToMany: " + ct("manyToMany6") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne7") + ",oneToMany: " + ct("oneToMany07") + ",manyToOne: "
                + ct("manyToOne7") + ",manyToMany: " + ct("manyToMany7") + ") " + body + "}", "Root.#", 1);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + ct("oneToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + ct("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne3") + ",oneToMany: " + ct("oneToMany01") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne4") + ",manyToOne: " + ct("manyToOne1") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany02") + ",manyToOne: "
                + ct("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne5") + ",oneToMany: " + ct("oneToMany03") + ",manyToOne: "
                + ct("manyToOne3") + ") " + body + "}", "Root.#", 0);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + ct("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne6") + ",manyToMany: " + ct("manyToMany1") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany04") + ",manyToMany: "
                + ct("manyToMany2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne7") + ",oneToMany: " + ct("oneToMany05") + ",manyToMany: "
                + ct("manyToMany3") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne4") + ",manyToMany: " + ct("manyToMany4") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne0") + ",manyToOne: " + ct("manyToOne5") + ",manyToMany: "
                + ct("manyToMany5") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany06") + ",manyToOne: "
                + ct("manyToOne6") + ",manyToMany: " + ct("manyToMany6") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne1") + ",oneToMany: " + ct("oneToMany07") + ",manyToOne: "
                + ct("manyToOne7") + ",manyToMany: " + ct("manyToMany7") + ") " + body + "}", "Root.#", 0);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + ct("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + ct("oneToMany02") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne1") + ",oneToMany: " + ct("oneToMany03") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne2") + ",manyToOne: " + ct("manyToOne1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany04") + ",manyToOne: "
                + ct("manyToOne2") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne3") + ",oneToMany: " + ct("oneToMany05") + ",manyToOne: "
                + ct("manyToOne3") + ") " + body + "}", "Root.#", 0);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + ct("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne4") + ",manyToMany: " + ct("manyToMany1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany06") + ",manyToMany: "
                + ct("manyToMany2") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne5") + ",oneToMany: " + ct("oneToMany07") + ",manyToMany: "
                + ct("manyToMany3") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne4") + ",manyToMany: " + ct("manyToMany4") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne6") + ",manyToOne: " + ct("manyToOne5") + ",manyToMany: "
                + ct("manyToMany5") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany00") + ",manyToOne: "
                + ct("manyToOne6") + ",manyToMany: " + ct("manyToMany6") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne7") + ",oneToMany: " + ct("oneToMany01") + ",manyToOne: "
                + ct("manyToOne7") + ",manyToMany: " + ct("manyToMany7") + ") " + body + "}", "Root.#", 0);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + ct("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + ct("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne1") + ",oneToMany: " + ct("oneToMany01") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne2") + ",manyToOne: " + ct("manyToOne3") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany02") + ",manyToOne: "
                + ct("manyToOne4") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne3") + ",oneToMany: " + ct("oneToMany03") + ",manyToOne: "
                + ct("manyToOne5") + ") " + body + "}", "Root.#", 0);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + ct("manyToMany0") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne4") + ",manyToMany: " + ct("manyToMany1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany04") + ",manyToMany: "
                + ct("manyToMany2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne5") + ",oneToMany: " + ct("oneToMany05") + ",manyToMany: "
                + ct("manyToMany3") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne6") + ",manyToMany: " + ct("manyToMany4") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne6") + ",manyToOne: " + ct("manyToOne7") + ",manyToMany: "
                + ct("manyToMany5") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany06") + ",manyToOne: "
                + ct("manyToOne0") + ",manyToMany: " + ct("manyToMany6") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne7") + ",oneToMany: " + ct("oneToMany07") + ",manyToOne: "
                + ct("manyToOne1") + ",manyToMany: " + ct("manyToMany7") + ") " + body + "}", "Root.#", 0);

        // test wrong results
        assertCount("{ Root(oneToOne:   " + ct("oneToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                    oneToMany: " + ct("oneToMany00") + ") " + body + "}",
                "Root.#", 1);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne1") + ",oneToMany: " + ct("oneToMany01") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne0") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne2") + ",manyToOne: " + ct("manyToOne1") + ") " + body + "}",
                "Root.#", 1);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany02") + ",manyToOne: "
                + ct("manyToOne2") + ") " + body + "}", "Root.#", 1);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne3") + ",oneToMany: " + ct("oneToMany03") + ",manyToOne: "
                + ct("manyToOne3") + ") " + body + "}", "Root.#", 1);
        assertCount(
                "{ Root(                                                                                                             manyToMany: "
                        + ct("manyToMany2") + ") " + body + "}",
                "Root.#", 2);
        assertCount(
                "{ Root(oneToOne:   " + ct("oneToOne4") + ",manyToMany: " + ct("manyToMany3") + ") " + body + "}",
                "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany04") + ",manyToMany: "
                + ct("manyToMany4") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne5") + ",oneToMany: " + ct("oneToMany05") + ",manyToMany: "
                + ct("manyToMany5") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                                                         manyToOne: "
                + ct("manyToOne4") + ",manyToMany: " + ct("manyToMany6") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne6") + ",manyToOne: " + ct("manyToOne5") + ",manyToMany: "
                + ct("manyToMany7") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(                                    oneToMany: " + ct("oneToMany06") + ",manyToOne: "
                + ct("manyToOne6") + ",manyToMany: " + ct("manyToMany0") + ") " + body + "}", "Root.#", 0);
        assertCount("{ Root(oneToOne:   " + ct("oneToOne7") + ",oneToMany: " + ct("oneToMany07") + ",manyToOne: "
                + ct("manyToOne7") + ",manyToMany: " + ct("manyToMany1") + ") " + body + "}", "Root.#", 0);

    }

    @Test
    public void testFunctionPropertyIndexing() {

        Settings.LogSchemaOutput.setValue(true);

        try (final Tx tx = app.tx()) {

            JsonSchema schema = StructrSchema.createFromDatabase(app);

            final JsonObjectType project = schema.addType("Project");
            final JsonObjectType task = schema.addType("Task");

            project.relate(task, "TASK", Relation.Cardinality.OneToMany, "project", "tasks");

            // add function property that extracts the project ID
            final JsonFunctionProperty projectId = task.addFunctionProperty("projectId");

            projectId.setIndexed(true);
            projectId.setReadFunction("this.project.id");
            projectId.setTypeHint("String");

            StructrSchema.extendDatabaseSchema(app, schema);

            tx.success();

        } catch (URISyntaxException | FrameworkException fex) {
            fex.printStackTrace();
        }

        final Class project = StructrApp.getConfiguration().getNodeEntityClass("Project");
        final Class task = StructrApp.getConfiguration().getNodeEntityClass("Task");
        final PropertyKey projectKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(task, "project");
        final PropertyKey tasksKey = StructrApp.getConfiguration().getPropertyKeyForJSONName(project, "tasks");

        String project1Id = null;
        String project2Id = null;

        try (final Tx tx = app.tx()) {

            final List<NodeInterface> tasks1 = new LinkedList<>();
            final List<NodeInterface> tasks2 = new LinkedList<>();

            final NodeInterface project1 = app.create(project, "Project1");
            final NodeInterface project2 = app.create(project, "Project2");

            project1Id = project1.getUuid();
            project2Id = project2.getUuid();

            tasks1.add(app.create(task, "Task1.1"));
            tasks1.add(app.create(task, "Task1.2"));
            tasks1.add(app.create(task, "Task1.3"));
            tasks1.add(app.create(task, "Task1.4"));
            tasks1.add(app.create(task, "Task1.5"));
            tasks1.add(app.create(task, "Task1.6"));
            tasks1.add(app.create(task, "Task1.7"));

            tasks2.add(app.create(task, "Task2.1"));
            tasks2.add(app.create(task, "Task2.2"));
            tasks2.add(app.create(task, "Task2.3"));
            tasks2.add(app.create(task, "Task2.4"));
            tasks2.add(app.create(task, "Task2.5"));
            tasks2.add(app.create(task, "Task2.6"));
            tasks2.add(app.create(task, "Task2.7"));

            project1.setProperty(tasksKey, tasks1);
            project2.setProperty(tasksKey, tasks2);

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        RestAssured.basePath = "/structr/graphql";

        // verify that all tasks are found
        assertCount("{ Task { id }}", "Task.#", 14);

        // verify that all tasks with a given project ID are found
        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task( projectId: { _equals: \"" + project1Id + "\"}) { name }}");
            assertMapPathValueIs(result, "Task.#", 7);
            assertMapPathValueIs(result, "Task.0.name", "Task1.1");
            assertMapPathValueIs(result, "Task.1.name", "Task1.2");
            assertMapPathValueIs(result, "Task.2.name", "Task1.3");
            assertMapPathValueIs(result, "Task.3.name", "Task1.4");
            assertMapPathValueIs(result, "Task.4.name", "Task1.5");
            assertMapPathValueIs(result, "Task.5.name", "Task1.6");
            assertMapPathValueIs(result, "Task.6.name", "Task1.7");
        }

        // verify that all tasks with a given project ID are found
        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task( projectId: { _equals: \"" + project2Id + "\"}) { name }}");
            assertMapPathValueIs(result, "Task.#", 7);
            assertMapPathValueIs(result, "Task.0.name", "Task2.1");
            assertMapPathValueIs(result, "Task.1.name", "Task2.2");
            assertMapPathValueIs(result, "Task.2.name", "Task2.3");
            assertMapPathValueIs(result, "Task.3.name", "Task2.4");
            assertMapPathValueIs(result, "Task.4.name", "Task2.5");
            assertMapPathValueIs(result, "Task.5.name", "Task2.6");
            assertMapPathValueIs(result, "Task.6.name", "Task2.7");
        }

        // modify data, remove some tasks from projects
        try (final Tx tx = app.tx()) {

            app.nodeQuery(task).andName("Task1.3").getFirst().setProperty(projectKey, null);
            app.nodeQuery(task).andName("Task1.4").getFirst().setProperty(projectKey, null);
            app.nodeQuery(task).andName("Task1.5").getFirst().setProperty(projectKey, null);

            app.nodeQuery(task).andName("Task2.1").getFirst().setProperty(projectKey, null);
            app.nodeQuery(task).andName("Task2.2").getFirst().setProperty(projectKey, null);
            app.nodeQuery(task).andName("Task2.3").getFirst().setProperty(projectKey, null);
            app.nodeQuery(task).andName("Task2.7").getFirst().setProperty(projectKey, null);

            tx.success();

        } catch (FrameworkException fex) {
            fex.printStackTrace();
        }

        // verify that all tasks are found
        assertCount("{ Task { id }}", "Task.#", 14);

        // verify that only tasks with the given project ID are found
        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task( projectId: { _equals: \"" + project1Id + "\"}) { name }}");
            assertMapPathValueIs(result, "Task.#", 4);
            assertMapPathValueIs(result, "Task.0.name", "Task1.1");
            assertMapPathValueIs(result, "Task.1.name", "Task1.2");
            assertMapPathValueIs(result, "Task.2.name", "Task1.6");
            assertMapPathValueIs(result, "Task.3.name", "Task1.7");
        }

        // verify that only tasks with the given project ID are found
        {
            final Map<String, Object> result = fetchGraphQL(
                    "{ Task( projectId: { _equals: \"" + project2Id + "\"}) { name }}");
            assertMapPathValueIs(result, "Task.#", 3);
            assertMapPathValueIs(result, "Task.0.name", "Task2.4");
            assertMapPathValueIs(result, "Task.1.name", "Task2.5");
            assertMapPathValueIs(result, "Task.2.name", "Task2.6");
        }
    }

    // ----- private methods -----
    private String eq(final String value) {
        return "{ name: { _equals: \"" + value + "\" }}";
    }

    private String ct(final String value) {
        return "{ name: { _contains: \"" + value + "\" }}";
    }

    private void createTestData(final App app, final Class type, final String name, final KeyData keys,
            final Object... data) throws FrameworkException {

        final PropertyMap map = new PropertyMap();

        map.put(keys.name, name);

        switch (data.length) {

        case 4:
            if (data[3] != null) {
                map.put(keys.t3, data[3]);
            }
        case 3:
            if (data[2] != null) {
                map.put(keys.t2, data[2]);
            }
        case 2:
            if (data[1] != null) {
                map.put(keys.t1, data[1]);
            }
        case 1:
            if (data[0] != null) {
                map.put(keys.t0, data[0]);
            }
        }

        app.create(type, map);
    }

    private void assertCount(final String query, final String path, final int count) {
        assertMapPathValueIs(fetchGraphQL(query), path, count);
    }

    private Map<String, Object> fetchGraphQL(final String query) {

        return RestAssured.given().contentType("application/json; charset=UTF-8")
                .filter(ResponseLoggingFilter.logResponseTo(System.out)).body(query)

                .expect().statusCode(200)

                .when().post("/")

                .andReturn().as(Map.class);
    }

    private void assertMapPathValueIs(final Map<String, Object> map, final String mapPath, final Object value) {

        final String[] parts = mapPath.split("[\\.]+");
        Object current = map;

        for (int i = 0; i < parts.length; i++) {

            final String part = parts[i];
            if (StringUtils.isNumeric(part)) {

                int index = Integer.valueOf(part);
                if (current instanceof List) {

                    final List list = (List) current;
                    if (index >= list.size()) {

                        // value for nonexisting fields must be null
                        assertEquals("Invalid map path result for " + mapPath, value, null);

                        // nothing more to check here
                        return;

                    } else {

                        current = list.get(index);
                    }
                }

            } else if ("#".equals(part) && current instanceof List) {

                assertEquals("Invalid collection size for " + mapPath, value, ((List) current).size());

                // nothing more to check here
                return;

            } else {

                if (current instanceof Map) {

                    current = ((Map) current).get(part);
                }
            }
        }

        // ignore type of value if numerical (GSON defaults to double...)
        if (value instanceof Number && current instanceof Number) {

            assertEquals("Invalid map path result for " + mapPath, ((Number) value).doubleValue(),
                    ((Number) current).doubleValue(), 0.0);

        } else {

            assertEquals("Invalid map path result for " + mapPath, value, current);
        }
    }

    // ----- nested classes -----
    private static class KeyData {

        public PropertyKey name = null;
        public PropertyKey t0 = null;
        public PropertyKey t1 = null;
        public PropertyKey t2 = null;
        public PropertyKey t3 = null;

        public KeyData(final PropertyKey name, final PropertyKey t0, final PropertyKey t1, final PropertyKey t2,
                final PropertyKey t3) {

            this.name = name;
            this.t0 = t0;
            this.t1 = t1;
            this.t2 = t2;
            this.t3 = t3;
        }
    }
}