From 7caa2b0b67a220591eff9d0580fd47c48d66fa0a Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 10 Jan 2025 15:19:56 +0100 Subject: [PATCH] FEATURE: Add `findByCriteria` and `findByIdentifier` flowQuery operation `findByCriteria` allows to query the subgraph below the contextNode with the following arguments: - string | null: nodeTypefilter - string | null: propertyValueCriteria - object{offset?:int, limit?:int} | null: pagination `findByIdentifier` will find a node with the given aggregate id in the subgraph defined by the contextNode: - string: nodeAggregateId Resolves: #5434 --- .../FindByCriteriaOperation.php | 207 ++++++++++++++++++ .../FindByIdentifierOperation.php | 114 ++++++++++ .../Features/Fusion/FlowQuery.feature | 43 ++++ 3 files changed, 364 insertions(+) create mode 100644 Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByCriteriaOperation.php create mode 100644 Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByIdentifierOperation.php diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByCriteriaOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByCriteriaOperation.php new file mode 100644 index 00000000000..d9d705b8d1f --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByCriteriaOperation.php @@ -0,0 +1,207 @@ + 'some string'" + * "prop > 123" + * "prop > 123.45" + * + * property value is greater than or equal to the specified value (string, integer or float): + * + * "prop >= 'some string'" + * "prop >= 123" + * "prop >= 123.45" + * + * property value is less than the specified value (string, integer or float): + * + * "prop < 'some string'" + * "prop < 123" + * "prop < 123.45" + * + * property value is less than or equal to the specified value (string, integer or float): + * + * "prop <= 'some string'" + * "prop <= 123" + * "prop <= 123.45" + * + * criteria can be combined using "AND" and "OR": + * + * "prop1 ^= 'foo' AND (prop2 = 'bar' OR prop3 = 'baz')" + * + * furthermore "NOT" can be used to negate a whole sub query + * + * "prop1 ^= 'foo' AND NOT (prop2 = 'bar' OR prop3 = 'baz')" + * + * + * Argument 3 ({limit?:int, offset?:int}}): pagination + * + * + * Example (node type): + * + * q(node).findByCriteria('Neos.NodeTypes:Text') + * + * Example (multiple node types): + * + * q(node).findByCriteria('[instanceof Neos.NodeTypes:Text],[instanceof Neos.NodeTypes:Image]') + * + * Example (node type with property filter): + * + * q(node).findByCriteria('Neos.NodeTypes:Text', 'text*="Neos"') + */ +class FindByCriteriaOperation extends AbstractOperation +{ + use CreateNodeHashTrait; + + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'findByCriteria'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 100; + + /** + * @Flow\Inject + * @var ContentRepositoryRegistry + */ + protected $contentRepositoryRegistry; + + /** + * {@inheritdoc} + * + * @param array $context (or array-like object) onto which this operation should be applied + * @return boolean true if the operation can be applied onto the $context, false otherwise + */ + public function canEvaluate($context) + { + foreach ($context as $contextNode) { + if (!$contextNode instanceof Node) { + return false; + } + } + + return true; + } + /** + * This operation operates rather on the given Context object than on the given node + * and thus may work with the legacy node interface until subgraphs are available + * {@inheritdoc} + * + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation + * @throws FlowQueryException + * @throws \Neos\Eel\Exception + * @throws \Neos\Eel\FlowQuery\FizzleException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments): void + { + /** @var array $contextNodes */ + $contextNodes = $flowQuery->getContext(); + if (count($contextNodes) === 0) { + return; + } + + $firstContextNode = reset($contextNodes); + assert($firstContextNode instanceof Node); + + $nodeTypeFilter = $arguments[0] ?? null; + $propertyValueFilter = $arguments[1] ?? null; + $pagination = $arguments[2] ?? null; + + assert($nodeTypeFilter === null || is_string($nodeTypeFilter)); + assert($propertyValueFilter === null || is_string($propertyValueFilter)); + assert($pagination === null || is_array($pagination)); + + /** @var Node[] $result */ + $result = []; + $findDescendentNodesFilter = FindDescendantNodesFilter::create( + nodeTypes: $nodeTypeFilter ? NodeTypeCriteria::fromFilterString($nodeTypeFilter) : null, + propertyValue: $propertyValueFilter ? PropertyValueCriteriaParser::parse($propertyValueFilter) : null, + pagination: $pagination ? Pagination::fromArray($pagination) : null + ); + + /** @var Node $contextNode */ + foreach ($flowQuery->getContext() as $contextNode) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); + foreach ($subgraph->findDescendantNodes($contextNode->aggregateId, $findDescendentNodesFilter) as $descendant) { + $result[] = $descendant; + } + } + + $flowQuery->setContext($result); + } +} diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByIdentifierOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByIdentifierOperation.php new file mode 100644 index 00000000000..e9266bffd95 --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByIdentifierOperation.php @@ -0,0 +1,114 @@ + $context (or array-like object) onto which this operation should be applied + * @return boolean true if the operation can be applied onto the $context, false otherwise + */ + public function canEvaluate($context) + { + foreach ($context as $contextNode) { + if (!$contextNode instanceof Node) { + return false; + } + } + + return true; + } + /** + * This operation operates rather on the given Context object than on the given node + * and thus may work with the legacy node interface until subgraphs are available + * {@inheritdoc} + * + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation + * @throws FlowQueryException + * @throws \Neos\Eel\Exception + * @throws \Neos\Eel\FlowQuery\FizzleException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments): void + { + /** @var array $contextNodes */ + $contextNodes = $flowQuery->getContext(); + if (count($contextNodes) === 0 || empty($arguments[0])) { + return; + } + + $firstContextNode = reset($contextNodes); + assert($firstContextNode instanceof Node); + + $nodeAggregateId = NodeAggregateId::fromString($arguments[0]); + + /** @var Node[] $result */ + $result = []; + + /** @var Node $contextNode */ + foreach ($flowQuery->getContext() as $contextNode) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); + $nodeByIdentifier = $subgraph->findNodeById($nodeAggregateId); + if ($nodeByIdentifier) { + $result[] = $nodeByIdentifier; + } + } + + $flowQuery->setContext($result); + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index 27d4eeffc79..c2dc72382a7 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -375,6 +375,49 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. absolutePath: a1b """ + Scenario: FindByCriteria + When the Fusion context node is "a1" + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + nodeTypeFilter = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2').get()} + nodeTypeExcludeFilter = ${q(node).findByCriteria('Neos.Neos:Document,!Neos.Neos:Test.DocumentType1').get()} + nodeTypeCombinedFilter = ${q(node).findByCriteria('Neos.Neos:Document1,Neos.Neos:Test.DocumentType2').get()} + nodeTypeFilterWithLimit = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2', null, {limit:2, offset:3}).get()} + + propertyFilter = ${q(node).findByCriteria(null, 'uriPathSegment*="b1"').get()} + propertyAndNodeTypeFilter = ${q(node).findByCriteria('Neos.Neos:DocumentType2a', 'uriPathSegment*="b1"').get()} + @process.render = Neos.Neos:Test.RenderNodesDataStructure + } + """ + Then I expect the following Fusion rendering result: + """ + nodeTypeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + nodeTypeExcludeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + nodeTypeCombinedFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + nodeTypeFilterWithLimit: a1a3,a1a4 + propertyFilter: a1b1,a1b1a,a1b1b + propertyAndNodeTypeFilter: a1b1a + """ + + Scenario: FindByIdentifier + When the Fusion context node is "a1" + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + child = ${q(node).findByIdentifier('a1b1').get()} + grandchild = ${q(node).findByIdentifier('a1b1a').get()} + sibling = ${q(node).findByIdentifier('a2').get()} + @process.render = Neos.Neos:Test.RenderNodesDataStructure + } + """ + Then I expect the following Fusion rendering result: + """ + child: a1b1 + grandchild: a1b1a + sibling: a2 + """ + Scenario: Unique When I execute the following Fusion code: """fusion