From 7411991a25794a5a220eb3dad6ae9dfe67345ec8 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 27 Apr 2024 12:27:13 +0200 Subject: [PATCH 01/17] 4150 - Enforce uniqueness of name on parent aggregate level --- .../src/Domain/Repository/ContentGraph.php | 13 ++++-- ...NodeAggregateName_ConstraintChecks.feature | 46 ++++++++++++++++--- .../Feature/NodeRenaming/NodeRenaming.php | 17 +++---- 3 files changed, 57 insertions(+), 19 deletions(-) rename Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/{NodeRenaming => 09-NodeRenaming}/01_ChangeNodeAggregateName_ConstraintChecks.feature (60%) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 4781ae1a82e..89115914877 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -201,6 +201,10 @@ public function findNodeAggregateById( } /** + * Parent node aggregates can have a greater dimension space coverage than the given child. + * Thus, it is not enough to just resolve them from the nodes and edges connected to the given child node aggregate. + * Instead, we resolve all parent node aggregate ids instead and fetch the complete aggregates from there. + * * @return iterable */ public function findParentNodeAggregates( @@ -208,12 +212,12 @@ public function findParentNodeAggregates( NodeAggregateId $childNodeAggregateId ): iterable { $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ph.name, ph.contentstreamid, ph.subtreetags, pdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->distinct() + ->select('pn.nodeaggregateid AS parentNodeAggregateId') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->innerJoin('ph', $this->tableNamePrefix . '_dimensionspacepoints', 'pdsp', 'pdsp.hash = ph.dimensionspacepointhash') ->where('cn.nodeaggregateid = :nodeAggregateId') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ch.contentstreamid = :contentStreamId') @@ -222,7 +226,10 @@ public function findParentNodeAggregates( 'contentStreamId' => $contentStreamId->value ]); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); + return NodeAggregates::fromArray(array_filter(array_map( + fn (array $row): ?NodeAggregate => $this->findNodeAggregateById($contentStreamId, NodeAggregateId::fromString($row['parentNodeAggregateId'])), + $this->fetchRows($queryBuilder) + ))); } public function findParentNodeAggregateByChildOriginDimensionSpacePoint( diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature similarity index 60% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature index 542dbc3fb26..6256baca534 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature @@ -6,7 +6,9 @@ Feature: Change node name These are the base test cases for the NodeAggregateCommandHandler to block invalid commands. Background: - Given using no content dimensions + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | And using the following node types: """yaml 'Neos.ContentRepository.Testing:Content': [] @@ -24,7 +26,7 @@ Feature: Change node name | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in the active content stream of workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -32,9 +34,9 @@ Feature: Change node name | 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 | - | sir-david-nodenborough | null | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | {} | {"tethered": "nodewyn-tetherton"} | - | nody-mc-nodeface | occupied | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | {} | {} | + | nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | null | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | {"tethered": "nodewyn-tetherton"} | + | nody-mc-nodeface | occupied | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | {} | Scenario: Try to rename a node aggregate in a non-existing workspace When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: @@ -44,6 +46,16 @@ Feature: Change node name | newNodeName | "new-name" | Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Scenario: Try to rename a node aggregate in a workspace whose content stream is closed: + When the command CloseContentStream is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | newNodeName | "new-name" | + Then the last command should have thrown an exception of type "ContentStreamIsClosed" + Scenario: Try to rename a non-existing node aggregate When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: | Key | Value | @@ -70,4 +82,26 @@ Feature: Change node name | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | newNodeName | "tethered" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to rename a node aggregate using a partially occupied name + # Could happen via creation or move with the same effect + Given the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"example": "source"} | + | targetOrigin | {"example": "peer"} | + And the graph projection is fully up to date + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | originDimensionSpacePoint | {"example": "peer"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | parentNodeAggregateId | "sir-david-nodenborough" | + | nodeName | "esquire" | + And the graph projection is fully up to date + When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "esquire" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php index 40403f523ba..7dbbe43fad4 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php @@ -42,16 +42,13 @@ private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, $this->requireNodeAggregateToNotBeRoot($nodeAggregate, 'and Root Node Aggregates cannot be renamed'); $this->requireNodeAggregateToBeUntethered($nodeAggregate); foreach ($contentRepository->getContentGraph()->findParentNodeAggregates($contentStreamId, $command->nodeAggregateId) as $parentNodeAggregate) { - foreach ($parentNodeAggregate->occupiedDimensionSpacePoints as $occupiedParentDimensionSpacePoint) { - $this->requireNodeNameToBeUnoccupied( - $contentStreamId, - $command->newNodeName, - $parentNodeAggregate->nodeAggregateId, - $occupiedParentDimensionSpacePoint, - $parentNodeAggregate->coveredDimensionSpacePoints, - $contentRepository - ); - } + $this->requireNodeNameToBeUncovered( + $contentStreamId, + $command->newNodeName, + $parentNodeAggregate->nodeAggregateId, + $parentNodeAggregate->coveredDimensionSpacePoints, + $contentRepository + ); } $events = Events::with( From ff3b1649426bb9698c2db35d7032c2df520af97c Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 27 Apr 2024 18:11:39 +0200 Subject: [PATCH 02/17] 4150 - refactor ContentGraphs to return single named child node aggregates --- .../src/Domain/Repository/ContentGraph.php | 14 +++++++------ .../Domain/Repository/ContentHypergraph.php | 9 +++------ ...NodeAggregateName_ConstraintChecks.feature | 4 ++-- .../Feature/Common/ConstraintChecks.php | 19 ++++++------------ .../Feature/Common/TetheredNodeInternals.php | 20 +++---------------- .../Classes/Feature/NodeMove/NodeMove.php | 3 --- .../Feature/NodeRenaming/NodeRenaming.php | 1 - .../ContentGraph/ContentGraphInterface.php | 8 +++----- .../Module/Administration/SitesController.php | 8 ++------ .../Domain/Service/SiteServiceInternals.php | 10 ++++------ 10 files changed, 31 insertions(+), 65 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 89115914877..0bbecfb4e7d 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -278,18 +278,20 @@ public function findChildNodeAggregates( return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); } - /** - * @return iterable - */ - public function findChildNodeAggregatesByName( + public function findChildNodeAggregateByName( ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable { + ): ?NodeAggregate { $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId) ->andWhere('ch.name = :relationName') ->setParameter('relationName', $name->value); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); + + return $this->nodeFactory->mapNodeRowsToNodeAggregate( + $this->fetchRows($queryBuilder), + $contentStreamId, + VisibilityConstraints::withoutRestrictions() + ); } /** diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php index 49f9b7af5be..83e2282a116 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php @@ -227,14 +227,11 @@ public function findChildNodeAggregates( ); } - /** - * @return iterable - */ - public function findChildNodeAggregatesByName( + public function findChildNodeAggregateByName( ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable { + ): ?NodeAggregate { $query = HypergraphChildQuery::create( $contentStreamId, $parentNodeAggregateId, @@ -244,7 +241,7 @@ public function findChildNodeAggregatesByName( $nodeRows = $query->execute($this->getDatabaseConnection())->fetchAllAssociative(); - return $this->nodeFactory->mapNodeRowsToNodeAggregates( + return $this->nodeFactory->mapNodeRowsToNodeAggregate( $nodeRows, VisibilityConstraints::withoutRestrictions() ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature index 6256baca534..560afeef8ed 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature @@ -77,14 +77,14 @@ Feature: Change node name | newNodeName | "new-name" | Then the last command should have thrown an exception of type "NodeAggregateIsTethered" - Scenario: Try to rename a node aggregate using an already occupied name + Scenario: Try to rename a node aggregate using an already covered name When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | newNodeName | "tethered" | Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" - Scenario: Try to rename a node aggregate using a partially occupied name + Scenario: Try to rename a node aggregate using a partially covered name # Could happen via creation or move with the same effect Given the command CreateNodeVariant is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 2ee3b0f4bf2..93b34cdf584 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -662,28 +662,21 @@ protected function requireNodeNameToBeUncovered( ContentStreamId $contentStreamId, ?NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, - DimensionSpacePointSet $dimensionSpacePointsToBeCovered, ContentRepository $contentRepository ): void { if ($nodeName === null) { return; } - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( + $childNodeAggregate = $contentRepository->getContentGraph()->findChildNodeAggregateByName( $contentStreamId, $parentNodeAggregateId, $nodeName ); - foreach ($childNodeAggregates as $childNodeAggregate) { - /* @var $childNodeAggregate NodeAggregate */ - $alreadyCoveredDimensionSpacePoints = $childNodeAggregate->coveredDimensionSpacePoints - ->getIntersection($dimensionSpacePointsToBeCovered); - if (!$alreadyCoveredDimensionSpacePoints->isEmpty()) { - throw new NodeNameIsAlreadyCovered( - 'Node name "' . $nodeName->value . '" is already covered in dimension space points ' - . $alreadyCoveredDimensionSpacePoints->toJson() . ' by node aggregate "' - . $childNodeAggregate->nodeAggregateId->value . '".' - ); - } + if ($childNodeAggregate instanceof NodeAggregate) { + throw new NodeNameIsAlreadyCovered( + 'Node name "' . $nodeName->value . '" is already covered by node aggregate "' + . $childNodeAggregate->nodeAggregateId->value . '".' + ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php index 5d2a01fce2d..e630a562078 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php @@ -63,20 +63,13 @@ protected function createEventsForMissingTetheredNode( NodeType $expectedTetheredNodeType, ContentRepository $contentRepository ): Events { - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( + $childNodeAggregate = $contentRepository->getContentGraph()->findChildNodeAggregateByName( $parentNodeAggregate->contentStreamId, $parentNodeAggregate->nodeAggregateId, $tetheredNodeName ); - $tmp = []; - foreach ($childNodeAggregates as $childNodeAggregate) { - $tmp[] = $childNodeAggregate; - } - /** @var array $childNodeAggregates */ - $childNodeAggregates = $tmp; - - if (count($childNodeAggregates) === 0) { + if ($childNodeAggregate === null) { // there is no tethered child node aggregate already; let's create it! $nodeType = $this->nodeTypeManager->requireNodeType($parentNodeAggregate->nodeTypeName); if ($nodeType->isOfType(NodeTypeName::ROOT_NODE_TYPE_NAME)) { @@ -131,9 +124,7 @@ protected function createEventsForMissingTetheredNode( ) ); } - } elseif (count($childNodeAggregates) === 1) { - /** @var NodeAggregate $childNodeAggregate */ - $childNodeAggregate = current($childNodeAggregates); + } else { if (!$childNodeAggregate->classification->isTethered()) { throw new \RuntimeException( 'We found a child node aggregate through the given node path; but it is not tethered.' @@ -155,11 +146,6 @@ protected function createEventsForMissingTetheredNode( $parentNodeAggregate, $contentRepository ); - } else { - throw new \RuntimeException( - 'There is >= 2 ChildNodeAggregates with the same name reachable from the parent' . - '- this is ambiguous and we should analyze how this may happen. That is very likely a bug.' - ); } } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php index a93f5f0aa8a..5636f86ca80 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php @@ -123,9 +123,6 @@ private function handleMoveNodeAggregate( $contentStreamId, $nodeAggregate->nodeName, $command->newParentNodeAggregateId, - // We need to check all covered DSPs of the parent node aggregate to prevent siblings - // with different node aggregate IDs but the same name - $newParentNodeAggregate->coveredDimensionSpacePoints, $contentRepository ); diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php index 7dbbe43fad4..86116ddecae 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php @@ -46,7 +46,6 @@ private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, $contentStreamId, $command->newNodeName, $parentNodeAggregate->nodeAggregateId, - $parentNodeAggregate->coveredDimensionSpacePoints, $contentRepository ); } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php index 4d8d7e4a4a7..1d9fa4e860b 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php @@ -122,17 +122,15 @@ public function findChildNodeAggregates( ): iterable; /** - * A node aggregate may have multiple child node aggregates with the same name - * as long as they do not share dimension space coverage + * A node aggregate can have no or exactly one child node aggregate with a given name as enforced by constraint checks * - * @return iterable * @internal only for consumption inside the Command Handler */ - public function findChildNodeAggregatesByName( + public function findChildNodeAggregateByName( ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable; + ): ?NodeAggregate; /** * @return iterable diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 24dd502fb69..5671876a527 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -205,16 +205,12 @@ public function updateSiteAction(Site $site, $newSiteNodeName) } foreach ($contentRepository->getWorkspaceFinder()->findAll() as $workspace) { - // technically, due to the name being the "identifier", there might be more than one :/ - /** @var NodeAggregate[] $siteNodeAggregates */ - /** @var Workspace $workspace */ - $siteNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( + $siteNodeAggregate = $contentRepository->getContentGraph()->findChildNodeAggregateByName( $workspace->currentContentStreamId, $sitesNode->nodeAggregateId, $site->getNodeName()->toNodeName() ); - - foreach ($siteNodeAggregates as $siteNodeAggregate) { + if ($siteNodeAggregate instanceof NodeAggregate) { $contentRepository->handle(ChangeNodeAggregateName::create( $workspace->workspaceName, $siteNodeAggregate->nodeAggregateId, diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index 7dbe858a5c5..3a9a901f231 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -61,14 +61,12 @@ public function removeSiteNode(SiteNodeName $siteNodeName): void $workspace->currentContentStreamId, NodeTypeNameFactory::forSites() ); - $siteNodeAggregates = $contentGraph->findChildNodeAggregatesByName( + $siteNodeAggregate = $contentGraph->findChildNodeAggregateByName( $workspace->currentContentStreamId, $sitesNodeAggregate->nodeAggregateId, $siteNodeName->toNodeName() ); - - foreach ($siteNodeAggregates as $siteNodeAggregate) { - assert($siteNodeAggregate instanceof NodeAggregate); + if ($siteNodeAggregate instanceof NodeAggregate) { $this->contentRepository->handle(RemoveNodeAggregate::create( $workspace->workspaceName, $siteNodeAggregate->nodeAggregateId, @@ -99,12 +97,12 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); } - $siteNodeAggregate = $this->contentRepository->getContentGraph()->findChildNodeAggregatesByName( + $siteNodeAggregate = $this->contentRepository->getContentGraph()->findChildNodeAggregateByName( $liveWorkspace->currentContentStreamId, $sitesNodeIdentifier, $site->getNodeName()->toNodeName(), ); - foreach ($siteNodeAggregate as $_) { + if ($siteNodeAggregate instanceof NodeAggregate) { // Site node already exists return; } From 641ab6dd89f728d37b20a29027a9b07b4c548f2c Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 27 Apr 2024 18:45:03 +0200 Subject: [PATCH 03/17] 4150 - Check for node name coverage instead of occupation ...to enforce uniqueness of names on aggregate level --- ...AggregateWithNode_ConstraintChecks.feature | 2 +- ...de_ConstraintChecks_WithDimensions.feature | 33 +++++++++++++++---- .../01-MoveNodes_ConstraintChecks.feature | 32 ++++++++++-------- .../06-AdditionalConstraintChecks.feature | 6 ++-- .../Feature/Common/ConstraintChecks.php | 32 ------------------ .../Feature/NodeCreation/NodeCreation.php | 6 +--- .../NodeDuplicationCommandHandler.php | 8 ++--- .../Exception/NodeNameIsAlreadyOccupied.php | 23 ------------- .../Classes/Command/SiteCommandController.php | 4 +-- .../Module/Administration/SitesController.php | 4 +-- 10 files changed, 56 insertions(+), 94 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature index 7ca3ef355a9..f59456f0558 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature @@ -152,7 +152,7 @@ Feature: Create node aggregate with node | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" Scenario: Try to create a node aggregate with a property the node type does not declare When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature index 3fd276365f5..175b6c20db0 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature @@ -9,8 +9,8 @@ Feature: Create node aggregate with node Background: Given using the following content dimensions: - | Identifier | Values | Generalizations | - | language | mul, de, gsw | gsw->de->mul | + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | And using the following node types: """yaml 'Neos.ContentRepository.Testing:Node': @@ -54,16 +54,35 @@ Feature: Create node aggregate with node | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "lady-eleonode-rootford" | - | originDimensionSpacePoint | {"language":"gsw"} | + | originDimensionSpacePoint | {"example":"spec"} | And the graph projection is fully up to date And the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "sir-david-nodenborough" | - | originDimensionSpacePoint | {"language":"de"} | + | originDimensionSpacePoint | {"example":"source"} | Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint" + Scenario: Try to create a node aggregate using a name that is already partially covered by one of its siblings + Given the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"example":"peer"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + And the graph projection is fully up to date + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + Scenario: Try to create a node aggregate with a root parent and a sibling already claiming the name # root nodes are special in that they have the empty DSP as origin, wich may affect constraint checks When the command CreateNodeAggregateWithNode is executed with payload: @@ -71,7 +90,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "lady-eleonode-rootford" | - | originDimensionSpacePoint | {"language":"de"} | + | originDimensionSpacePoint | {"example":"source"} | | nodeName | "document" | And the graph projection is fully up to date And the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: @@ -79,6 +98,6 @@ Feature: Create node aggregate with node | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "lady-eleonode-rootford" | - | originDimensionSpacePoint | {"language":"de"} | + | originDimensionSpacePoint | {"example":"source"} | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature index 47ae7031caf..00f3dbc8266 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature @@ -163,13 +163,19 @@ Feature: Move node to a new parent / within the current parent before a sibling Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePointSet" Scenario: Using the scatter strategy, try to move a node to a parent that already has a child node of the same name - Given the following CreateNodeAggregateWithNode commands are executed: + Given the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | sourceOrigin | {"example": "source"} | + | targetOrigin | {"example": "peer"} | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | - | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | document | + | nody-mc-nodeface | {"example": "peer"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | document | When the command MoveNodeAggregate is executed with payload and exceptions are caught: | Key | Value | - | dimensionSpacePoint | {"example": "source"} | + | dimensionSpacePoint | {"example": "peer"} | | nodeAggregateId | "nody-mc-nodeface" | | newParentNodeAggregateId | "lady-eleonode-rootford" | | relationDistributionStrategy | "scatter" | @@ -190,9 +196,9 @@ Feature: Move node to a new parent / within the current parent before a sibling Scenario: Using the gatherAll strategy, try to move a node to a parent that already has a child node of the same name in a generalization Given the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | - | rival-destinode | {"example": "general"} | Neos.ContentRepository.Testing:Document | general-nodesworth | target-document | - | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | nodimus-prime | target-document | + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | rival-destinode | {"example": "general"} | Neos.ContentRepository.Testing:Document | general-nodesworth | target-document | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | nodimus-prime | target-document | # Remove the node with the conflicting name in all variants except the generalization And the command RemoveNodeAggregate is executed with payload: | Key | Value | @@ -208,11 +214,11 @@ Feature: Move node to a new parent / within the current parent before a sibling And the graph projection is fully up to date When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"example": "source"} | - | nodeAggregateId | "nody-mc-nodeface" | - | newParentNodeAggregateId | "general-nodesworth" | - | relationDistributionStrategy | "gatherAll" | + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "general-nodesworth" | + | relationDistributionStrategy | "gatherAll" | Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" Scenario: Try to move a node to a parent whose node type does not allow child nodes of the node's type @@ -267,7 +273,7 @@ Feature: Move node to a new parent / within the current parent before a sibling Scenario: Try to move existing node after a node which is not a child of the new parent When the command MoveNodeAggregate is executed with payload and exceptions are caught: | Key | Value | - | dimensionSpacePoint | {"example": "spec"} | + | dimensionSpacePoint | {"example": "spec"} | | nodeAggregateId | "sir-david-nodenborough" | | newParentNodeAggregateId | "anthony-destinode" | | newPrecedingSiblingNodeAggregateId | "sir-nodeward-nodington-iii" | @@ -295,7 +301,7 @@ Feature: Move node to a new parent / within the current parent before a sibling Scenario: Try to move existing node before a node which is not a child of the new parent When the command MoveNodeAggregate is executed with payload and exceptions are caught: | Key | Value | - | dimensionSpacePoint | {"example": "spec"} | + | dimensionSpacePoint | {"example": "spec"} | | nodeAggregateId | "sir-david-nodenborough" | | newParentNodeAggregateId | "anthony-destinode" | | newSucceedingSiblingNodeAggregateId | "sir-nodeward-nodington-iii" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature index a401fe2a00d..a3ecd838bde 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature @@ -60,20 +60,20 @@ Feature: Additional constraint checks after move node capabilities are introduce | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | parentNodeAggregateId | "sir-nodeward-nodington-iii" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | parentNodeAggregateId | "lady-abigail-nodenborough" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | parentNodeAggregateId | "general-nodesworth" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 93b34cdf584..db33a2bce60 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -47,7 +47,6 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsAbstract; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsNotOfTypeRoot; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsOfTypeRoot; @@ -624,37 +623,6 @@ protected function requireNodeAggregateToBeChild( ); } - /** - * @throws NodeNameIsAlreadyOccupied - */ - protected function requireNodeNameToBeUnoccupied( - ContentStreamId $contentStreamId, - ?NodeName $nodeName, - NodeAggregateId $parentNodeAggregateId, - OriginDimensionSpacePoint $parentOriginDimensionSpacePoint, - DimensionSpacePointSet $dimensionSpacePoints, - ContentRepository $contentRepository - ): void { - if ($nodeName === null) { - return; - } - $dimensionSpacePointsOccupiedByChildNodeName = $contentRepository->getContentGraph() - ->getDimensionSpacePointsOccupiedByChildNodeName( - $contentStreamId, - $nodeName, - $parentNodeAggregateId, - $parentOriginDimensionSpacePoint, - $dimensionSpacePoints - ); - if (count($dimensionSpacePointsOccupiedByChildNodeName) > 0) { - throw new NodeNameIsAlreadyOccupied( - 'Child node name "' . $nodeName->value . '" is already occupied for parent "' - . $parentNodeAggregateId->value . '" in dimension space points ' - . $dimensionSpacePointsOccupiedByChildNodeName->toJson() - ); - } - } - /** * @throws NodeNameIsAlreadyCovered */ diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index 72ecce321d3..cf0f1a31704 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -171,14 +171,10 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $parentNodeAggregate->coveredDimensionSpacePoints ); if ($command->nodeName) { - $this->requireNodeNameToBeUnoccupied( + $this->requireNodeNameToBeUncovered( $contentStreamId, $command->nodeName, $command->parentNodeAggregateId, - $parentNodeAggregate->classification->isRoot() - ? DimensionSpace\OriginDimensionSpacePoint::createWithoutDimensions() - : $command->originDimensionSpacePoint, - $coveredDimensionSpacePoints, $contentRepository ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index acac660dfbd..f07da21bf8b 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -136,16 +136,12 @@ private function handleCopyNodesRecursively( $parentNodeAggregate->coveredDimensionSpacePoints ); - // Constraint: The node name must be free in all these dimension space points + // Constraint: The node name must be free for a new child of the parent node aggregate if ($command->targetNodeName) { - $this->requireNodeNameToBeUnoccupied( + $this->requireNodeNameToBeUncovered( $contentStreamId, $command->targetNodeName, $command->targetParentNodeAggregateId, - $parentNodeAggregate->classification->isRoot() - ? OriginDimensionSpacePoint::createWithoutDimensions() - : $command->targetDimensionSpacePoint, - $coveredDimensionSpacePoints, $contentRepository ); } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php deleted file mode 100644 index a457d6d1fa9..00000000000 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php +++ /dev/null @@ -1,23 +0,0 @@ -quit(1); - } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyOccupied $exception) { + } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyCovered $exception) { $this->outputLine('A site with siteNodeName "%s" already exists', [$nodeName ?: $name]); $this->quit(1); } diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 5671876a527..8f1dc87c732 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -18,7 +18,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -412,7 +412,7 @@ public function createSiteNodeAction($packageKey, $siteName, $nodeType) 1412372375 ); $this->redirect('createSiteNode'); - } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyOccupied $exception) { + } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyCovered $exception) { $this->addFlashMessage( $this->getModuleLabel('sites.SiteCreationError.siteWithSiteNodeNameAlreadyExists.body', [$siteName]), $this->getModuleLabel('sites.SiteCreationError.siteWithSiteNodeNameAlreadyExists.title'), From 22b99984771e767eaa48d0b5b9f95c94d3c311d8 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 27 Apr 2024 20:15:03 +0200 Subject: [PATCH 04/17] 4150 - reserve tethered node names ... and prevent manual, erroneous structure adjustments --- ...de_ConstraintChecks_WithDimensions.feature | 41 +++++++++++++++++++ .../01-MoveNodes_ConstraintChecks.feature | 32 +++++++++++++++ ...NodeAggregateName_ConstraintChecks.feature | 18 ++++++++ ...eNodeAggregateType_BasicErrorCases.feature | 4 +- .../Feature/Common/ConstraintChecks.php | 39 ++++++------------ .../Feature/NodeCreation/NodeCreation.php | 4 +- .../NodeDuplicationCommandHandler.php | 1 - .../Classes/Feature/NodeMove/NodeMove.php | 8 +++- .../Feature/NodeRenaming/NodeRenaming.php | 2 + .../Feature/NodeTypeChange/NodeTypeChange.php | 9 ++-- 10 files changed, 120 insertions(+), 38 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature index 175b6c20db0..65f01548c35 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature @@ -101,3 +101,44 @@ Feature: Create node aggregate with node | originDimensionSpacePoint | {"example":"source"} | | nodeName | "document" | Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to create a node aggregate using a name of a not yet existent, tethered child of the parent + Given the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + And the graph projection is fully up to date + Given I change the node types in content repository "default" to: + """yaml + 'Neos.ContentRepository.Testing:LeafNode': {} + 'Neos.ContentRepository.Testing:Node': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:LeafNode' + properties: + postalAddress: + type: 'Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress' + 'Neos.ContentRepository.Testing:NodeWithInvalidPropertyType': + properties: + postalAddress: + type: '\I\Do\Not\Exist' + 'Neos.ContentRepository.Testing:NodeWithInvalidDefaultValue': + properties: + postalAddress: + type: 'Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress' + defaultValue: + iDoNotExist: 'whatever' + 'Neos.ContentRepository.Testing:AbstractNode': + abstract: true + """ + # We don't run structure adjustments here on purpose + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:LeafNode" | + | parentNodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeName | "tethered" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature index 00f3dbc8266..b6e8919b959 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature @@ -181,6 +181,38 @@ Feature: Move node to a new parent / within the current parent before a sibling | relationDistributionStrategy | "scatter" | Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + Scenario: Using the scatter (or really any) strategy, try to move a node to a parent that reserves the name for a tethered child + Given I change the node types in content repository "default" to: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + 'Neos.ContentRepository.Testing:Content': + constraints: + nodeTypes: + '*': true + 'Neos.ContentRepository.Testing:Document': false + 'Neos.ContentRepository.Testing:DocumentWithTetheredChildNode': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Content' + constraints: + nodeTypes: + '*': true + 'Neos.ContentRepository.Testing:Content': false + another-tethered: + type: 'Neos.ContentRepository.Testing:Content' + """ + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | another-tethered | + + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + Scenario: Using the gatherSpecializations strategy, try to move a node to a parent that already has a child node of the same name in a specialization Given the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature index 560afeef8ed..bc4fa5f84fc 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature @@ -105,3 +105,21 @@ Feature: Change node name | nodeAggregateId | "nody-mc-nodeface" | | newNodeName | "esquire" | Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to rename a node aggregate using a name of a not yet existent, tethered child + Given I change the node types in content repository "default" to: + """yaml + 'Neos.ContentRepository.Testing:Content': [] + 'Neos.ContentRepository.Testing:Document': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Content' + another-tethered: + type: 'Neos.ContentRepository.Testing:Content' + """ + # We don't run structure adjustments here on purpose + When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "another-tethered" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature index 597ab0036b9..9de7da5b20d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature @@ -135,7 +135,7 @@ Feature: Change node aggregate type - basic error cases | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeConstraintException" - Scenario: Try to change the node type of an auto created child node to anything other than defined: + Scenario: Try to change the node type of an tethered child node: When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: | Key | Value | | nodeAggregateId | "parent2-na" | @@ -152,4 +152,4 @@ Feature: Change node aggregate type - basic error cases | nodeAggregateId | "nody-mc-nodeface" | | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | | strategy | "happypath" | - Then the last command should have thrown an exception of type "NodeConstraintException" + Then the last command should have thrown an exception of type "NodeAggregateIsTethered" diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index db33a2bce60..6b2e92f22a6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -220,6 +220,17 @@ protected function requireNodeTypeToDeclareReference(NodeTypeName $nodeTypeName, throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt($referenceName, $nodeTypeName); } + protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void + { + $nodeType = $this->requireNodeType($nodeTypeName); + if ($nodeType->hasTetheredNode($nodeName)) { + throw new NodeNameIsAlreadyCovered( + 'Node name "' . $nodeName->value . '" is reserved for a tethered child of parent node aggregate of type "' + . $nodeTypeName->value . '".' + ); + } + } + protected function requireNodeTypeToAllowNodesOfTypeInReference( NodeTypeName $nodeTypeName, ReferenceName $referenceName, @@ -258,16 +269,12 @@ protected function requireNodeTypeToAllowNumberOfReferencesInReference(Serialize /** * NodeType and NodeName must belong together to the same node, which is the to-be-checked one. * - * @param ContentStreamId $contentStreamId - * @param NodeType $nodeType - * @param NodeName|null $nodeName * @param array|NodeAggregateId[] $parentNodeAggregateIds * @throws NodeConstraintException */ protected function requireConstraintsImposedByAncestorsAreMet( ContentStreamId $contentStreamId, NodeType $nodeType, - ?NodeName $nodeName, array $parentNodeAggregateIds, ContentRepository $contentRepository ): void { @@ -280,7 +287,7 @@ protected function requireConstraintsImposedByAncestorsAreMet( if (!$parentAggregate->classification->isTethered()) { try { $parentsNodeType = $this->requireNodeType($parentAggregate->nodeTypeName); - $this->requireNodeTypeConstraintsImposedByParentToBeMet($parentsNodeType, $nodeName, $nodeType); + $this->requireNodeTypeConstraintsImposedByParentToBeMet($parentsNodeType, $nodeType); } catch (NodeTypeNotFound $e) { // skip constraint check; Once the parent is changed to be of an available type, // the constraint checks are executed again. See handleChangeNodeAggregateType @@ -315,7 +322,6 @@ protected function requireConstraintsImposedByAncestorsAreMet( */ protected function requireNodeTypeConstraintsImposedByParentToBeMet( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): void { // !!! IF YOU ADJUST THIS METHOD, also adjust the method below. @@ -326,37 +332,16 @@ protected function requireNodeTypeConstraintsImposedByParentToBeMet( 1707561400 ); } - if ( - $nodeName - && $parentsNodeType->hasTetheredNode($nodeName) - && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) - ) { - throw new NodeConstraintException( - 'Node type "' . $nodeType->name->value . '" does not match configured "' - . $this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->value - . '" for auto created child nodes for parent type "' . $parentsNodeType->name->value - . '" with name "' . $nodeName->value . '"', - 1707561404 - ); - } } protected function areNodeTypeConstraintsImposedByParentValid( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): bool { // !!! IF YOU ADJUST THIS METHOD, also adjust the method above. if (!$parentsNodeType->allowsChildNodeType($nodeType)) { return false; } - if ( - $nodeName - && $parentsNodeType->hasTetheredNode($nodeName) - && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) - ) { - return false; - } return true; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index cf0f1a31704..2dbe6215f97 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -63,6 +63,8 @@ abstract protected function requireNodeTypeToNotBeAbstract(NodeType $nodeType): abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): void; + abstract protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void; + abstract protected function getPropertyConverter(): PropertyConverter; abstract protected function getNodeTypeManager(): NodeTypeManager; @@ -138,7 +140,6 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $this->requireConstraintsImposedByAncestorsAreMet( $contentStreamId, $nodeType, - $command->nodeName, [$command->parentNodeAggregateId], $contentRepository ); @@ -177,6 +178,7 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $command->parentNodeAggregateId, $contentRepository ); + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($parentNodeAggregate->nodeTypeName, $command->nodeName); } $descendantNodeAggregateIds = $command->tetheredDescendantNodeAggregateIds->completeForNodeOfType( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index f07da21bf8b..687495d116f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -97,7 +97,6 @@ private function handleCopyNodesRecursively( $this->requireConstraintsImposedByAncestorsAreMet( $contentStreamId, $nodeType, - $command->targetNodeName, [$command->targetParentNodeAggregateId], $contentRepository ); diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php index 5636f86ca80..e8397501fce 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php @@ -28,6 +28,7 @@ use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\RelationDistributionStrategy; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; @@ -40,6 +41,7 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsNoSibling; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @@ -51,6 +53,8 @@ abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\ abstract protected function areAncestorNodeTypeConstraintChecksEnabled(): bool; + abstract protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void; + abstract protected function requireProjectedNodeAggregate( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, @@ -108,7 +112,6 @@ private function handleMoveNodeAggregate( $this->requireConstraintsImposedByAncestorsAreMet( $contentStreamId, $this->requireNodeType($nodeAggregate->nodeTypeName), - $nodeAggregate->nodeName, [$command->newParentNodeAggregateId], $contentRepository ); @@ -125,6 +128,9 @@ private function handleMoveNodeAggregate( $command->newParentNodeAggregateId, $contentRepository ); + if ($nodeAggregate->nodeName) { + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($newParentNodeAggregate->nodeTypeName, $nodeAggregate->nodeName); + } $this->requireNodeAggregateToCoverDimensionSpacePoints( $newParentNodeAggregate, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php index 86116ddecae..a989db70d68 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php @@ -22,6 +22,7 @@ use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; use Neos\ContentRepository\Core\Feature\NodeRenaming\Event\NodeAggregateNameWasChanged; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; /** * @internal implementation detail of Command Handlers @@ -48,6 +49,7 @@ private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, $parentNodeAggregate->nodeAggregateId, $contentRepository ); + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($parentNodeAggregate->nodeTypeName, $command->newNodeName); } $events = Events::with( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index 25e60f27449..f95ed8a3f08 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -50,6 +50,8 @@ trait NodeTypeChange { abstract protected function getNodeTypeManager(): NodeTypeManager; + abstract protected function requireNodeAggregateToBeUntethered(NodeAggregate $nodeAggregate): void; + abstract protected function requireProjectedNodeAggregate( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, @@ -59,20 +61,17 @@ abstract protected function requireProjectedNodeAggregate( abstract protected function requireConstraintsImposedByAncestorsAreMet( ContentStreamId $contentStreamId, NodeType $nodeType, - ?NodeName $nodeName, array $parentNodeAggregateIds, ContentRepository $contentRepository ): void; abstract protected function requireNodeTypeConstraintsImposedByParentToBeMet( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): void; abstract protected function areNodeTypeConstraintsImposedByParentValid( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): bool; @@ -119,6 +118,7 @@ private function handleChangeNodeAggregateType( $command->nodeAggregateId, $contentRepository ); + $this->requireNodeAggregateToBeUntethered($nodeAggregate); // node type detail checks $this->requireNodeTypeToNotBeOfTypeRoot($newNodeType); @@ -135,7 +135,6 @@ private function handleChangeNodeAggregateType( $this->requireConstraintsImposedByAncestorsAreMet( $contentStreamId, $newNodeType, - $nodeAggregate->nodeName, [$parentNodeAggregate->nodeAggregateId], $contentRepository ); @@ -250,7 +249,6 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( // so we use $newNodeType (the target node type of $node after the operation) here. $this->requireNodeTypeConstraintsImposedByParentToBeMet( $newNodeType, - $childNodeAggregate->nodeName, $this->requireNodeType($childNodeAggregate->nodeTypeName) ); @@ -301,7 +299,6 @@ private function deleteDisallowedNodesWhenChangingNodeType( !$childNodeAggregate->classification->isTethered() && !$this->areNodeTypeConstraintsImposedByParentValid( $newNodeType, - $childNodeAggregate->nodeName, $this->requireNodeType($childNodeAggregate->nodeTypeName) ) ) { From eff68767dcbaca8801cf4f4e3eb95b56be76f672 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 27 Apr 2024 23:04:53 +0200 Subject: [PATCH 05/17] 4150 - Add test cases for renaming varied and scattered node aggregates --- ...odeAggregateName_ConstraintChecks.feature} | 0 .../02-ChangeNodeAggregateName.feature | 135 ++++++++++++++++++ .../ChangeNodeAggregateName.feature | 84 ----------- 3 files changed, 135 insertions(+), 84 deletions(-) rename Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/{01_ChangeNodeAggregateName_ConstraintChecks.feature => 01-ChangeNodeAggregateName_ConstraintChecks.feature} (100%) create mode 100644 Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature delete mode 100644 Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01-ChangeNodeAggregateName_ConstraintChecks.feature similarity index 100% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01-ChangeNodeAggregateName_ConstraintChecks.feature diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature new file mode 100644 index 00000000000..a8507e73c56 --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature @@ -0,0 +1,135 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Change node aggregate name + + As a user of the CR I want to change the name of a node aggregate + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Node': {} + 'Neos.ContentRepository.Testing:NodeWithTetheredChildren': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Node' + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + 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 the active content stream of workspace "live" and dimension space point {"example":"source"} + + 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 | nodeTypeName | originDimensionSpacePoint | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | Neos.ContentRepository.Testing:Node | {"example":"general"} | lady-eleonode-rootford | parent-document | {} | + | nody-mc-nodeface | Neos.ContentRepository.Testing:NodeWithTetheredChildren | {"example":"source"} | sir-david-nodenborough | document | {"tethered": "nodimus-prime"} | + | nodimus-mediocre | Neos.ContentRepository.Testing:Node | {"example":"source"} | nodimus-prime | grandchild-document | {} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"general"} | + And the graph projection is fully up to date + # leave spec as a virtual variant + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"peer"} | + And the graph projection is fully up to date + + Scenario: Rename a child node aggregate with descendants + When the command ChangeNodeAggregateName is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + Then I expect exactly 11 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 10 is of type "NodeAggregateNameWasChanged" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + And I expect the node aggregate "nody-mc-nodeface" to exist + And I expect this node aggregate to be named "renamed-document" + + And I expect the graph projection to consist of exactly 9 nodes + + When I am in the active content stream of workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"general"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node + + When I am in the active content stream of workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in the active content stream of workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in the active content stream of workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node + + Scenario: Rename a scattered node aggregate + Given the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "peer"} | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + When the command ChangeNodeAggregateName is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + Then I expect exactly 12 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 11 is of type "NodeAggregateNameWasChanged" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + And I expect the node aggregate "nody-mc-nodeface" to exist + And I expect this node aggregate to be named "renamed-document" + + And I expect the graph projection to consist of exactly 9 nodes + + When I am in the active content stream of workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"general"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node + + When I am in the active content stream of workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in the active content stream of workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in the active content stream of workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-prime" and node path "renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "renamed-document/tethered/grandchild-document" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature deleted file mode 100644 index e1760070c4e..00000000000 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature +++ /dev/null @@ -1,84 +0,0 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Change node name - - As a user of the CR I want to change the name of a hierarchical relation between two nodes (e.g. in taxonomies) - - Background: - Given using no content dimensions - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Content': [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - 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 the active content stream of workspace "live" and dimension space point {} - - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - - Scenario: Change node name of content node - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Content" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "dog" | - | nodeAggregateClassification | "regular" | - - And the graph projection is fully up to date - When the command "ChangeNodeAggregateName" is executed with payload: - | Key | Value | - | workspaceName | "live" | - | nodeAggregateId | "nody-mc-nodeface" | - | newNodeName | "cat" | - - Then I expect exactly 4 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 3 is of type "NodeAggregateNameWasChanged" with payload: - | Key | Expected | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | newNodeName | "cat" | - - Scenario: Change node name actually updates projection - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Content" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "dog" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - # we read the node initially, to ensure it is filled in the cache (to check whether cache clearing actually works) - When I am in the active content stream of workspace "live" and dimension space point {} - Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} - Then I expect this node to have the following child nodes: - | Name | NodeDiscriminator | - | dog | cs-identifier;nody-mc-nodeface;{} | - - When the command "ChangeNodeAggregateName" is executed with payload: - | Key | Value | - | workspaceName | "live" | - | nodeAggregateId | "nody-mc-nodeface" | - | newNodeName | "cat" | - And the graph projection is fully up to date - - Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} - Then I expect this node to have the following child nodes: - | Name | NodeDiscriminator | - | cat | cs-identifier;nody-mc-nodeface;{} | - From 281efadbbc8b188cb9bb75a3450f21334a7a199e Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 27 Apr 2024 23:10:03 +0200 Subject: [PATCH 06/17] 4150 - Acknowledge that there are already tests for renaming timestamps --- .../Features/NodeTraversal/Timestamps.feature | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index ed4672f8e85..9af70be68dc 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -117,7 +117,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command "ChangeNodeAggregateName" is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | newNodeName | "a-renamed" | And the graph projection is fully up to date @@ -135,7 +135,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeReferences is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | sourceOriginDimensionSpacePoint | {"language": "ch"} | | sourceNodeAggregateId | "a" | | referenceName | "ref" | @@ -161,7 +161,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command ChangeNodeAggregateType was published with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | newNodeTypeName | "Neos.ContentRepository.Testing:SpecialPage" | | strategy | "happypath" | @@ -217,7 +217,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command MoveNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | dimensionSpacePoint | {"language": "ch"} | | relationDistributionStrategy | "gatherSpecializations" | | nodeAggregateId | "a" | @@ -253,7 +253,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command DisableNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | coveredDimensionSpacePoint | {"language": "ch"} | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | @@ -272,7 +272,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T14:00:00+01:00" And the command EnableNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | coveredDimensionSpacePoint | {"language": "ch"} | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | @@ -292,7 +292,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeProperties is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | propertyValues | {"text": "Changed"} | And I execute the findNodeById query for node aggregate id "non-existing" I expect no node to be returned From 591486f491db9bca86776872d1e38db84f1bb6ce Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sun, 28 Apr 2024 12:10:44 +0200 Subject: [PATCH 07/17] 4150 - Move node name from hierarchy relation to node --- .../DoctrineDbalContentGraphProjection.php | 9 ----- .../DoctrineDbalContentGraphSchemaBuilder.php | 2 +- .../Projection/Feature/NodeVariation.php | 3 -- .../Domain/Projection/HierarchyRelation.php | 3 -- .../src/Domain/Projection/NodeRecord.php | 3 ++ .../ProjectionIntegrityViolationDetector.php | 4 +-- .../src/Domain/Repository/ContentGraph.php | 14 ++++---- .../src/Domain/Repository/ContentSubgraph.php | 34 +++++++++---------- .../Repository/ProjectionContentGraph.php | 7 ++-- 9 files changed, 33 insertions(+), 46 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index a3a92150aa8..e857b41f9a5 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -446,7 +446,6 @@ private function createNodeWithHierarchy( $node->relationAnchorPoint, new DimensionSpacePointSet([$dimensionSpacePoint]), $succeedingSibling?->relationAnchorPoint, - $nodeName ); } } @@ -457,7 +456,6 @@ private function createNodeWithHierarchy( * @param NodeRelationAnchorPoint $parentNodeAnchorPoint * @param NodeRelationAnchorPoint $childNodeAnchorPoint * @param NodeRelationAnchorPoint|null $succeedingSiblingNodeAnchorPoint - * @param NodeName|null $relationName * @param ContentStreamId $contentStreamId * @param DimensionSpacePointSet $dimensionSpacePointSet * @throws \Doctrine\DBAL\DBALException @@ -468,7 +466,6 @@ private function connectHierarchy( NodeRelationAnchorPoint $childNodeAnchorPoint, DimensionSpacePointSet $dimensionSpacePointSet, ?NodeRelationAnchorPoint $succeedingSiblingNodeAnchorPoint, - NodeName $relationName = null ): void { foreach ($dimensionSpacePointSet as $dimensionSpacePoint) { $position = $this->getRelationPosition( @@ -485,7 +482,6 @@ private function connectHierarchy( $hierarchyRelation = new HierarchyRelation( $parentNodeAnchorPoint, $childNodeAnchorPoint, - $relationName, $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, @@ -600,7 +596,6 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void INSERT INTO ' . $this->tableNamePrefix . '_hierarchyrelation ( parentnodeanchor, childnodeanchor, - `name`, position, dimensionspacepointhash, subtreetags, @@ -609,7 +604,6 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void SELECT h.parentnodeanchor, h.childnodeanchor, - h.name, h.position, h.dimensionspacepointhash, h.subtreetags, @@ -782,7 +776,6 @@ protected function copyHierarchyRelationToDimensionSpacePoint( $copy = new HierarchyRelation( $newParent, $newChild, - $sourceHierarchyRelation->name, $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, @@ -1002,7 +995,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded INSERT INTO ' . $this->tableNamePrefix . '_hierarchyrelation ( parentnodeanchor, childnodeanchor, - `name`, position, subtreetags, dimensionspacepointhash, @@ -1011,7 +1003,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded SELECT h.parentnodeanchor, h.childnodeanchor, - h.name, h.position, h.subtreetags, :newDimensionSpacePointHash AS dimensionspacepointhash, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 7722ff41f6f..033fc5fdcb6 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -39,6 +39,7 @@ private function createNodeTable(): Table DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotnull(false), DbalSchemaFactory::columnForDimensionSpacePointHash('origindimensionspacepointhash')->setNotnull(false), DbalSchemaFactory::columnForNodeTypeName('nodetypename'), + (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('properties', Type::getType(Types::TEXT)))->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION), (new Column('classification', Type::getType(Types::BINARY)))->setLength(20)->setNotnull(true), (new Column('created', Type::getType(Types::DATETIME_IMMUTABLE)))->setDefault('CURRENT_TIMESTAMP')->setNotnull(true), @@ -56,7 +57,6 @@ private function createNodeTable(): Table private function createHierarchyRelationTable(): Table { $table = new Table($this->tableNamePrefix . '_hierarchyrelation', [ - (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true), diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php index 27b2322512e..c00ccd2d787 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php @@ -107,7 +107,6 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria $hierarchyRelation = new HierarchyRelation( $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, - $sourceNode->nodeName, $event->contentStreamId, $uncoveredDimensionSpacePoint, $uncoveredDimensionSpacePoint->hash, @@ -360,7 +359,6 @@ public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, $peerNode->relationAnchorPoint, new DimensionSpacePointSet([$coveredDimensionSpacePoint]), $peerSucceedingSiblingNode?->relationAnchorPoint, - $sourceNode->nodeName ); } @@ -393,7 +391,6 @@ abstract protected function connectHierarchy( NodeRelationAnchorPoint $childNodeAnchorPoint, DimensionSpacePointSet $dimensionSpacePointSet, ?NodeRelationAnchorPoint $succeedingSiblingNodeAnchorPoint, - NodeName $relationName = null ): void; abstract protected function copyReferenceRelations( diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php index 8bc02f1e7d3..c0016155b15 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php @@ -18,7 +18,6 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @@ -31,7 +30,6 @@ public function __construct( public NodeRelationAnchorPoint $parentNodeAnchor, public NodeRelationAnchorPoint $childNodeAnchor, - public ?NodeName $name, public ContentStreamId $contentStreamId, public DimensionSpacePoint $dimensionSpacePoint, public string $dimensionSpacePointHash, @@ -54,7 +52,6 @@ public function addToDatabase(Connection $databaseConnection, string $tableNameP $databaseConnection->insert($tableNamePrefix . '_hierarchyrelation', [ 'parentnodeanchor' => $this->parentNodeAnchor->value, 'childnodeanchor' => $this->childNodeAnchor->value, - 'name' => $this->name?->value, 'contentstreamid' => $this->contentStreamId->value, 'dimensionspacepointhash' => $this->dimensionSpacePointHash, 'position' => $this->position, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index f9bfb9f6d7d..d18a09f78ef 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -59,6 +59,7 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa 'origindimensionspacepointhash' => $this->originDimensionSpacePointHash, 'properties' => json_encode($this->properties), 'nodetypename' => $this->nodeTypeName->value, + 'name' => $this->nodeName?->value, 'classification' => $this->classification->value, 'lastmodified' => $this->timestamps->lastModified, 'originallastmodified' => $this->timestamps->originalLastModified, @@ -145,6 +146,7 @@ public static function createNewInDatabase( $originDimensionSpacePointHash, $properties, $nodeTypeName, + $nodeName, $classification, $timestamps ) { @@ -156,6 +158,7 @@ public static function createNewInDatabase( 'origindimensionspacepointhash' => $originDimensionSpacePointHash, 'properties' => json_encode($properties), 'nodetypename' => $nodeTypeName->value, + 'name' => $nodeName?->value, 'classification' => $classification->value, 'created' => $timestamps->created, 'originalcreated' => $timestamps->originalCreated, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php index adb394fb6cd..2ed14e52b6f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php @@ -159,7 +159,7 @@ public function tetheredNodesAreNamed(): Result INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint WHERE n.classification = :tethered - AND h.name IS NULL + AND n.name IS NULL GROUP BY n.nodeaggregateid, h.contentstreamid', [ 'tethered' => NodeAggregateClassification::CLASSIFICATION_TETHERED->value @@ -190,7 +190,7 @@ public function subtreeTagsAreInherited(): Result // This could probably be solved with JSON_ARRAY_INTERSECT(JSON_KEYS(ph.subtreetags), JSON_KEYS(h.subtreetags) but unfortunately that's only available with MariaDB 11.2+ according to https://mariadb.com/kb/en/json_array_intersect/ $hierarchyRelationsWithMissingSubtreeTags = $this->client->getConnection()->executeQuery( 'SELECT - ph.name + ph.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 0bbecfb4e7d..ab7ed9bd58a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -140,7 +140,7 @@ public function findRootNodeAggregates( FindRootNodeAggregatesFilter $filter, ): NodeAggregates { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') @@ -164,7 +164,7 @@ public function findNodeAggregatesByType( NodeTypeName $nodeTypeName ): iterable { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') @@ -182,7 +182,7 @@ public function findNodeAggregateById( NodeAggregateId $nodeAggregateId ): ?NodeAggregate { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.childnodeanchor') ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') @@ -248,7 +248,7 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint( ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') @@ -284,7 +284,7 @@ public function findChildNodeAggregateByName( NodeName $name ): ?NodeAggregate { $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId) - ->andWhere('ch.name = :relationName') + ->andWhere('cn.name = :relationName') ->setParameter('relationName', $name->value); return $this->nodeFactory->mapNodeRowsToNodeAggregate( @@ -325,7 +325,7 @@ public function getDimensionSpacePointsOccupiedByChildNodeName( ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)') - ->andWhere('h.name = :nodeName') + ->andWhere('n.name = :nodeName') ->setParameters([ 'parentNodeAggregateId' => $parentNodeAggregateId->value, 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash, @@ -378,7 +378,7 @@ public function getSubgraphs(): array private function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder { return $this->createQueryBuilder() - ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('cn.*, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index c1a278822d7..a4734791b40 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -169,7 +169,7 @@ public function countBackReferences(NodeAggregateId $nodeAggregateId, CountBackR public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') + ->select('n.*, h.subtreetags') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('n.nodeaggregateid = :nodeAggregateId')->setParameter('nodeAggregateId', $nodeAggregateId->value) @@ -182,7 +182,7 @@ public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') + ->select('n.*, h.subtreetags') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $nodeTypeName->value) @@ -197,7 +197,7 @@ public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ch.name, ch.subtreetags') + ->select('pn.*, ch.subtreetags') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') @@ -239,14 +239,14 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('cn.*, h.name, h.subtreetags') + ->select('cn.*, h.subtreetags') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('h.name = :edgeName')->setParameter('edgeName', $nodeName->value); + ->andWhere('cn.name = :nodeName')->setParameter('nodeName', $nodeName->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } @@ -290,7 +290,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi { $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation - ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') @@ -299,7 +299,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('c.*, h.name, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') + ->select('c.*, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') ->from('tree', 'p') ->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = p.relationanchorpoint') ->innerJoin('p', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = h.childnodeanchor') @@ -382,7 +382,7 @@ public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, CountA public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClosestNodeFilter $filter): ?Node { $queryBuilderInitial = $this->createQueryBuilder() - ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') + ->select('n.*, ph.subtreetags, 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') @@ -392,7 +392,7 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') + ->select('pn.*, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') @@ -602,7 +602,7 @@ private function searchPropertyValueStatement(QueryBuilder $queryBuilder, Proper private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter|CountChildNodesFilter $filter): QueryBuilder { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') + ->select('n.*, h.subtreetags') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_node', 'n', 'h.childnodeanchor = n.relationanchorpoint') @@ -627,7 +627,7 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod $sourceTablePrefix = $backReferences ? 'd' : 's'; $destinationTablePrefix = $backReferences ? 's' : 'd'; $queryBuilder = $this->createQueryBuilder() - ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.name, {$destinationTablePrefix}h.subtreetags, r.name AS referencename, r.properties AS referenceproperties") + ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.subtreetags, r.name AS referencename, r.properties AS referenceproperties") ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') ->innerJoin('sh', $this->tableNamePrefix . '_referencerelation', 'r', 'r.nodeanchorpoint = sn.relationanchorpoint') @@ -695,7 +695,7 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') + ->select('n.*, h.subtreetags') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) @@ -727,9 +727,9 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId, FindAncestorNodesFilter|CountAncestorNodesFilter|FindClosestNodeFilter $filter): array { $queryBuilderInitial = $this->createQueryBuilder() - ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') + ->select('n.*, ph.subtreetags, ph.parentnodeanchor') ->from($this->tableNamePrefix . '_node', 'n') - // we need to join with the hierarchy relation, because we need the node name. + // we need to join with the hierarchy relation, because we need the subtree tags. ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') ->innerJoin('ch', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = ch.childnodeanchor') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'n.relationanchorpoint = ph.childnodeanchor') @@ -742,7 +742,7 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId $this->addSubtreeTagConstraints($queryBuilderInitial, 'ch'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') + ->select('pn.*, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') @@ -769,7 +769,7 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate { $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation - ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->tableNamePrefix . '_node', 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') @@ -783,7 +783,7 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('cn.*, h.name, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') + ->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') ->from('tree', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index 02e6b63e5b9..55b43e2a7cc 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -71,7 +71,7 @@ public function findParentNode( : $originDimensionSpacePoint->hash ]; $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT p.*, ph.contentstreamid, ph.name, ph.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node p + 'SELECT p.*, ph.contentstreamid, ph.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node p INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph ON ph.childnodeanchor = p.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch ON ch.parentnodeanchor = p.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_node c ON ch.childnodeanchor = c.relationanchorpoint @@ -102,7 +102,7 @@ public function findNodeInAggregate( DimensionSpacePoint $coveredDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n + 'SELECT n.*, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.nodeaggregateid = :nodeAggregateId @@ -131,7 +131,7 @@ public function findNodeByIds( OriginDimensionSpacePoint $originDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n + 'SELECT n.*, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.nodeaggregateid = :nodeAggregateId @@ -657,7 +657,6 @@ protected function mapRawDataToHierarchyRelation(array $rawData): HierarchyRelat return new HierarchyRelation( NodeRelationAnchorPoint::fromInteger((int)$rawData['parentnodeanchor']), NodeRelationAnchorPoint::fromInteger((int)$rawData['childnodeanchor']), - $rawData['name'] ? NodeName::fromString($rawData['name']) : null, ContentStreamId::fromString($rawData['contentstreamid']), DimensionSpacePoint::fromJsonString($dimensionspacepointRaw), $rawData['dimensionspacepointhash'], From e2bcf5ca9ad9dc3f93cc7eb2c22d63ec822dc928 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sun, 28 Apr 2024 12:11:21 +0200 Subject: [PATCH 08/17] 4150 - Rename node aggregates with copy on write --- .../DoctrineDbalContentGraphProjection.php | 38 +++++++-------- .../Features/NodeTraversal/Timestamps.feature | 48 +++++++++++++++++++ 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index e857b41f9a5..61c99f595a2 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -359,28 +359,22 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event, $eventEnvelope) { - $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h - INNER JOIN ' . $this->tableNamePrefix . '_node n on - h.childnodeanchor = n.relationanchorpoint - SET - h.name = :newName, - n.lastmodified = :lastModified, - n.originallastmodified = :originalLastModified - - WHERE - n.nodeaggregateid = :nodeAggregateId - and h.contentstreamid = :contentStreamId - ', [ - 'newName' => $event->newNodeName->value, - 'nodeAggregateId' => $event->nodeAggregateId->value, - 'contentStreamId' => $event->contentStreamId->value, - 'lastModified' => $eventEnvelope->recordedAt, - 'originalLastModified' => self::initiatingDateTime($eventEnvelope), - ], [ - 'lastModified' => Types::DATETIME_IMMUTABLE, - 'originalLastModified' => Types::DATETIME_IMMUTABLE, - ]); + foreach ($this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( + $event->nodeAggregateId, + $event->contentStreamId, + ) as $anchorPoint) { + $this->updateNodeRecordWithCopyOnWrite( + $event->contentStreamId, + $anchorPoint, + function (NodeRecord $node) use ($event, $eventEnvelope) { + $node->nodeName = $event->newNodeName; + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); + } + ); + } }); } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index 9af70be68dc..3a8a6026792 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -131,6 +131,54 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + Scenario: NodeAggregateNameWasChanged events update last modified timestamps only in the user workspace + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + And the graph projection is fully up to date + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review" | + And the graph projection is fully up to date + And the current date and time is "2023-03-16T14:00:00+01:00" + And the command "ChangeNodeAggregateName" is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | nodeAggregateId | "a" | + | newNodeName | "a-renamed" | + And the graph projection is fully up to date + + And I am in the active content stream of workspace "user-test" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | + + And I am in the active content stream of workspace "user-test" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | + + When I am in the active content stream of workspace "review" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | | | + + When I am in the active content stream of workspace "review" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | | | + + When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | | | + + When I am in the active content stream of workspace "live" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | | | + Scenario: NodeReferencesWereSet events update last modified timestamps When the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeReferences is executed with payload: From e0046f8b4e9171dec443ff62396658b7e4ed317b Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sun, 28 Apr 2024 13:02:18 +0200 Subject: [PATCH 09/17] 4150 - Adjust tethered node name integrity checks --- ...ectionIntegrityViolationDetectionTrait.php | 26 +++++-- .../TetheredNodesAreNamed.feature | 67 +++++++++---------- .../DoctrineDbalContentGraphProjection.php | 10 +-- 3 files changed, 59 insertions(+), 44 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index cb43d749e47..2c0c6bea3e7 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -22,6 +22,7 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap\Helpers\TestingNodeAggregateId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -123,22 +124,35 @@ public function iChangeTheFollowingHierarchyRelationsDimensionSpacePointHash(Tab } /** - * @When /^I change the following hierarchy relation's name:$/ + * @When /^I change the following node's name:$/ * @param TableNode $payloadTable * @throws DBALException */ - public function iChangeTheFollowingHierarchyRelationsEdgeName(TableNode $payloadTable): void + public function iChangeTheFollowingNodesName(TableNode $payloadTable): void { $dataset = $this->transformPayloadTableToDataset($payloadTable); - $record = $this->transformDatasetToHierarchyRelationRecord($dataset); - unset($record['position']); + + $relationAnchorPoint = $this->dbalClient->getConnection()->executeQuery( + 'SELECT n.relationanchorpoint FROM ' . $this->getTableNamePrefix() . '_node n + JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint + WHERE h.contentstreamid = :contentStreamId + AND n.nodeaggregateId = :nodeAggregateId + AND n.origindimensionspacepointhash = :originDimensionSpacePointHash', + [ + 'contentStreamId' => $dataset['contentStreamId'], + 'nodeAggregateId' => $dataset['nodeAggregateId'], + 'originDimensionSpacePointHash' => OriginDimensionSpacePoint::fromArray($dataset['originDimensionSpacePoint'])->hash, + ] + )->fetchOne(); $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_hierarchyrelation', + $this->getTableNamePrefix() . '_node', [ 'name' => $dataset['newName'] ], - $record + [ + 'relationanchorpoint' => $relationAnchorPoint + ] ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature index 3068aaa7edf..6c697e4970a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature @@ -14,48 +14,47 @@ Feature: Run projection integrity violation detection regarding naming of tether And using identifier "default", I define a content repository And I am in content repository "default" And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | + | 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 the active content stream of workspace "live" and dimension space point {"language":"de"} And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {"language":"de"} | + | coveredDimensionSpacePoints | [{"language":"de"}] | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + | nodeAggregateClassification | "regular" | And the graph projection is fully up to date - Scenario: Create node variants of different type + Scenario: Remove tethered node's name When the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nodewyn-tetherton" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "to-be-hacked-to-null" | - | nodeAggregateClassification | "tethered" | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {"language":"de"} | + | coveredDimensionSpacePoints | [{"language":"de"}] | + | parentNodeAggregateId | "sir-david-nodenborough" | + | nodeName | "to-be-hacked-to-null" | + | nodeAggregateClassification | "tethered" | And the graph projection is fully up to date - And I change the following hierarchy relation's name: - | Key | Value | - | contentStreamId | "cs-identifier" | - | dimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "sir-david-nodenborough" | - | childNodeAggregateId | "nodewyn-tetherton" | - | newName | null | + And I change the following node's name: + | Key | Value | + | contentStreamId | "cs-identifier" | + | originDimensionSpacePoint | {"language":"de"} | + | nodeAggregateId | "nodewyn-tetherton" | + | newName | null | And I run integrity violation detection Then I expect the integrity violation detection result to contain exactly 1 errors And I expect integrity violation detection result error number 1 to have code 1597923103 diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 61c99f595a2..0dad54c1a01 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -359,10 +359,12 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event, $eventEnvelope) { - foreach ($this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( - $event->nodeAggregateId, - $event->contentStreamId, - ) as $anchorPoint) { + foreach ( + $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( + $event->nodeAggregateId, + $event->contentStreamId, + ) as $anchorPoint + ) { $this->updateNodeRecordWithCopyOnWrite( $event->contentStreamId, $anchorPoint, From 01c3817c5a471c83796abd6ec570021e0eb064c9 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 13:25:12 +0200 Subject: [PATCH 10/17] 4150 - Adjust NodeQueryBuilder to node names --- .../src/NodeQueryBuilder.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index 9fd82b24be9..1997347d513 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -42,7 +42,7 @@ public function __construct( public function buildBasicNodeAggregateQuery(): QueryBuilder { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNames->node(), 'n') ->innerJoin('n', $this->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') @@ -54,7 +54,7 @@ public function buildBasicNodeAggregateQuery(): QueryBuilder public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder { return $this->createQueryBuilder() - ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('cn.*, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNames->node(), 'pn') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') @@ -86,7 +86,7 @@ public function buildFindRootNodeAggregatesQuery(ContentStreamId $contentStreamI return $queryBuilder; } - public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.name, h.subtreetags'): QueryBuilder + public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.subtreetags'): QueryBuilder { return $this->createQueryBuilder() ->select($select) @@ -99,7 +99,7 @@ public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionS public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') + ->select('n.*, h.subtreetags') ->from($this->tableNames->node(), 'pn') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->node(), 'n', 'h.childnodeanchor = n.relationanchorpoint') @@ -111,7 +111,7 @@ public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() - ->select('pn.*, ch.name, ch.subtreetags') + ->select('pn.*, ch.subtreetags') ->from($this->tableNames->node(), 'pn') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->node(), 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') From f1a0ac527b3eaadc488f89b5028a67257638e9b4 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 13:26:18 +0200 Subject: [PATCH 11/17] 4150 - Adjust findChildNodeAggregateByName to interface (and reality) --- .../src/Domain/Repository/ContentGraph.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 909667d052a..d21056908c7 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -253,7 +253,7 @@ public function findTetheredChildNodeAggregates(NodeAggregateId $parentNodeAggre public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name - ): NodeAggregate { + ): ?NodeAggregate { $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId) ->andWhere('cn.name = :relationName') ->setParameter('relationName', $name->value); @@ -321,7 +321,7 @@ private function createQueryBuilder(): QueryBuilder return $this->client->getConnection()->createQueryBuilder(); } - private function mapQueryBuilderToNodeAggregate(QueryBuilder $queryBuilder): NodeAggregate + private function mapQueryBuilderToNodeAggregate(QueryBuilder $queryBuilder): ?NodeAggregate { return $this->nodeFactory->mapNodeRowsToNodeAggregate( $this->fetchRows($queryBuilder), From e58c81a5498f97c06311e1718fded91d2b9ae3c2 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 13:26:33 +0200 Subject: [PATCH 12/17] Prevent renaming nodes on a closed content stream --- .../Classes/Feature/NodeRenaming/NodeRenaming.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php index c275045c19a..547a0693554 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php @@ -33,6 +33,7 @@ trait NodeRenaming private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $nodeAggregate = $this->requireProjectedNodeAggregate( $contentGraph, From ae737d011df95a620ad30bb055df945501b94b26 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 13:27:00 +0200 Subject: [PATCH 13/17] 4150 - Adjust site handling --- .../Controller/Module/Administration/SitesController.php | 2 +- Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index ebcf45b0831..06828e270da 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -204,7 +204,7 @@ public function updateSiteAction(Site $site, $newSiteNodeName) } foreach ($contentRepository->getWorkspaceFinder()->findAll() as $workspace) { - $siteNodeAggregate = $contentRepository->getContentGraph()->findChildNodeAggregateByName( + $siteNodeAggregate = $contentRepository->getContentGraph(WorkspaceName::forLive())->findChildNodeAggregateByName( $sitesNode->nodeAggregateId, $site->getNodeName()->toNodeName() ); diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index f61eb25a1a1..062e7ea8f18 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -30,6 +30,7 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Neos\Domain\Exception\SiteNodeTypeIsInvalid; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Model\SiteNodeName; @@ -95,7 +96,7 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); } - $siteNodeAggregate = $this->contentRepository->getContentGraph()->findChildNodeAggregateByName( + $siteNodeAggregate = $this->contentRepository->getContentGraph(WorkspaceName::forLive())->findChildNodeAggregateByName( $sitesNodeIdentifier, $site->getNodeName()->toNodeName(), ); From 725aae9313b7261090c922c7f4d7769d7bfadba0 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 13:28:53 +0200 Subject: [PATCH 14/17] 4150 - Adjust node name comments --- .../src/Domain/Projection/NodeRecord.php | 1 - .../Classes/Projection/ContentGraph/Node.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index 6ebe8c10c97..5af86607ea4 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -41,7 +41,6 @@ public function __construct( public SerializedPropertyValues $properties, public NodeTypeName $nodeTypeName, public NodeAggregateClassification $classification, - /** Transient node name to store a node name after fetching a node with hierarchy (not always available) */ public ?NodeName $nodeName, public Timestamps $timestamps, ) { diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php index e89b8aec790..48250a8107c 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php @@ -46,7 +46,7 @@ * @param NodeTypeName $nodeTypeName The node's node type name; always set, even if unknown to the NodeTypeManager * @param NodeType|null $nodeType The node's node type, null if unknown to the NodeTypeManager - @deprecated Don't rely on this too much, as the capabilities of the NodeType here will probably change a lot; Ask the {@see NodeTypeManager} instead * @param PropertyCollection $properties All properties of this node. References are NOT part of this API; To access references, {@see ContentSubgraphInterface::findReferences()} can be used; To read the serialized properties use {@see PropertyCollection::serialized()}. - * @param NodeName|null $nodeName The optionally named hierarchy relation to the node's parent. + * @param NodeName|null $nodeName The optional name of the node, describing its relation to its parent * @param NodeTags $tags explicit and inherited SubtreeTags of this node * @param Timestamps $timestamps Creation and modification timestamps of this node */ From 07f4afde6f4b05c3ff53c389bfdf74b6d84ffc69 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 13:44:54 +0200 Subject: [PATCH 15/17] 4150 - Adjust Integrity violation trait to TableNames --- .../Bootstrap/ProjectionIntegrityViolationDetectionTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index 8f987ef9bc2..873a700ea17 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -133,8 +133,8 @@ public function iChangeTheFollowingNodesName(TableNode $payloadTable): void $dataset = $this->transformPayloadTableToDataset($payloadTable); $relationAnchorPoint = $this->dbalClient->getConnection()->executeQuery( - 'SELECT n.relationanchorpoint FROM ' . $this->getTableNamePrefix() . '_node n - JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint + 'SELECT n.relationanchorpoint FROM ' . $this->tableNames()->node() . ' n + JOIN ' . $this->tableNames()->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE h.contentstreamid = :contentStreamId AND n.nodeaggregateId = :nodeAggregateId AND n.origindimensionspacepointhash = :originDimensionSpacePointHash', From 07f031f92f4a892cab9684637302ff2a46759f92 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 15:02:54 +0200 Subject: [PATCH 16/17] 4150 - Adjust new tests to workspace names --- .../02-ChangeNodeAggregateName.feature | 18 +++++++++--------- .../Features/NodeTraversal/Timestamps.feature | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature index a8507e73c56..457b44cb3bf 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature @@ -24,7 +24,7 @@ Feature: Change node aggregate name | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + And I am in workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -68,22 +68,22 @@ Feature: Change node aggregate name And I expect the graph projection to consist of exactly 9 nodes - When I am in the active content stream of workspace "live" and dimension space point {"example": "general"} + When I am in workspace "live" and dimension space point {"example": "general"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"general"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "source"} + When I am in workspace "live" and dimension space point {"example": "source"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} - When I am in the active content stream of workspace "live" and dimension space point {"example": "spec"} + When I am in workspace "live" and dimension space point {"example": "spec"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} - When I am in the active content stream of workspace "live" and dimension space point {"example": "peer"} + When I am in workspace "live" and dimension space point {"example": "peer"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"peer"} Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"peer"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node @@ -114,22 +114,22 @@ Feature: Change node aggregate name And I expect the graph projection to consist of exactly 9 nodes - When I am in the active content stream of workspace "live" and dimension space point {"example": "general"} + When I am in workspace "live" and dimension space point {"example": "general"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"general"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "source"} + When I am in workspace "live" and dimension space point {"example": "source"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} - When I am in the active content stream of workspace "live" and dimension space point {"example": "spec"} + When I am in workspace "live" and dimension space point {"example": "spec"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} - When I am in the active content stream of workspace "live" and dimension space point {"example": "peer"} + When I am in workspace "live" and dimension space point {"example": "peer"} Then I expect node aggregate identifier "nody-mc-nodeface" and node path "renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"peer"} Then I expect node aggregate identifier "nodimus-prime" and node path "renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"peer"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "renamed-document/tethered/grandchild-document" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index f197b533c52..4487f84325f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -149,32 +149,32 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | newNodeName | "a-renamed" | And the graph projection is fully up to date - And I am in the active content stream of workspace "user-test" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | - And I am in the active content stream of workspace "user-test" and dimension space point {"language":"ch"} + And I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | - When I am in the active content stream of workspace "review" and dimension space point {"language":"de"} + When I am in workspace "review" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | | | - When I am in the active content stream of workspace "review" and dimension space point {"language":"ch"} + When I am in workspace "review" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | | | - When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | | | - When I am in the active content stream of workspace "live" and dimension space point {"language":"ch"} + When I am in workspace "live" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | | | From e07783c6f29dee2c562ea028dcab72d863ba983a Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sat, 11 May 2024 18:20:51 +0200 Subject: [PATCH 17/17] 4150 - use already defined workspace name --- .../Module/Administration/SitesController.php | 2 +- .../Classes/Domain/Service/SiteServiceInternals.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 06828e270da..0eef114fab8 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -204,7 +204,7 @@ public function updateSiteAction(Site $site, $newSiteNodeName) } foreach ($contentRepository->getWorkspaceFinder()->findAll() as $workspace) { - $siteNodeAggregate = $contentRepository->getContentGraph(WorkspaceName::forLive())->findChildNodeAggregateByName( + $siteNodeAggregate = $contentRepository->getContentGraph($workspace->workspaceName)->findChildNodeAggregateByName( $sitesNode->nodeAggregateId, $site->getNodeName()->toNodeName() ); diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index 062e7ea8f18..ae5d75bb1e5 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -30,7 +30,6 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\Neos\Domain\Exception\SiteNodeTypeIsInvalid; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Model\SiteNodeName; @@ -96,10 +95,11 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); } - $siteNodeAggregate = $this->contentRepository->getContentGraph(WorkspaceName::forLive())->findChildNodeAggregateByName( - $sitesNodeIdentifier, - $site->getNodeName()->toNodeName(), - ); + $siteNodeAggregate = $this->contentRepository->getContentGraph($liveWorkspace->workspaceName) + ->findChildNodeAggregateByName( + $sitesNodeIdentifier, + $site->getNodeName()->toNodeName(), + ); if ($siteNodeAggregate instanceof NodeAggregate) { // Site node already exists return;