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: Add ContentSubgraphInterface::findClosestNode() #4519

Merged
merged 3 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -34,6 +34,7 @@
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\FindClosestNodeFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter;
Expand Down Expand Up @@ -355,6 +356,50 @@ public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, CountA
);
}

public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClosestNodeFilter $filter): ?Node
{
$queryBuilderInitial = $this->createQueryBuilder()
->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', 'ph', 'n.relationanchorpoint = ph.childnodeanchor')
->andWhere('ph.contentstreamid = :contentStreamId')
->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')
->andWhere('n.nodeaggregateid = :entryNodeAggregateId');
$this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph');

$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')
->setMaxResults(1)
->setParameter('contentStreamId', $this->contentStreamId->value)
->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash)
->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value);
if ($filter->nodeTypeConstraints !== null) {
$this->addNodeTypeConstraints($queryBuilderCte, $filter->nodeTypeConstraints, 'p');
}
$nodeRows = $this->fetchCteResults(
$queryBuilderInitial,
$queryBuilderRecursive,
$queryBuilderCte,
'ancestry'
);
return $this->nodeFactory->mapNodeRowsToNodes(
$nodeRows,
$this->dimensionSpacePoint,
$this->visibilityConstraints
)->first();
}

public function findDescendantNodes(NodeAggregateId $entryNodeAggregateId, FindDescendantNodesFilter $filter): Nodes
{
['queryBuilderInitial' => $queryBuilderInitial, 'queryBuilderRecursive' => $queryBuilderRecursive, 'queryBuilderCte' => $queryBuilderCte] = $this->buildDescendantNodesQueries($entryNodeAggregateId, $filter);
Expand Down Expand Up @@ -666,7 +711,7 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod
/**
* @return array{queryBuilderInitial: QueryBuilder, queryBuilderRecursive: QueryBuilder, queryBuilderCte: QueryBuilder}
*/
private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId, FindAncestorNodesFilter|CountAncestorNodesFilter $filter): array
private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId, FindAncestorNodesFilter|CountAncestorNodesFilter|FindClosestNodeFilter $filter): array
{
$queryBuilderInitial = $this->createQueryBuilder()
->select('n.*, ph.name, ph.contentstreamid, ph.parentnodeanchor')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindBackReferencesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter;
Expand Down Expand Up @@ -457,6 +458,13 @@ public function countAncestorNodes(
return 0;
}

public function findClosestNode(
NodeAggregateId $entryNodeAggregateId,
FindClosestNodeFilter $filter
): ?Node {
return null;
}

public function findDescendantNodes(
NodeAggregateId $entryNodeAggregateId,
FindDescendantNodesFilter $filter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
@contentrepository @adapters=DoctrineDBAL
# TODO implement for Postgres
Feature: Find nodes using the findClosestNode query

Background:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | mul, de, en, ch | ch->de->mul, en->mul |
And using the following node types:
"""yaml
'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 using identifier "default", I define a content repository
And I am in content repository "default"
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:
# findClosestNode queries without results
# When I execute the findClosestNode query for entry node aggregate id "non-existing" and filter '{"nodeTypeConstraints": "Neos.ContentRepository.Testing:Page"}' I expect no node to be returned
# # a2a2a is disabled
# When I execute the findClosestNode query for entry node aggregate id "a2a2a" and filter '{"nodeTypeConstraints": "Neos.ContentRepository.Testing:Page"}' I expect no node to be returned
# # a2b is disabled
# When I execute the findClosestNode query for entry node aggregate id "a2b1" and filter '{"nodeTypeConstraints": "Neos.ContentRepository.Testing:Page"}' I expect no node to be returned

# findClosestNode queries with results
When I execute the findClosestNode query for entry node aggregate id "a2a2b" and filter '{"nodeTypeConstraints": "Neos.ContentRepository.Testing:Page"}' I expect the node "a2a2b" to be returned
When I execute the findClosestNode query for entry node aggregate id "a2a2b" and filter '{"nodeTypeConstraints": "Neos.ContentRepository.Testing:SpecialPage"}' I expect the node "a2a" to be returned
When I execute the findClosestNode query for entry node aggregate id "a2a2b" and filter '{"nodeTypeConstraints": "!Neos.ContentRepository.Testing:Page,!Neos.ContentRepository.Testing:SpecialPage"}' I expect the node "home" to be returned
When I execute the findClosestNode query for entry node aggregate id "a2a" and filter '{"nodeTypeConstraints": "Neos.ContentRepository.Testing:SpecialPage"}' I expect the node "a2a" to be returned
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, Filter
return $this->wrappedContentSubgraph->countAncestorNodes($entryNodeAggregateId, $filter);
}

public function findClosestNode(NodeAggregateId $entryNodeAggregateId, Filter\FindClosestNodeFilter $filter): ?Node
{
// TODO: Implement findClosestNode() method.
return $this->wrappedContentSubgraph->findClosestNode($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 @@ -109,6 +109,14 @@ public function findAncestorNodes(NodeAggregateId $entryNodeAggregateId, Filter\
*/
public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, Filter\CountAncestorNodesFilter $filter): int;

/**
* Find the closest matching node, the entry node itself or the first ancestors of the $entryNodeAggregateId, that match the specified $filter and return it
* Note: in contrast to {@see findAncestorNodes()} the resulting node will be the entry node if it matches the filter!
*
* @return Node|null the closest node that matches the given $filter, or NULL if no matching node was found
*/
public function findClosestNode(NodeAggregateId $entryNodeAggregateId, Filter\FindClosestNodeFilter $filter): ?Node;

/**
* 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,42 @@
<?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::findClosestNode()}
*
* Example:
*
* FindClosestAncestorNodeFilter::create(nodeTypeConstraints: 'Some.Included:NodeType,!Some.Excluded:NodeType');
*
* @api for the factory methods; NOT for the inner state.
*/
final class FindClosestNodeFilter
{
/**
* @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
): self {
if (is_string($nodeTypeConstraints)) {
$nodeTypeConstraints = NodeTypeConstraints::fromFilterString($nodeTypeConstraints);
}
return new self($nodeTypeConstraints);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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\FindClosestNodeFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter;
Expand Down Expand Up @@ -283,6 +284,19 @@ public function iExecuteTheFindAncestorNodesQueryIExpectTheFollowingNodes(string
Assert::assertSame($expectedTotalCount ?? count($expectedNodeIds), $actualCount, 'countAncestorNodes returned an unexpected result');
}

/**
* @When /^I execute the findClosestNode query for entry node aggregate id "(?<entryNodeIdSerialized>[^"]*)"(?: and filter '(?<filterSerialized>[^']*)')? I expect (?:the node "(?<expectedNodeId>[^"]*)"|no node) to be returned?$/
*/
public function iExecuteTheFindClosestNodeQueryIExpectTheFollowingNodes(string $entryNodeIdSerialized, string $filterSerialized = '', string $expectedNodeId = null): void
{
$entryNodeAggregateId = NodeAggregateId::fromString($entryNodeIdSerialized);
$filterValues = !empty($filterSerialized) ? json_decode($filterSerialized, true, 512, JSON_THROW_ON_ERROR) : [];
$filter = FindClosestNodeFilter::create(...$filterValues);
$subgraph = $this->getCurrentSubgraph();
$actualNodeId = $subgraph->findClosestNode($entryNodeAggregateId, $filter)?->nodeAggregateId->value;
Assert::assertSame($expectedNodeId, $actualNodeId, 'findClosestNode returned an unexpected result');
}

/**
* @When I execute the countNodes query I expect the result to be :expectedResult
*/
Expand Down
Loading