From ab338d6e1aee927484ffbde74237b83a5cdca097 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Sun, 15 Dec 2024 22:37:46 +0100 Subject: [PATCH] 5054 - WIP: Implement constraint checks for node variation with custom destination --- ...CreateNodeVariant_ConstraintChecks.feature | 104 +++++++++++++++++- .../Command/CreateNodeVariant.php | 18 ++- .../Feature/NodeVariation/NodeVariation.php | 44 +++++++- 3 files changed, 156 insertions(+), 10 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature index 5df29d7fe45..dc98a1ccc1a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature @@ -15,14 +15,24 @@ Feature: Create node variant tethered: type: 'Neos.ContentRepository.Testing:Tethered' 'Neos.ContentRepository.Testing:Tethered': [] + 'Neos.ContentRepository.Testing:RestrictiveDocument': + constraints: + nodeTypes: + 'Neos.ContentRepository.Testing:Document': false + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' + constraints: + nodeTypes: + 'Neos.ContentRepository.Testing:Document': false """ And using identifier "default", I define a content repository And I am in content repository "default" And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | And I am in workspace "live" and dimension space point {"market":"DE", "language":"gsw"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -36,6 +46,15 @@ Feature: Create node variant # We have to add yet another node since we need test cases with a partially covering parent node # Node /document/child | nody-mc-nodeface | child | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | {} | + And I am in workspace "live" and dimension space point {"market":"DE", "language":"de"} + # We have to add yet another node that could be varied but not to a different parent + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | + | polyglot-mc-nodeface | polyglot-child | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | + # ...and we have to add yet another node for node type constraint checks + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | the-governode | governode | lady-eleonode-rootford | Neos.ContentRepository.Testing:RestrictiveDocument | {"tethered": "nodimer-tetherton"} | Scenario: Try to create a variant in a workspace that does not exist When the command CreateNodeVariant is executed with payload and exceptions are caught: @@ -120,3 +139,82 @@ Feature: Create node variant | sourceOrigin | {"market":"DE", "language":"gsw"} | | targetOrigin | {"market":"DE", "language":"de"} | Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint" + + Scenario: Try to create a variant as a child of a different parent aggregate that does not exist + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"DE", "language":"gsw"} | + | parentNodeAggregateId | "i-do-not-exist" | + Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + + Scenario: Try to create a variant as a sibling of a non-existing succeeding sibling + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"DE", "language":"gsw"} | + | parentNodeAggregateId | "nody-mc-nodeface" | + | succeedingSiblingNodeAggregateId | "i-do-not-exist" | + Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + + Scenario: Try to create a variant before a sibling which is not a child of the new parent + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"DE", "language":"gsw"} | + | parentNodeAggregateId | "nody-mc-nodeface" | + | succeedingSiblingNodeAggregateId | "sir-david-nodenborough" | + Then the last command should have thrown an exception of type "NodeAggregateIsNoChild" + + Scenario: Try to create a variant before a sibling which is none (no new parent case) + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"DE", "language":"gsw"} | + | succeedingSiblingNodeAggregateId | "nody-mc-nodeface" | + Then the last command should have thrown an exception of type "NodeAggregateIsNoSibling" + + Scenario: Try to create a variant as a child of a different parent aggregate that does not cover the requested DSP + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"CH", "language":"de"} | + | parentNodeAggregateId | "sir-david-nodenborough" | + Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint" + + Scenario: Try to create a variant of a node having a name that is already taken by one of the variant's siblings + Given I am in workspace "live" and dimension space point {"market":"DE", "language":"gsw"} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | + | evil-occupant | polyglot-child | nody-mc-nodeface | Neos.ContentRepository.Testing:Document | + + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"DE", "language":"gsw"} | + | parentNodeAggregateId | "nody-mc-nodeface" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to vary a node as a child of another parent whose node type does not allow child nodes of the variant's type + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"DE", "language":"gsw"} | + | parentNodeAggregateId | "the-governode" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Try to vary a node as a child of another parent whose parent's node type does not allow grand child nodes of the variant's type + When the command CreateNodeVariant is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "polyglot-mc-nodeface" | + | sourceOrigin | {"market":"DE", "language":"de"} | + | targetOrigin | {"market":"DE", "language":"gsw"} | + | parentNodeAggregateId | "nodimer-tetherton" | + Then the last command should have thrown an exception of type "NodeConstraintException" diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php index 581bfd8b6d8..5eb6c9f1b2d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php @@ -37,12 +37,16 @@ * @param NodeAggregateId $nodeAggregateId The identifier of the affected node aggregate * @param OriginDimensionSpacePoint $sourceOrigin Dimension Space Point from which the node is to be copied from * @param OriginDimensionSpacePoint $targetOrigin Dimension Space Point to which the node is to be copied to + * @param ?NodeAggregateId $parentNodeAggregateId The optional id of the node aggregate to be used as the variant's parent + * @param ?NodeAggregateId $succeedingSiblingNodeAggregateId The optional id of the node aggregate to be used as the variant's succeeding sibling */ private function __construct( public WorkspaceName $workspaceName, public NodeAggregateId $nodeAggregateId, public OriginDimensionSpacePoint $sourceOrigin, public OriginDimensionSpacePoint $targetOrigin, + public ?NodeAggregateId $parentNodeAggregateId, + public ?NodeAggregateId $succeedingSiblingNodeAggregateId, ) { } @@ -51,10 +55,12 @@ private function __construct( * @param NodeAggregateId $nodeAggregateId The identifier of the affected node aggregate * @param OriginDimensionSpacePoint $sourceOrigin Dimension Space Point from which the node is to be copied from * @param OriginDimensionSpacePoint $targetOrigin Dimension Space Point to which the node is to be copied to + * @param ?NodeAggregateId $parentNodeAggregateId The id of the node aggregate to be used as the variant's parent + * @param ?NodeAggregateId $succeedingSiblingNodeAggregateId The optional id of the node aggregate to be used as the variant's succeeding sibling */ - public static function create(WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin): self + public static function create(WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, ?NodeAggregateId $parentNodeAggregateId = null, ?NodeAggregateId $succeedingSiblingNodeAggregateId = null): self { - return new self($workspaceName, $nodeAggregateId, $sourceOrigin, $targetOrigin); + return new self($workspaceName, $nodeAggregateId, $sourceOrigin, $targetOrigin, $parentNodeAggregateId, $succeedingSiblingNodeAggregateId); } public static function fromArray(array $array): self @@ -64,6 +70,12 @@ public static function fromArray(array $array): self NodeAggregateId::fromString($array['nodeAggregateId']), OriginDimensionSpacePoint::fromArray($array['sourceOrigin']), OriginDimensionSpacePoint::fromArray($array['targetOrigin']), + array_key_exists('parentNodeAggregateId', $array) + ? NodeAggregateId::fromString($array['parentNodeAggregateId']) + : null, + array_key_exists('succeedingSiblingNodeAggregateId', $array) + ? NodeAggregateId::fromString($array['succeedingSiblingNodeAggregateId']) + : null, ); } @@ -83,6 +95,8 @@ public function createCopyForWorkspace( $this->nodeAggregateId, $this->sourceOrigin, $this->targetOrigin, + $this->parentNodeAggregateId, + $this->succeedingSiblingNodeAggregateId, ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php index 1e01e281265..e6a0b4ee46d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php @@ -65,11 +65,45 @@ private function handleCreateNodeVariant( $this->requireNodeAggregateToBeUntethered($nodeAggregate); $this->requireNodeAggregateToOccupyDimensionSpacePoint($nodeAggregate, $command->sourceOrigin); $this->requireNodeAggregateToNotOccupyDimensionSpacePoint($nodeAggregate, $command->targetOrigin); - $parentNodeAggregate = $this->requireProjectedParentNodeAggregate( - $contentGraph, - $command->nodeAggregateId, - $command->sourceOrigin - ); + + if ($command->succeedingSiblingNodeAggregateId) { + $this->requireProjectedNodeAggregate($contentGraph, $command->succeedingSiblingNodeAggregateId); + } + if ($command->parentNodeAggregateId) { + $parentNodeAggregate = $this->requireProjectedNodeAggregate($contentGraph, $command->parentNodeAggregateId); + if ($command->succeedingSiblingNodeAggregateId) { + $this->requireNodeAggregateToBeChild( + $contentGraph, + $command->succeedingSiblingNodeAggregateId, + $command->parentNodeAggregateId, + $command->targetOrigin->toDimensionSpacePoint(), + ); + } + if ($nodeAggregate->nodeName) { + $this->requireNodeNameToBeUncovered($contentGraph, $nodeAggregate->nodeName, $command->parentNodeAggregateId); + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($parentNodeAggregate->nodeTypeName, $nodeAggregate->nodeName); + } + $this->requireConstraintsImposedByAncestorsAreMet( + $contentGraph, + $this->requireNodeType($nodeAggregate->nodeTypeName), + [$command->parentNodeAggregateId], + ); + } else { + $parentNodeAggregate = $this->requireProjectedParentNodeAggregate( + $contentGraph, + $command->nodeAggregateId, + $command->sourceOrigin + ); + if ($command->succeedingSiblingNodeAggregateId) { + $this->requireNodeAggregateToBeSibling( + $contentGraph, + $command->nodeAggregateId, + $command->succeedingSiblingNodeAggregateId, + $command->targetOrigin->toDimensionSpacePoint(), + ); + } + } + $this->requireNodeAggregateToCoverDimensionSpacePoint( $parentNodeAggregate, $command->targetOrigin->toDimensionSpacePoint()