Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: 9.0 Implement ContentSubgraphInterface::findAncestorNodes and countAncestorNodes #4349

Merged
merged 5 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountAncestorNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountBackReferencesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountDescendantNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountReferencesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindAncestorNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindBackReferencesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
Expand Down Expand Up @@ -305,6 +307,43 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi
return $rootSubtrees->first();
}

public function findAncestorNodes(NodeAggregateId $entryNodeAggregateId, FindAncestorNodesFilter $filter): Nodes
{
[
dlubitz marked this conversation as resolved.
Show resolved Hide resolved
'queryBuilderInitial' => $queryBuilderInitial,
'queryBuilderRecursive' => $queryBuilderRecursive,
'queryBuilderCte' => $queryBuilderCte
] = $this->buildAncestorNodesQueries($entryNodeAggregateId, $filter);
$nodeRows = $this->fetchCteResults(
$queryBuilderInitial,
$queryBuilderRecursive,
$queryBuilderCte,
'ancestry'
);

return $this->nodeFactory->mapNodeRowsToNodes(
$nodeRows,
$this->dimensionSpacePoint,
$this->visibilityConstraints
);
}

public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, CountAncestorNodesFilter $filter): int
{
[
'queryBuilderInitial' => $queryBuilderInitial,
'queryBuilderRecursive' => $queryBuilderRecursive,
'queryBuilderCte' => $queryBuilderCte
] = $this->buildAncestorNodesQueries($entryNodeAggregateId, $filter);

return $this->fetchCteCountResult(
$queryBuilderInitial,
$queryBuilderRecursive,
$queryBuilderCte,
'ancestry'
);
}

public function findDescendantNodes(NodeAggregateId $entryNodeAggregateId, FindDescendantNodesFilter $filter): Nodes
{
['queryBuilderInitial' => $queryBuilderInitial, 'queryBuilderRecursive' => $queryBuilderRecursive, 'queryBuilderCte' => $queryBuilderCte] = $this->buildDescendantNodesQueries($entryNodeAggregateId, $filter);
Expand Down Expand Up @@ -599,6 +638,48 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod
return $queryBuilder;
}

/**
* @return array{queryBuilderInitial: QueryBuilder, queryBuilderRecursive: QueryBuilder, queryBuilderCte: QueryBuilder}
*/
private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId, FindAncestorNodesFilter|CountAncestorNodesFilter $filter): array
{
$queryBuilderInitial = $this->createQueryBuilder()
// @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation
nezaniel marked this conversation as resolved.
Show resolved Hide resolved
->select('n.*, ph.name, ph.contentstreamid, ph.parentnodeanchor')
->from($this->tableNamePrefix . '_node', 'n')
// we need to join with the hierarchy relation, because we need the node name.
->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = n.relationanchorpoint')
->innerJoin('ch', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = ch.childnodeanchor')
->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'n.relationanchorpoint = ph.childnodeanchor')
->where('ch.contentstreamid = :contentStreamId')
->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('ph.contentstreamid = :contentStreamId')
->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('c.nodeaggregateid = :entryNodeAggregateId');
$this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph');
$this->addRestrictionRelationConstraints($queryBuilderInitial, 'c', 'ch');

$queryBuilderRecursive = $this->createQueryBuilder()
->select('p.*, h.name, h.contentstreamid, h.parentnodeanchor')
->from('ancestry', 'c')
->innerJoin('c', $this->tableNamePrefix . '_node', 'p', 'p.relationanchorpoint = c.parentnodeanchor')
->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = p.relationanchorpoint')
->where('h.contentstreamid = :contentStreamId')
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash');
$this->addRestrictionRelationConstraints($queryBuilderRecursive, 'p');

$queryBuilderCte = $this->createQueryBuilder()
->select('*')
->from('ancestry', 'p')
->setParameter('contentStreamId', $this->contentStreamId->value)
->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash)
->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value);
if ($filter->nodeTypeConstraints !== null) {
$this->addNodeTypeConstraints($queryBuilderCte, $filter->nodeTypeConstraints, 'p');
}
return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte');
}

/**
* @return array{queryBuilderInitial: QueryBuilder, queryBuilderRecursive: QueryBuilder, queryBuilderCte: QueryBuilder}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,14 +426,27 @@ public function findSubtree(
return $this->nodeFactory->mapNodeRowsToSubtree($nodeRows, $this->visibilityConstraints);
}

public function findAncestorNodes(
NodeAggregateId $entryNodeAggregateId,
Filter\FindAncestorNodesFilter $filter
): Nodes {
return Nodes::createEmpty();
}

public function countAncestorNodes(
NodeAggregateId $entryNodeAggregateId,
Filter\CountAncestorNodesFilter $filter
): int {
return 0;
}

public function findDescendantNodes(
NodeAggregateId $entryNodeAggregateId,
FindDescendantNodesFilter $filter
): Nodes {
return Nodes::createEmpty();
}


public function countDescendantNodes(NodeAggregateId $entryNodeAggregateId, Filter\CountDescendantNodesFilter $filter): int
{
// TODO: Implement countDescendantNodes() method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ class FeatureContext implements \Behat\Behat\Context\Context
*/
protected $behatTestHelperObjectName = BehatTestHelper::class;

protected ?ContentRepositoryRegistry $contentRepositoryRegistry = null;

public function __construct()
{
if (self::$bootstrap === null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
@contentrepository @adapters=DoctrineDBAL
# TODO implement for Postgres
Feature: Find and count nodes using the findAncestorNodes and countAncestorNodes queries

Background:
Given I have the following content dimensions:
| Identifier | Values | Generalizations |
| language | mul, de, en, ch | ch->de->mul, en->mul |
And I have the following NodeTypes configuration:
"""
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:AbstractPage':
abstract: true
'Neos.ContentRepository.Testing:SomeMixin':
abstract: true
'Neos.ContentRepository.Testing:Homepage':
superTypes:
'Neos.ContentRepository.Testing:AbstractPage': true
childNodes:
terms:
type: 'Neos.ContentRepository.Testing:Terms'
contact:
type: 'Neos.ContentRepository.Testing:Contact'

'Neos.ContentRepository.Testing:Terms':
superTypes:
'Neos.ContentRepository.Testing:AbstractPage': true
properties:
text:
defaultValue: 'Terms default'
'Neos.ContentRepository.Testing:Contact':
superTypes:
'Neos.ContentRepository.Testing:AbstractPage': true
'Neos.ContentRepository.Testing:SomeMixin': true
properties:
text:
defaultValue: 'Contact default'
'Neos.ContentRepository.Testing:Page':
superTypes:
'Neos.ContentRepository.Testing:AbstractPage': true
'Neos.ContentRepository.Testing:SpecialPage':
superTypes:
'Neos.ContentRepository.Testing:AbstractPage': true
"""
And I am user identified by "initiating-user-identifier"
And the command CreateRootWorkspace is executed with payload:
| Key | Value |
| workspaceName | "live" |
| workspaceTitle | "Live" |
| workspaceDescription | "The live workspace" |
| newContentStreamId | "cs-identifier" |
And the graph projection is fully up to date
And I am in content stream "cs-identifier" and dimension space point {"language":"de"}
And the command CreateRootNodeAggregateWithNode is executed with payload:
| Key | Value |
| nodeAggregateId | "lady-eleonode-rootford" |
| nodeTypeName | "Neos.ContentRepository:Root" |
And the graph projection is fully up to date
And the following CreateNodeAggregateWithNode commands are executed:
| nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | initialPropertyValues | tetheredDescendantNodeAggregateIds |
| home | home | Neos.ContentRepository.Testing:Homepage | lady-eleonode-rootford | {} | {"terms": "terms", "contact": "contact"} |
| a | a | Neos.ContentRepository.Testing:Page | home | {} | {} |
| a1 | a1 | Neos.ContentRepository.Testing:Page | a | {} | {} |
| a2 | a2 | Neos.ContentRepository.Testing:Page | a | {} | {} |
| a2a | a2a | Neos.ContentRepository.Testing:SpecialPage | a2 | {} | {} |
| a2a1 | a2a1 | Neos.ContentRepository.Testing:Page | a2a | {} | {} |
| a2a2 | a2a2 | Neos.ContentRepository.Testing:Page | a2a | {} | {} |
| a2a2a | a2a2a | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} |
| a2a2b | a2a2b | Neos.ContentRepository.Testing:Page | a2a2 | {} | {} |
| a2b | a2b | Neos.ContentRepository.Testing:Page | a2 | {} | {} |
| a2b1 | a2b1 | Neos.ContentRepository.Testing:Page | a2b | {} | {} |
| b | b | Neos.ContentRepository.Testing:Page | home | {} | {} |
And the command DisableNodeAggregate is executed with payload:
| Key | Value |
| nodeAggregateId | "a2a2a" |
| nodeVariantSelectionStrategy | "allVariants" |
And the graph projection is fully up to date
And the command DisableNodeAggregate is executed with payload:
| Key | Value |
| nodeAggregateId | "a2b" |
| nodeVariantSelectionStrategy | "allVariants" |
And the graph projection is fully up to date

Scenario:
# findAncestorNodes queries without results
When I execute the findAncestorNodes query for entry node aggregate id "non-existing" I expect no nodes to be returned
# a2a2a is disabled
When I execute the findAncestorNodes query for entry node aggregate id "a2a2a" I expect no nodes to be returned
# a2b is disabled
When I execute the findAncestorNodes query for entry node aggregate id "a2b1" I expect no nodes to be returned

# findAncestorNodes queries with results
When I execute the findAncestorNodes query for entry node aggregate id "a2a2b" I expect the nodes "a2a2,a2a,a2,a,home,lady-eleonode-rootford" to be returned and the total count to be 6
When I execute the findAncestorNodes query for entry node aggregate id "a2a2b" and filter '{"nodeTypeConstraints": "Neos.ContentRepository.Testing:Page"}' I expect the nodes "a2a2,a2,a" to be returned and the total count to be 3
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi
// TODO populate NodeByNodeAggregateIdCache and ParentNodeIdByChildNodeIdCache from result
}

public function findAncestorNodes(NodeAggregateId $entryNodeAggregateId, Filter\FindAncestorNodesFilter $filter): Nodes
{
// TODO: implement runtime caches
return $this->wrappedContentSubgraph->findAncestorNodes($entryNodeAggregateId, $filter);
}

public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, Filter\CountAncestorNodesFilter $filter): int
{
// TODO: Implement countAncestorNodes() method.
return $this->wrappedContentSubgraph->countAncestorNodes($entryNodeAggregateId, $filter);
}

public function findDescendantNodes(NodeAggregateId $entryNodeAggregateId, FindDescendantNodesFilter $filter): Nodes
{
// TODO: implement runtime caches
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ public function findPrecedingSiblingNodes(NodeAggregateId $siblingNodeAggregateI
*/
public function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $edgeName): ?Node;

/**
* Recursively find all nodes above the $entryNodeAggregateId that match the specified $filter and return them as a flat list
*/
public function findAncestorNodes(NodeAggregateId $entryNodeAggregateId, Filter\FindAncestorNodesFilter $filter): Nodes;

/**
* Count all nodes above the $entryNodeAggregateId that match the specified $filter
* @see findAncestorNodes
*/
public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, Filter\CountAncestorNodesFilter $filter): int;

/**
* Recursively find all nodes underneath the $entryNodeAggregateId that match the specified $filter and return them as a flat list
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Projection\ContentGraph\Filter;

use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTypeConstraints;

/**
* Immutable filter DTO for {@see ContentSubgraphInterface::countAncestorNodes()}
*
* Example:
*
* FindAncestorNodesFilter::create(nodeTypeConstraints: 'Some.Included:NodeType,!Some.Excluded:NodeType');
*
* @api for the factory methods; NOT for the inner state.
*/
final class CountAncestorNodesFilter
{
/**
* @internal (the properties themselves are readonly; only the write-methods are API.
*/
private function __construct(
public readonly ?NodeTypeConstraints $nodeTypeConstraints
) {
}

/**
* Creates an instance with the specified filter options
*
* Note: The signature of this method might be extended in the future, so it should always be used with named arguments
* @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments
*/
public static function create(
NodeTypeConstraints|string $nodeTypeConstraints = null
): self {
if (is_string($nodeTypeConstraints)) {
$nodeTypeConstraints = NodeTypeConstraints::fromFilterString($nodeTypeConstraints);
}
return new self($nodeTypeConstraints);
}

public static function fromFindAncestorNodesFilter(FindAncestorNodesFilter $filter): self
{
return new self($filter->nodeTypeConstraints);
}

/**
* Returns a new instance with the specified additional filter options
*
* Note: The signature of this method might be extended in the future, so it should always be used with named arguments
* @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments
*/
public function with(
NodeTypeConstraints|string $nodeTypeConstraints = null
): self {
return self::create(
$nodeTypeConstraints ?? $this->nodeTypeConstraints,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Projection\ContentGraph\Filter;

use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTypeConstraints;

/**
* Immutable filter DTO for {@see ContentSubgraphInterface::findAncestorNodes()}
*
* Example:
*
* FindAncestorNodesFilter::create(nodeTypeConstraints: 'Some.Included:NodeType,!Some.Excluded:NodeType');
*
* @api for the factory methods; NOT for the inner state.
*/
final class FindAncestorNodesFilter
{
/**
* @internal (the properties themselves are readonly; only the write-methods are API.
*/
private function __construct(
public readonly ?NodeTypeConstraints $nodeTypeConstraints
) {
}

/**
* Creates an instance with the specified filter options
*
* Note: The signature of this method might be extended in the future, so it should always be used with named arguments
* @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments
*/
public static function create(
NodeTypeConstraints|string $nodeTypeConstraints = null
): self {
if (is_string($nodeTypeConstraints)) {
$nodeTypeConstraints = NodeTypeConstraints::fromFilterString($nodeTypeConstraints);
}
return new self($nodeTypeConstraints);
}

/**
* Returns a new instance with the specified additional filter options
*
* Note: The signature of this method might be extended in the future, so it should always be used with named arguments
* @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments
*/
public function with(
NodeTypeConstraints|string $nodeTypeConstraints = null
): self {
return self::create(
$nodeTypeConstraints ?? $this->nodeTypeConstraints,
);
}
}
Loading