diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index e62ee4b7bd4..08848c98681 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -178,6 +178,9 @@ public function addNodeTypeCriteria(QueryBuilder $queryBuilder, ExpandedNodeType public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void { + if ($searchTerm->term === '') { + return; + } $queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature index 9ae1f9eaa73..61e14ccc33c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature @@ -78,10 +78,12 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri | a2a | Neos.ContentRepository.Testing:SpecialPage | a2 | {"text": "a2a"} | {} | | a2a1 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a1", "stringProperty": "the brown fox likes Äpfel", "booleanProperty": true, "integerProperty": 33, "floatProperty": 12.345, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-13"}} | {} | | a2a2 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a2", "stringProperty": "the red fox", "booleanProperty": false, "integerProperty": 22, "floatProperty": 12.34, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-14"}} | {} | - # Note that this node is disabled! See below. - | a2a3 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a3", "stringProperty": "the red Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | + # Note that the node a2a3 is disabled! See below. + | a2a3-disabled | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a3", "stringProperty": "the red Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | | a2a4 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a4", "stringProperty": "the brown Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | | a2a5 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a5", "stringProperty": "the brown bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-13"}} | {} | + # Note that the node a2a6 must not contain any properties! + | a2a6-empty | Neos.ContentRepository.Testing:Page | a2a | {} | {} | | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | | b1 | Neos.ContentRepository.Testing:Page | b | {"text": "b1"} | {} | And the current date and time is "2023-03-16T13:00:00+01:00" @@ -90,16 +92,19 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri | nodeAggregateId | "a2a5" | | propertyValues | {"integerProperty": 20} | And the command DisableNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "a2a3" | - | nodeVariantSelectionStrategy | "allVariants" | + | Key | Value | + | nodeAggregateId | "a2a3-disabled" | + | nodeVariantSelectionStrategy | "allVariants" | Scenario: # Child nodes without filter When I execute the findChildNodes query for parent node aggregate id "home" I expect the nodes "terms,contact,a,b" to be returned When I execute the findChildNodes query for parent node aggregate id "a" I expect the nodes "a1,a2" to be returned When I execute the findChildNodes query for parent node aggregate id "a1" I expect no nodes to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + # Child nodes with empty filter + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": ""}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"nodeTypes": ""}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned # Child nodes filtered by node type When I execute the findChildNodes query for parent node aggregate id "home" and filter '{"nodeTypes": "Neos.ContentRepository.Testing:AbstractPage"}' I expect the nodes "terms,contact,a,b" to be returned @@ -149,10 +154,10 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"propertyValue": "stringProperty $=~ \"Bear\""}' I expect the nodes "a2a4,a2a5" to be returned # Child nodes with custom ordering - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a4,a2a2,a2a1" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "non_existing", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "booleanProperty", "direction": "ASCENDING"}, {"type": "propertyName", "field": "dateProperty", "direction": "ASCENDING"}]}' I expect the nodes "a2a4,a2a5,a2a2,a2a1" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "CREATED", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "LAST_MODIFIED", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a1,a2a2,a2a4" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "ASCENDING"}]}' I expect the nodes "a2a6-empty,a2a1,a2a2,a2a4,a2a5" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a4,a2a2,a2a1,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "non_existing", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "booleanProperty", "direction": "ASCENDING"}, {"type": "propertyName", "field": "dateProperty", "direction": "ASCENDING"}]}' I expect the nodes "a2a6-empty,a2a4,a2a5,a2a2,a2a1" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "CREATED", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "LAST_MODIFIED", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a1,a2a2,a2a4,a2a6-empty" to be returned diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php index 7d6a807baa7..8d1f7b07571 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php @@ -17,6 +17,12 @@ /** * A search term for use in Filters for the {@see ContentSubgraphInterface} API. * + * The search is defined the following: + * - test all properties if one contains the term + * - the term is checked case-insensitive + * - an empty term will lead to no filtering + * - FIXME: define the search behaviour across non-string-typed properties + * * @api DTO for {@see ContentSubgraphInterface} */ final readonly class SearchTerm diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php index 1afdeb42cad..1d3dff96350 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php @@ -21,6 +21,9 @@ public static function matchesNode(Node $node, SearchTerm $searchTerm): bool public static function matchesSerializedPropertyValues(SerializedPropertyValues $serializedPropertyValues, SearchTerm $searchTerm): bool { + if ($searchTerm->term === '') { + return true; + } foreach ($serializedPropertyValues as $serializedPropertyValue) { if (self::matchesValue($serializedPropertyValue->value, $searchTerm)) { return true; diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php index f320822539c..eaf22358166 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php @@ -106,6 +106,12 @@ public static function matchingArrayComparisonExamples(): iterable } } + public function emptySearchTermAlwaysMatches(): iterable + { + yield '1 property' => ['', self::value('foo')]; + yield '1 empty property' => ['', self::value('foo')]; + yield '0 properties' => ['', SerializedPropertyValues::fromArray([])]; + } public function notMatchingExamples(): iterable { @@ -125,6 +131,7 @@ public function notMatchingExamples(): iterable * @dataProvider matchingNumberLikeComparisonExamples * @dataProvider matchingBooleanLikeComparisonExamples * @dataProvider matchingArrayComparisonExamples + * @dataProvider emptySearchTermAlwaysMatches */ public function searchTermMatchesProperties( string $searchTerm, diff --git a/Neos.Neos/Classes/Controller/Service/NodesController.php b/Neos.Neos/Classes/Controller/Service/NodesController.php index 5faf919d6ae..2e5090b31dc 100644 --- a/Neos.Neos/Classes/Controller/Service/NodesController.php +++ b/Neos.Neos/Classes/Controller/Service/NodesController.php @@ -114,7 +114,7 @@ public function indexAction( string $contextNode = null, array|string $nodeIdentifiers = [] ): void { - $searchTerm = $searchTerm === '' ? null : SearchTerm::fulltext($searchTerm); + $searchTerm = SearchTerm::fulltext($searchTerm); $nodeTypeCriteria = NodeTypeCriteria::create( NodeTypeNames::fromStringArray($nodeTypes), NodeTypeNames::createEmpty() @@ -176,7 +176,7 @@ public function indexAction( } } } else { - if (!empty($searchTerm)) { + if ($searchTerm->term !== '') { throw new \RuntimeException('Combination of $nodeIdentifiers and $searchTerm not supported'); } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 663b53980ff..e2c8ce3fc5d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -165,11 +165,6 @@ parameters: count: 1 path: Neos.Neos/Classes/Controller/Module/User/UserSettingsController.php - - - message: "#^Parameter \\#2 \\$searchTerm of static method Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTermMatcher\\:\\:matchesNode\\(\\) expects Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTerm, Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTerm\\|null given\\.$#" - count: 1 - path: Neos.Neos/Classes/Controller/Service/NodesController.php - - message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\NodeType\\\\ExpandedNodeTypeCriteria\\:\\:matches\" is called\\.$#" count: 1