diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index b6b760811e6..4e54cd78c14 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -474,10 +474,10 @@ private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, Fi $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm); + $queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm)); } if ($filter->propertyValue !== null) { - $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); + $queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue)); } $this->addSubtreeTagConstraints($queryBuilder); return $queryBuilder; @@ -505,16 +505,16 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "{$destinationTablePrefix}n"); } if ($filter->nodeSearchTerm !== null) { - $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n"); + $queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n")); } if ($filter->nodePropertyValue !== null) { - $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"); + $queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n")); } if ($filter->referenceSearchTerm !== null) { - $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r'); + $queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r')); } if ($filter->referencePropertyValue !== null) { - $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r'); + $queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r')); } if ($filter->referenceName !== null) { $queryBuilder->andWhere('r.name = :referenceName')->setParameter('referenceName', $filter->referenceName->value); @@ -543,10 +543,10 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm); + $queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm)); } if ($filter->propertyValue !== null) { - $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); + $queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue)); } if ($filter->pagination !== null) { $this->applyPagination($queryBuilder, $filter->pagination); @@ -595,9 +595,20 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId */ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregateId, FindDescendantNodesFilter|CountDescendantNodesFilter $filter): array { - $queryBuilderInitial = $this->createQueryBuilder() + $queryBuilderInitial = $this->createQueryBuilder(); + + // todo optimise also if ordering is ASC + $optimize = $filter->pagination !== null && $filter->pagination->limit === 1 && $filter->pagination->offset === 0 && $filter->ordering === null; + + $nodeMatcher = (string)$queryBuilderInitial->expr()->and(...(array_filter([ + $filter->nodeTypes !== null ? $this->nodeQueryBuilder->addNodeTypeCriteria2($queryBuilderInitial, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)) : '', + $filter->searchTerm !== null ? $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderInitial, $filter->searchTerm) : '', + $filter->propertyValue !== null ? $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderInitial, $filter->propertyValue) : '', + ]) ?: ['true'])); // todo empty true case + + $queryBuilderInitial // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation - ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position' . ($optimize ? ", CASE WHEN $nodeMatcher THEN 1 ELSE 0 END AS isMatch" : '')) ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') @@ -610,24 +621,40 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate ->andWhere('p.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial); + $nodeMatcher = (string)$queryBuilderInitial->expr()->and(...(array_filter([ + $filter->nodeTypes !== null ? $this->nodeQueryBuilder->addNodeTypeCriteria2($queryBuilderInitial, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'cn') : '', + $filter->searchTerm !== null ? $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderInitial, $filter->searchTerm, 'cn') : '', + $filter->propertyValue !== null ? $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderInitial, $filter->propertyValue, 'cn') : '', + ]) ?: ['true'])); + $queryBuilderRecursive = $this->createQueryBuilder() - ->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') + ->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position' . ($optimize ? ", CASE WHEN $nodeMatcher THEN 1 ELSE 0 END AS isMatch" : '')) ->from('tree', 'pn') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = h.childnodeanchor') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); + + if ($optimize) { + $queryBuilderRecursive->andWhere('pn.isMatch = 0'); + } + $this->addSubtreeTagConstraints($queryBuilderRecursive); $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint, 'tree', 'n'); - if ($filter->nodeTypes !== null) { - $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); - } - if ($filter->searchTerm !== null) { - $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm); - } - if ($filter->propertyValue !== null) { - $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue); + + if ($optimize === false) { + if ($filter->nodeTypes !== null) { + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); + } + if ($filter->searchTerm !== null && $filter->searchTerm !== '') { + $queryBuilderCte->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm)); + } + if ($filter->propertyValue !== null) { + $queryBuilderCte->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue)); + } + } else { + $queryBuilderCte->where('n.isMatch = 1'); } return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte'); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index 6e73a70b3bc..77d53ec8a2f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -186,25 +186,51 @@ public function addNodeTypeCriteria(QueryBuilder $queryBuilder, ExpandedNodeType } } - public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void + // todo combine with above + public function addNodeTypeCriteria2(QueryBuilder $queryBuilder, ExpandedNodeTypeCriteria $constraintsWithSubNodeTypes, string $nodeTableAlias = 'n'): string { - if ($searchTerm->term === '') { - return; + $nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.'; + $allowanceQueryPart = ''; + if (!$constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->isEmpty()) { + $allowanceQueryPart = $queryBuilder->expr()->in($nodeTablePrefix . 'nodetypename', ':explicitlyAllowedNodeTypeNames'); + $queryBuilder->setParameter('explicitlyAllowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->toStringArray(), ArrayParameterType::STRING); + } + $denyQueryPart = ''; + if (!$constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->isEmpty()) { + $denyQueryPart = $queryBuilder->expr()->notIn($nodeTablePrefix . 'nodetypename', ':explicitlyDisallowedNodeTypeNames'); + $queryBuilder->setParameter('explicitlyDisallowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->toStringArray(), ArrayParameterType::STRING); + } + if ($allowanceQueryPart && $denyQueryPart) { + if ($constraintsWithSubNodeTypes->isWildCardAllowed) { + return (string)$queryBuilder->expr()->or($allowanceQueryPart, $denyQueryPart); + } else { + return (string)$queryBuilder->expr()->and($allowanceQueryPart, $denyQueryPart); + } + } elseif ($allowanceQueryPart && !$constraintsWithSubNodeTypes->isWildCardAllowed) { + return $allowanceQueryPart; + } elseif ($denyQueryPart) { + return $denyQueryPart; } - $queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); + throw new \RuntimeException(sprintf('NO'), 1736614392); + return ''; } - public function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): void + public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): string { - $queryBuilder->andWhere($this->propertyValueConstraints($queryBuilder, $propertyValue, $nodeTableAlias)); + if ($searchTerm->term === '') { + throw new \RuntimeException(sprintf('NO'), 1736614392); + return ''; + } + $queryBuilder->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); + return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL'; } - private function propertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias): string + public function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): string { return match ($propertyValue::class) { - AndCriteria::class => (string)$queryBuilder->expr()->and($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), - NegateCriteria::class => 'NOT (' . $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')', - OrCriteria::class => (string)$queryBuilder->expr()->or($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), + AndCriteria::class => (string)$queryBuilder->expr()->and($this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), + NegateCriteria::class => 'NOT (' . $this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')', + OrCriteria::class => (string)$queryBuilder->expr()->or($this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), PropertyValueContains::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), PropertyValueEndsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive), PropertyValueEquals::class => is_string($propertyValue->value) ? $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive) : $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '=', $nodeTableAlias), diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature index 91f6da0f171..805646baa58 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature @@ -91,7 +91,7 @@ Feature: Find and count nodes using the findDescendantNodes and countDescendantN | nodeAggregateId | "a2a2a" | | nodeVariantSelectionStrategy | "allVariants" | - Scenario: + Scenario: Default # findDescendantNodes queries without results When I execute the findDescendantNodes query for entry node aggregate id "non-existing" I expect no nodes to be returned @@ -131,3 +131,43 @@ Feature: Find and count nodes using the findDescendantNodes and countDescendantN When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"ordering": [{"type": "propertyName", "field": "integerProperty", "direction": "DESCENDING"}]}' I expect the nodes "a2a2b,a2,a1,terms,contact,a,b,b1,a3,a2a,a2a1,a2a2" to be returned When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"ordering": [{"type": "propertyName", "field": "booleanProperty", "direction": "ASCENDING"}, {"type": "timestampField", "field": "LAST_MODIFIED", "direction": "DESCENDING"}]}' I expect the nodes "terms,contact,b,a1,b1,a2,a2a,a2a1,a2a2,a3,a2a2b,a" to be returned When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"ordering": [{"type": "propertyName", "field": "integerProperty", "direction": "DESCENDING"}], "pagination": {"limit": 3, "offset": 4}}' I expect the nodes "contact,a,b" to be returned and the total count to be 12 + + Scenario: Optimized limit=1 + + # findDescendantNodes queries without results + When I execute the findDescendantNodes query for entry node aggregate id "non-existing" and filter '{"pagination": {"limit": 1}}' I expect no nodes to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"searchTerm": "a2a2a", "pagination": {"limit": 1}}' I expect no nodes to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"searchTerm": "string", "pagination": {"limit": 1}}' I expect no nodes to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty > 125", "pagination": {"limit": 1}}' I expect no nodes to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty >= 126", "pagination": {"limit": 1}}' I expect no nodes to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty < 20", "pagination": {"limit": 1}}' I expect no nodes to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty <= 19", "pagination": {"limit": 1}}' I expect no nodes to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty <= 19 OR integerProperty <= 18", "pagination": {"limit": 1}}' I expect no nodes to be returned + # The following should not return node "b1" because boolean true !== "true" + # TODO Broken!!! When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "stringProperty = true", "pagination": {"limit": 1}}' I expect no nodes to be returned + # The following should not return any node because date time properties are serialized into a full timestamp in the format "1980-12-13T00:00:00+00:00" + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "dateProperty = \"1980-12-13\"", "pagination": {"limit": 1}}' I expect no nodes to be returned + + # findDescendantNodes queries with results + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"pagination": {"limit": 1}}' I expect the nodes "terms" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"nodeTypes": "Neos.ContentRepository.Testing:Page", "pagination": {"limit": 1}}' I expect the nodes "a" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"searchTerm": "a2", "pagination": {"limit": 1}}' I expect the nodes "a2" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"searchTerm": "a1", "pagination": {"limit": 1}}' I expect the nodes "a1" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "text ^= \"a1\"", "pagination": {"limit": 1}}' I expect the nodes "a1" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "text ^= \"a1\" OR text $= \"a1\"", "pagination": {"limit": 1}}' I expect the nodes "a1" to be returned + # When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "stringProperty *= \"späCi\" OR text $= \"a1\"", "pagination": {"limit": 1}}' I expect the nodes "b" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "booleanProperty = true", "pagination": {"limit": 1}}' I expect the nodes "a" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "booleanProperty = false", "pagination": {"limit": 1}}' I expect the nodes "a3" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty >= 20", "pagination": {"limit": 1}}' I expect the nodes "a1" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty > 20", "pagination": {"limit": 1}}' I expect the nodes "a1" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty <= 21", "pagination": {"limit": 1}}' I expect the nodes "a2a2b" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "integerProperty < 21", "pagination": {"limit": 1}}' I expect the nodes "a2a2b" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "floatProperty >= 123.45", "pagination": {"limit": 1}}' I expect the nodes "a2a" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "floatProperty > 123.45", "pagination": {"limit": 1}}' I expect the nodes "a2a1" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "floatProperty = 123.45", "pagination": {"limit": 1}}' I expect the nodes "a2a" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "dateProperty >= \"1980-12-13\"", "pagination": {"limit": 1}}' I expect the nodes "a2a1" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"propertyValue": "dateProperty > \"1980-12-13\"", "pagination": {"limit": 1}}' I expect the nodes "a2a1" to be returned + # special cases with ordering + # Todo not when ordering When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"ordering": [{"type": "propertyName", "field": "integerProperty", "direction": "DESCENDING"}], "pagination": {"limit": 1}}' I expect the nodes "a2a2b" to be returned + When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"ordering": [{"type": "propertyName", "field": "booleanProperty", "direction": "ASCENDING"}, {"type": "timestampField", "field": "LAST_MODIFIED", "direction": "DESCENDING"}], "pagination": {"limit": 1}}' I expect the nodes "terms" to be returned + # todo When I execute the findDescendantNodes query for entry node aggregate id "home" and filter '{"ordering": [{"type": "propertyName", "field": "integerProperty", "direction": "DESCENDING"}], "pagination": {"limit": 1, "offset": 2}}' I expect the nodes "contact,a,b" to be returned and the total count to be 12