Skip to content

Commit 70df082

Browse files
committed
FEATURE: Add findByCriteria and findByIdentifier flowQuery operation
`findByCriteria` allows to query the subgraph below the contextNode 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 arguments: - string: nodeAggregateId Resolves: neos#5434
1 parent f20b28c commit 70df082

File tree

3 files changed

+323
-0
lines changed

3 files changed

+323
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations;
6+
7+
/*
8+
* This file is part of the Neos.ContentRepository package.
9+
*
10+
* (c) Contributors of the Neos Project - www.neos.io
11+
*
12+
* This package is Open Source Software. For the full copyright and license
13+
* information, please view the LICENSE file which was distributed with this
14+
* source code.
15+
*/
16+
17+
use Neos\Flow\Annotations as Flow;
18+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
19+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
20+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination;
21+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser;
22+
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
23+
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
24+
use Neos\Eel\FlowQuery\FlowQuery;
25+
use Neos\Eel\FlowQuery\FlowQueryException;
26+
use Neos\Eel\FlowQuery\Operations\AbstractOperation;
27+
28+
/**
29+
* "findByCriteria" operation working on ContentRepository nodes. This operation allows for retrieval of descendant nodes
30+
*
31+
* Argument 1 (string|null): nodeTypeFilter, A list of NodeType Names seperated by ",", disallowed NodeTypes are prefixed with "!"
32+
*
33+
* Argument 2 (string|null): propertyValueCriteria A property criteria in the form
34+
*
35+
* property criteria are specified as "<property> <operator> <value>". Multiple criteria can be combined using "AND", "OR", "NOT" and "()"
36+
*
37+
*
38+
* property criteria support the following comparison operators:
39+
*
40+
* =~ : Strict equality of case-insensitive value and operand
41+
* = : Strict equality of value and operand
42+
* !=~ : Strict inequality of case-insensitive value and operand
43+
* != : Strict inequality of value and operand
44+
* < : Value is less than operand
45+
* <= : Value is less than or equal to operand
46+
* > : Value is greater than operand
47+
* >= : Value is greater than or equal to operand
48+
* $=~ : Value ends with operand (string-based) or case-insensitive value's last element is equal to operand (array-based)
49+
* $= : Value ends with operand (string-based) or value's last element is equal to operand (array-based)
50+
* ^=~ : Value starts with operand (string-based) or case-insensitive value's first element is equal to operand (array-based)
51+
* ^= : Value starts with operand (string-based) or value's first element is equal to operand (array-based)
52+
* *=~ : Value contains operand (string-based) or case-insensitive value contains an element that is equal to operand (array based)
53+
* *= : Value contains operand (string-based) or value contains an element that is equal to operand (array based)
54+
*
55+
* criteria can be combined using "AND" and "OR":
56+
*
57+
* "prop1 ^= 'foo' AND (prop2 = 'bar' OR prop3 = 'baz')"
58+
*
59+
* furthermore "NOT" can be used to negate a whole sub query
60+
*
61+
* "prop1 ^= 'foo' AND NOT (prop2 = 'bar' OR prop3 = 'baz')"
62+
*
63+
* Argument 3 ({limit?:int, offset?:int}}): Pagination of the date
64+
*
65+
*
66+
* Example (node type):
67+
*
68+
* q(node).findByCriteria('Neos.NodeTypes:Text')
69+
*
70+
* Example (multiple node types):
71+
*
72+
* q(node).findByCriteria('Neos.NodeTypes:Text,Neos.NodeTypes:Image')
73+
*
74+
* Example (node type with property filter):
75+
*
76+
* q(node).findByCriteria('Neos.NodeTypes:Text', 'text*="Neos"')
77+
*
78+
* Example (node type with property filter and pagination):
79+
*
80+
* q(node).findByCriteria('Neos.NodeTypes:Document', 'title*="Flow"', {limit:10, offset:2})
81+
*/
82+
class FindByCriteriaOperation extends AbstractOperation
83+
{
84+
use CreateNodeHashTrait;
85+
86+
/**
87+
* {@inheritdoc}
88+
*
89+
* @var string
90+
*/
91+
protected static $shortName = 'findByCriteria';
92+
93+
/**
94+
* {@inheritdoc}
95+
*
96+
* @var integer
97+
*/
98+
protected static $priority = 100;
99+
100+
/**
101+
* @Flow\Inject
102+
* @var ContentRepositoryRegistry
103+
*/
104+
protected $contentRepositoryRegistry;
105+
106+
/**
107+
* {@inheritdoc}
108+
*
109+
* @param array<int,mixed> $context (or array-like object) onto which this operation should be applied
110+
* @return boolean true if the operation can be applied onto the $context, false otherwise
111+
*/
112+
public function canEvaluate($context)
113+
{
114+
foreach ($context as $contextNode) {
115+
if (!$contextNode instanceof Node) {
116+
return false;
117+
}
118+
}
119+
120+
return true;
121+
}
122+
/**
123+
* This operation operates rather on the given Context object than on the given node
124+
* and thus may work with the legacy node interface until subgraphs are available
125+
* {@inheritdoc}
126+
*
127+
* @param FlowQuery<int,mixed> $flowQuery the FlowQuery object
128+
* @param array<int,mixed> $arguments the arguments for this operation
129+
* @throws FlowQueryException
130+
* @throws \Neos\Eel\Exception
131+
* @throws \Neos\Eel\FlowQuery\FizzleException
132+
*/
133+
public function evaluate(FlowQuery $flowQuery, array $arguments): void
134+
{
135+
/** @var array<int,Node> $contextNodes */
136+
$contextNodes = $flowQuery->getContext();
137+
if (count($contextNodes) === 0) {
138+
return;
139+
}
140+
141+
$firstContextNode = reset($contextNodes);
142+
assert($firstContextNode instanceof Node);
143+
144+
$nodeTypeFilter = $arguments[0] ?? null;
145+
$propertyValueFilter = $arguments[1] ?? null;
146+
$pagination = $arguments[2] ?? null;
147+
148+
assert($nodeTypeFilter === null || is_string($nodeTypeFilter));
149+
assert($propertyValueFilter === null || is_string($propertyValueFilter));
150+
assert($pagination === null || is_array($pagination));
151+
152+
/** @var Node[] $result */
153+
$result = [];
154+
$findDescendentNodesFilter = FindDescendantNodesFilter::create(
155+
nodeTypes: $nodeTypeFilter ? NodeTypeCriteria::fromFilterString($nodeTypeFilter) : null,
156+
propertyValue: $propertyValueFilter ? PropertyValueCriteriaParser::parse($propertyValueFilter) : null,
157+
pagination: $pagination ? Pagination::fromArray($pagination) : null
158+
);
159+
160+
/** @var Node $contextNode */
161+
foreach ($flowQuery->getContext() as $contextNode) {
162+
$subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode);
163+
foreach ($subgraph->findDescendantNodes($contextNode->aggregateId, $findDescendentNodesFilter) as $descendant) {
164+
$result[] = $descendant;
165+
}
166+
}
167+
168+
$flowQuery->setContext($result);
169+
}
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations;
6+
7+
/*
8+
* This file is part of the Neos.ContentRepository package.
9+
*
10+
* (c) Contributors of the Neos Project - www.neos.io
11+
*
12+
* This package is Open Source Software. For the full copyright and license
13+
* information, please view the LICENSE file which was distributed with this
14+
* source code.
15+
*/
16+
17+
use Neos\Flow\Annotations as Flow;
18+
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
19+
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
20+
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
21+
use Neos\Eel\FlowQuery\FlowQuery;
22+
use Neos\Eel\FlowQuery\FlowQueryException;
23+
use Neos\Eel\FlowQuery\Operations\AbstractOperation;
24+
25+
/**
26+
* "findByIdentifier" operation working on ContentRepository nodes. This operation allows for retrieval of nodes by identifier
27+
* from the current subgraph
28+
*
29+
* Example:
30+
*
31+
* q(site).findByIdentifier('30e893c1-caef-0ca5-b53d-e5699bb8e506')
32+
*/
33+
class FindByIdentifierOperation extends AbstractOperation
34+
{
35+
use CreateNodeHashTrait;
36+
37+
/**
38+
* {@inheritdoc}
39+
*
40+
* @var string
41+
*/
42+
protected static $shortName = 'findByIdentifier';
43+
44+
/**
45+
* {@inheritdoc}
46+
*
47+
* @var integer
48+
*/
49+
protected static $priority = 100;
50+
51+
/**
52+
* @Flow\Inject
53+
* @var ContentRepositoryRegistry
54+
*/
55+
protected $contentRepositoryRegistry;
56+
57+
/**
58+
* {@inheritdoc}
59+
*
60+
* @param array<int,mixed> $context (or array-like object) onto which this operation should be applied
61+
* @return boolean true if the operation can be applied onto the $context, false otherwise
62+
*/
63+
public function canEvaluate($context)
64+
{
65+
foreach ($context as $contextNode) {
66+
if (!$contextNode instanceof Node) {
67+
return false;
68+
}
69+
}
70+
71+
return true;
72+
}
73+
/**
74+
* This operation operates rather on the given Context object than on the given node
75+
* and thus may work with the legacy node interface until subgraphs are available
76+
* {@inheritdoc}
77+
*
78+
* @param FlowQuery<int,mixed> $flowQuery the FlowQuery object
79+
* @param array<int,mixed> $arguments the arguments for this operation
80+
* @throws FlowQueryException
81+
* @throws \Neos\Eel\Exception
82+
* @throws \Neos\Eel\FlowQuery\FizzleException
83+
*/
84+
public function evaluate(FlowQuery $flowQuery, array $arguments): void
85+
{
86+
/** @var array<int,Node> $contextNodes */
87+
$contextNodes = $flowQuery->getContext();
88+
if (count($contextNodes) === 0 || empty($arguments[0])) {
89+
return;
90+
}
91+
92+
$firstContextNode = reset($contextNodes);
93+
assert($firstContextNode instanceof Node);
94+
95+
$nodeAggregateId = NodeAggregateId::fromString($arguments[0]);
96+
97+
/** @var Node[] $result */
98+
$result = [];
99+
100+
/** @var Node $contextNode */
101+
foreach ($flowQuery->getContext() as $contextNode) {
102+
$subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode);
103+
$nodeByIdentifier = $subgraph->findNodeById($nodeAggregateId);
104+
if ($nodeByIdentifier) {
105+
$result[] = $nodeByIdentifier;
106+
}
107+
}
108+
109+
$flowQuery->setContext($result);
110+
}
111+
}

Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature

+42
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,48 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods.
375375
absolutePath: a1b
376376
"""
377377

378+
Scenario: FindByCriteria
379+
When the Fusion context node is "a1"
380+
When I execute the following Fusion code:
381+
"""fusion
382+
test = Neos.Fusion:DataStructure {
383+
nodeTypeFilter = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2').get()}
384+
nodeTypeExcludeFilter = ${q(node).findByCriteria('Neos.Neos:Document,!Neos.Neos:Test.DocumentType1').get()}
385+
nodeTypeCombinedFilter = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType1,Neos.Neos:Test.DocumentType2a').get()}
386+
nodeTypeFilterWithLimit = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2', null, {limit:2, offset:3}).get()}
387+
propertyFilter = ${q(node).findByCriteria(null, 'uriPathSegment*="b1"').get()}
388+
propertyAndNodeTypeFilter = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2a', 'uriPathSegment*="b1"').get()}
389+
@process.render = Neos.Neos:Test.RenderNodesDataStructure
390+
}
391+
"""
392+
Then I expect the following Fusion rendering result:
393+
"""
394+
nodeTypeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a
395+
nodeTypeExcludeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a
396+
nodeTypeCombinedFilter: a1a,a1a1,a1a3,a1a4,a1a5,a1a7,a1b,a1b1,a1b1a,a1b1b,aib3,a1c
397+
nodeTypeFilterWithLimit: a1a3,a1a4
398+
propertyFilter: a1b1,a1b1a,a1b1b
399+
propertyAndNodeTypeFilter: a1b1a
400+
"""
401+
402+
Scenario: FindByIdentifier
403+
When the Fusion context node is "a1"
404+
When I execute the following Fusion code:
405+
"""fusion
406+
test = Neos.Fusion:DataStructure {
407+
child = ${q(node).findByIdentifier('a1b1').get()}
408+
grandchild = ${q(node).findByIdentifier('a1b1a').get()}
409+
sibling = ${q(node).findByIdentifier('a2').get()}
410+
@process.render = Neos.Neos:Test.RenderNodesDataStructure
411+
}
412+
"""
413+
Then I expect the following Fusion rendering result:
414+
"""
415+
child: a1b1
416+
grandchild: a1b1a
417+
sibling: a2
418+
"""
419+
378420
Scenario: Unique
379421
When I execute the following Fusion code:
380422
"""fusion

0 commit comments

Comments
 (0)