Skip to content

Commit

Permalink
FEATURE: Add findByCriteria and findByIdentifier flowQuery operation
Browse files Browse the repository at this point in the history
`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
  • Loading branch information
mficzel committed Jan 10, 2025
1 parent f20b28c commit 7f1a11d
Show file tree
Hide file tree
Showing 3 changed files with 323 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Eel\FlowQuery\FlowQueryException;
use Neos\Eel\FlowQuery\Operations\AbstractOperation;

/**
* "findByCriteria" operation working on ContentRepository nodes. This operation allows for retrieval of descendant nodes
*
* Argument 1 (string|null): nodeTypeFilter, A list of NodeType Names seperated by ",", disallowed NodeTypes are prefixed with "!"
*
* Argument 2 (string|null): propertyValueCriteria A property criteria in the form
*
* property criteria are specified as "<property> <operator> <value>". Multiple criteria can be combined using "AND", "OR", "NOT" and "()"
*
*
* property criteria support the following comparison operators:
*
* =~ : Strict equality of case-insensitive value and operand
* = : Strict equality of value and operand
* !=~ : Strict inequality of case-insensitive value and operand
* != : Strict inequality of value and operand
* < : Value is less than operand
* <= : Value is less than or equal to operand
* > : Value is greater than operand
* >= : Value is greater than or equal to operand
* $=~ : Value ends with operand (string-based) or case-insensitive value's last element is equal to operand (array-based)
* $= : Value ends with operand (string-based) or value's last element is equal to operand (array-based)
* ^=~ : Value starts with operand (string-based) or case-insensitive value's first element is equal to operand (array-based)
* ^= : Value starts with operand (string-based) or value's first element is equal to operand (array-based)
* *=~ : Value contains operand (string-based) or case-insensitive value contains an element that is equal to operand (array based)
* *= : Value contains operand (string-based) or value contains an element that is equal to operand (array based)
*
* 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 of the date
*
*
* Example (node type):
*
* q(node).findByCriteria('Neos.NodeTypes:Text')
*
* Example (multiple node types):
*
* q(node).findByCriteria('Neos.NodeTypes:Text,Neos.NodeTypes:Image')
*
* Example (node type with property filter):
*
* q(node).findByCriteria('Neos.NodeTypes:Text', 'text*="Neos"')
*
* Example (node type with property filter and pagination):
*
* q(node).findByCriteria('Neos.NodeTypes:Document', 'title*="Flow"', {limit:10, offset:2})
*/
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<int,mixed> $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<int,mixed> $flowQuery the FlowQuery object
* @param array<int,mixed> $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<int,Node> $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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Eel\FlowQuery\FlowQueryException;
use Neos\Eel\FlowQuery\Operations\AbstractOperation;

/**
* "findByIdentifier" operation working on ContentRepository nodes. This operation allows for retrieval of nodes by identifier
* from the current subgraph
*
* Example:
*
* q(site).findByIdentifier('30e893c1-caef-0ca5-b53d-e5699bb8e506')
*/
class FindByIdentifierOperation extends AbstractOperation
{
use CreateNodeHashTrait;

/**
* {@inheritdoc}
*
* @var string
*/
protected static $shortName = 'findByIdentifier';

/**
* {@inheritdoc}
*
* @var integer
*/
protected static $priority = 100;

/**
* @Flow\Inject
* @var ContentRepositoryRegistry
*/
protected $contentRepositoryRegistry;

/**
* {@inheritdoc}
*
* @param array<int,mixed> $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<int,mixed> $flowQuery the FlowQuery object
* @param array<int,mixed> $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<int,Node> $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);
}
}
42 changes: 42 additions & 0 deletions Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,48 @@ 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:Test.Document1,Neos.Neos:Test.DocumentType2a').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:Test.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
Expand Down

0 comments on commit 7f1a11d

Please sign in to comment.