From 60313840a75358bee96fdc93e331e0a293a3c120 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Fri, 13 Sep 2024 21:21:14 +0200 Subject: [PATCH 01/11] 4191 - adjust ChangeNodeType test suite structure --- .../01-ChangeNodeAggregateType_ConstraintChecks.feature} | 0 .../02-ChangeNodeAggregateType_DeleteStrategy.feature} | 0 .../03-ChangeNodeAggregateType_HappyPathStrategy.feature} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/{NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature => 11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature} (100%) rename Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/{NodeRetyping/ChangeNodeAggregateType_DeleteStrategy.feature => 11-NodeTypeChange/02-ChangeNodeAggregateType_DeleteStrategy.feature} (100%) rename Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/{NodeRetyping/ChangeNodeAggregateType_HappyPathStrategy.feature => 11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature} (100%) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature similarity index 100% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_DeleteStrategy.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/02-ChangeNodeAggregateType_DeleteStrategy.feature similarity index 100% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_DeleteStrategy.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/02-ChangeNodeAggregateType_DeleteStrategy.feature diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_HappyPathStrategy.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature similarity index 100% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_HappyPathStrategy.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature From c96f0f6135248e6796d496b5a35d6adb857f2762 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Thu, 26 Sep 2024 08:22:09 +0200 Subject: [PATCH 02/11] WIP - adjust test suite --- ...NodeAggregateType_ConstraintChecks.feature | 93 +++++++------------ 1 file changed, 34 insertions(+), 59 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature index 5f344a132a5..636b84c2f62 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature @@ -9,19 +9,23 @@ Feature: Change node aggregate type - basic error cases | language | de, gsw | gsw->de | And using the following node types: """yaml - 'Neos.ContentRepository.Testing:AutoCreated': [] + 'Neos.ContentRepository.Testing:AnotherRoot': + superTypes: + 'Neos.ContentRepository.Testing:Root': true + 'Neos.ContentRepository.Testing:Tethered': [] + 'Neos.ContentRepository.Testing:Simple': [] 'Neos.ContentRepository.Testing:ParentNodeType': childNodes: - autocreated: - type: 'Neos.ContentRepository.Testing:AutoCreated' + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' constraints: nodeTypes: '*': TRUE - 'Neos.ContentRepository.Testing:NodeTypeB': FALSE + 'Neos.ContentRepository.Testing:NodeTypeB': false constraints: nodeTypes: '*': TRUE - 'Neos.ContentRepository.Testing:NodeTypeB': FALSE + 'Neos.ContentRepository.Testing:NodeTypeB': false 'Neos.ContentRepository.Testing:ChildOfNodeTypeA': [] 'Neos.ContentRepository.Testing:ChildOfNodeTypeB': [] 'Neos.ContentRepository.Testing:NodeTypeA': @@ -54,17 +58,13 @@ Feature: Change node aggregate type - basic error cases | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | | nodeTypeName | "Neos.ContentRepository:Root" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | parent | lady-eleonode-rootford | Neos.ContentRepository.Testing:ParentNodeType | {"tethered": "nodewyn-tetherton"} | + | nody-mc-nodeface | | sir-david-nodenborough | Neos.ContentRepository.Testing:Simple | | + | nodimus-prime | | nodewyn-tetherton | Neos.ContentRepository.Testing:Simple | | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "parent" | - | initialPropertyValues | {} | - - - Scenario: Try to change the node aggregate type on a non-existing content stream + Scenario: Try to change the node aggregate type in a workspace that currently does not exist When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | | workspaceName | "non-existing" | @@ -76,11 +76,27 @@ Feature: Change node aggregate type - basic error cases Scenario: Try to change the type on a non-existing node aggregate When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | + | nodeAggregateId | "non-existing" | | newNodeTypeName | "Neos.ContentRepository.Testing:ChildOfNodeTypeA" | | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + Scenario: Try to change the type of a root node aggregate: + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | newNodeTypeName | "Neos.ContentRepository.Testing:AnotherRoot" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeAggregateIsRoot" + + Scenario: Try to change the type of a tethered node aggregate: + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nodewyn-tetherton" | + | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeAggregateIsTethered" + Scenario: Try to change a node aggregate to a non existing type When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | @@ -90,13 +106,6 @@ Feature: Change node aggregate type - basic error cases Then the last command should have thrown an exception of type "NodeTypeNotFound" Scenario: Try to change to a node type disallowed by the parent node - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "parent" | - | initialPropertyValues | {} | When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | @@ -104,45 +113,11 @@ 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 to a node type that is not allowed by the grand parent aggregate inside an autocreated parent aggregate - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "parent2-na" | - | nodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "parent2" | - | initialPropertyValues | {} | - | tetheredDescendantNodeAggregateIds | {"autocreated": "autocreated-child"} | - - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "autocreated-child" | - | initialPropertyValues | {} | + Scenario: Try to change to a node type that is not allowed by the grand parent aggregate inside an tethered parent aggregate When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | + | nodeAggregateId | "nodimus-prime" | | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeB" | | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeConstraintException" - - 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" | - | nodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "parent2" | - | initialPropertyValues | {} | - | tetheredDescendantNodeAggregateIds | {"autocreated": "nody-mc-nodeface"} | - - When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | strategy | "happypath" | - Then the last command should have thrown an exception of type "NodeAggregateIsTethered" From 301ee1f21a90d85e2842c7b70f3f3670fcd62367 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Mon, 21 Oct 2024 18:48:07 +0200 Subject: [PATCH 03/11] 4191 - WIP: Properly run constraint checks on ChangeNodeType --- ...NodeAggregateType_ConstraintChecks.feature | 68 +++- ...odeAggregateType_HappyPathStrategy.feature | 294 ++++++++++++++---- .../Feature/NodeTypeChange/NodeTypeChange.php | 19 +- 3 files changed, 300 insertions(+), 81 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature index 6c20d1b4e16..7c65b11d417 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature @@ -11,9 +11,11 @@ Feature: Change node aggregate type - basic error cases """yaml 'Neos.ContentRepository.Testing:AnotherRoot': superTypes: - 'Neos.ContentRepository.Testing:Root': true + 'Neos.ContentRepository:Root': true 'Neos.ContentRepository.Testing:Tethered': [] 'Neos.ContentRepository.Testing:Simple': [] + 'Neos.ContentRepository.Testing:AbstractNode': + abstract: true 'Neos.ContentRepository.Testing:ParentNodeType': childNodes: tethered: @@ -48,9 +50,9 @@ Feature: Change node aggregate type - basic error cases 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" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | And I am in workspace "live" and dimension space point {"language":"de"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -59,8 +61,8 @@ Feature: Change node aggregate type - basic error cases And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | | sir-david-nodenborough | parent | lady-eleonode-rootford | Neos.ContentRepository.Testing:ParentNodeType | {"tethered": "nodewyn-tetherton"} | - | nody-mc-nodeface | | sir-david-nodenborough | Neos.ContentRepository.Testing:Simple | | - | nodimus-prime | | nodewyn-tetherton | Neos.ContentRepository.Testing:Simple | | + | nody-mc-nodeface | null | sir-david-nodenborough | Neos.ContentRepository.Testing:Simple | {} | + | nodimus-prime | null | nodewyn-tetherton | Neos.ContentRepository.Testing:Simple | {} | Scenario: Try to change the node aggregate type in a workspace that currently does not exist When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: @@ -71,6 +73,18 @@ Feature: Change node aggregate type - basic error cases | strategy | "happypath" | Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" + Scenario: Try to change the node aggregate type in a workspace whose content stream is closed + When the command CloseContentStream is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + And the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | workspaceName | "live" | + | nodeAggregateId | "sir-david-nodenborough" | + | newNodeTypeName | "Neos.ContentRepository.Testing:ChildOfNodeTypeA" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "ContentStreamIsClosed" + Scenario: Try to change the type on a non-existing node aggregate When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | @@ -85,7 +99,7 @@ Feature: Change node aggregate type - basic error cases | nodeAggregateId | "lady-eleonode-rootford" | | newNodeTypeName | "Neos.ContentRepository.Testing:AnotherRoot" | | strategy | "happypath" | - Then the last command should have thrown an exception of type "NodeAggregateIsRoot" + Then the last command should have thrown an exception of type "NodeTypeIsOfTypeRoot" Scenario: Try to change the type of a tethered node aggregate: When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: @@ -103,6 +117,14 @@ Feature: Change node aggregate type - basic error cases | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeTypeNotFound" + Scenario: Try to change a node aggregate to an abstract type + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | newNodeTypeName | "Neos.ContentRepository.Testing:AbstractNode" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeTypeIsAbstract" + Scenario: Try to change to a node type disallowed by the parent node When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | @@ -111,7 +133,22 @@ 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 to a node type that is not allowed by the grand parent aggregate inside an tethered parent aggregate + Scenario: Try to change to a node type disallowed by the parent node in a variant + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"language": "gsw"} | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "scatter" | + And the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeB" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Try to change to a node type that is not allowed by the grand parent aggregate inside a tethered parent aggregate When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | @@ -119,3 +156,18 @@ Feature: Change node aggregate type - basic error cases | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeB" | | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Try to change to a node type that is not allowed by the grand parent aggregate inside a tethered parent aggregate in a variant + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | dimensionSpacePoint | {"language": "gsw"} | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "scatter" | + And the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeB" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeConstraintException" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature index 739cf1b7b2e..e18eee5e6af 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature @@ -3,26 +3,34 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy As a user of the CR I want to change the type of a node aggregate. - # @todo change type to a type with a tethered child with the same name as one of the original one's but of different type Background: Given using the following content dimensions: | Identifier | Values | Generalizations | | language | de, gsw | gsw->de | And using the following node types: """yaml - 'Neos.ContentRepository.Testing:AutoCreated': [] + 'Neos.ContentRepository.Testing:Tethered': [] + 'Neos.ContentRepository.Testing:NodeTypeCCollection': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' + constraints: + nodeTypes: + '*': TRUE + 'Neos.ContentRepository.Testing:NodeTypeA': FALSE + 'Neos.ContentRepository.Testing:NodeTypeB': FALSE 'Neos.ContentRepository.Testing:ParentNodeType': childNodes: - autocreated: - type: 'Neos.ContentRepository.Testing:AutoCreated' + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' constraints: nodeTypes: '*': TRUE 'Neos.ContentRepository.Testing:NodeTypeB': FALSE 'Neos.ContentRepository.Testing:ParentNodeTypeB': childNodes: - autocreated: - type: 'Neos.ContentRepository.Testing:AutoCreated' + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' constraints: nodeTypes: '*': TRUE @@ -31,54 +39,84 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy nodeTypes: '*': TRUE 'Neos.ContentRepository.Testing:NodeTypeA': FALSE - 'Neos.ContentRepository.Testing:ChildOfNodeTypeA': [] - 'Neos.ContentRepository.Testing:ChildOfNodeTypeB': [] + 'Neos.ContentRepository.Testing:ParentNodeTypeC': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:NodeTypeCCollection' + constraints: + nodeTypes: + '*': TRUE + 'Neos.ContentRepository.Testing:NodeTypeA': FALSE + 'Neos.ContentRepository.Testing:GrandParentNodeTypeA': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:ParentNodeType' + 'Neos.ContentRepository.Testing:GrandParentNodeTypeB': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:ParentNodeTypeB' + 'Neos.ContentRepository.Testing:GrandParentNodeTypeC': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:ParentNodeTypeC' + 'Neos.ContentRepository.Testing:ChildOfNodeTypeA': + properties: + defaultTextA: + type: string + defaultValue: 'defaultTextA' + commonDefaultText: + type: string + defaultValue: 'commonDefaultTextA' + 'Neos.ContentRepository.Testing:ChildOfNodeTypeB': + properties: + defaultTextB: + type: string + defaultValue: 'defaultTextB' + commonDefaultText: + type: string + defaultValue: 'commonDefaultTextB' 'Neos.ContentRepository.Testing:NodeTypeA': childNodes: child-of-type-a: type: 'Neos.ContentRepository.Testing:ChildOfNodeTypeA' properties: - text: + defaultTextA: type: string - defaultValue: 'text' + defaultValue: 'defaultTextA' + commonDefaultText: + type: string + defaultValue: 'commonDefaultTextA' 'Neos.ContentRepository.Testing:NodeTypeB': childNodes: child-of-type-b: type: 'Neos.ContentRepository.Testing:ChildOfNodeTypeB' properties: - otherText: + defaultTextB: + type: string + defaultValue: 'defaultTextB' + commonDefaultText: type: string - defaultValue: 'otherText' + defaultValue: 'commonDefaultTextB' """ 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" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | And I am in 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" | - - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "parent" | - | initialPropertyValues | {} | - + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | + | sir-david-nodenborough | {"language":"de"} | lady-eleonode-rootford | parent | Neos.ContentRepository.Testing:ParentNodeType | Scenario: Try to change to a node type that disallows already present children with the HAPPYPATH conflict resolution strategy - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "sir-david-nodenborough" | + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | + | nody-mc-nodeface | {"language":"de"} | sir-david-nodenborough | null | Neos.ContentRepository.Testing:NodeTypeA | When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | @@ -88,22 +126,10 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy Then the last command should have thrown an exception of type "NodeConstraintException" Scenario: Try to change to a node type that disallows already present grandchildren with the HAPPYPATH conflict resolution strategy - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "parent2-na" | - | nodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "parent2" | - | tetheredDescendantNodeAggregateIds | {"autocreated": "autocreated-child"} | - - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "autocreated-child" | - | initialPropertyValues | {} | + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | + | parent2-na | {"language":"de"} | lady-eleonode-rootford | parent2 | Neos.ContentRepository.Testing:ParentNodeType | {} | {"tethered": "nodewyn-tetherton"} | + | nody-mc-nodeface | {"language":"de"} | nodewyn-tetherton | null | Neos.ContentRepository.Testing:NodeTypeA | {} | {} | When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | @@ -112,43 +138,175 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeConstraintException" - Scenario: Change node type successfully - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nodea-identifier-de" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | initialPropertyValues | {} | - | tetheredDescendantNodeAggregateIds | { "child-of-type-a": "child-of-type-a-id"} | + Scenario: Try to change to a node type with a differently typed tethered child that disallows already present (grand)children with the HAPPYPATH conflict resolution strategy + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | parent2 | Neos.ContentRepository.Testing:ParentNodeType | {} | {"tethered": "nodewyn-tetherton"} | + | nodimus-prime | {"language":"de"} | nodewyn-tetherton | null | Neos.ContentRepository.Testing:NodeTypeA | {} | {} | + + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeTypeC" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Try to change to a node type whose tethered child that is also type-changed disallows already present children with the HAPPYPATH conflict resolution strategy (recursive case) + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:GrandParentNodeTypeA | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + | nodingers-cat | {"language": "de"} | nodewyn-tetherton | Neos.ContentRepository.Testing:NodeTypeA | {} | + + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeB" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Try to change to a node type whose tethered child that is also type-changed disallows already present (grand)children with the HAPPYPATH conflict resolution strategy (recursive case) + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:GrandParentNodeTypeA | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + | nodingers-cat | {"language": "de"} | nodimer-tetherton | Neos.ContentRepository.Testing:NodeTypeA | {} | + + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeB" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Try to change to a node type whose tethered child that is also type-changed has a differently typed tethered child that disallows already present grandchildren with the HAPPYPATH conflict resolution strategy (recursive case) + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:GrandParentNodeTypeB | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + | nodingers-cat | {"language": "de"} | nodimer-tetherton | Neos.ContentRepository.Testing:NodeTypeB | {} | + + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeC" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + + Scenario: Change node type with tethered children + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeTypeA | {} | { "child-of-type-a": "nodewyn-tetherton"} | When the command CreateNodeVariant is executed with payload: - | Key | Value | - | nodeAggregateId | "nodea-identifier-de" | - | sourceOrigin | {"language":"de"} | - | targetOrigin | {"language":"gsw"} | + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | When the command ChangeNodeAggregateType is executed with payload: | Key | Value | - | nodeAggregateId | "nodea-identifier-de" | + | nodeAggregateId | "nody-mc-nodeface" | | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeB" | | strategy | "happypath" | - | tetheredDescendantNodeAggregateIds | { "child-of-type-b": "child-of-type-b-id"} | + | tetheredDescendantNodeAggregateIds | { "child-of-type-b": "nodimer-tetherton"} | # the type has changed When I am in workspace "live" and dimension space point {"language":"de"} - Then I expect node aggregate identifier "nodea-identifier-de" to lead to node cs-identifier;nodea-identifier-de;{"language":"de"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" + And I expect this node to have the following properties: + | Key | Value | + # Not modified because it was already present + | commonDefaultText | "commonDefaultTextA" | + | defaultTextB | "defaultTextB" | + # defaultTextA missing because it does not exist in NodeTypeB + + And I expect this node to have the following child nodes: + | Name | NodeDiscriminator | + # the tethered child of the old node type has not been removed with this strategy. + | child-of-type-a | cs-identifier;nodewyn-tetherton;{"language":"de"} | + | child-of-type-b | cs-identifier;nodimer-tetherton;{"language":"de"} | + + And I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to have the following properties: + | Key | Value | + | commonDefaultText | "commonDefaultTextA" | + | defaultTextB | "defaultTextA" | + + And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to have the following properties: + | Key | Value | + | commonDefaultText | "commonDefaultTextB" | + | defaultTextB | "defaultTextB" | When I am in workspace "live" and dimension space point {"language":"gsw"} - Then I expect node aggregate identifier "nodea-identifier-de" to lead to node cs-identifier;nodea-identifier-de;{"language":"gsw"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"gsw"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" - # the old "childOfTypeA" has not been removed with this strategy. And I expect this node to have the following child nodes: - | Name | NodeDiscriminator | - | child-of-type-a | cs-identifier;child-of-type-a-id;{"language":"gsw"} | - | child-of-type-b | cs-identifier;child-of-type-b-id;{"language":"gsw"} | + | Name | NodeDiscriminator | + # the tethered child of the old node type has not been removed with this strategy. + | child-of-type-a | cs-identifier;nodewyn-tetherton;{"language":"gsw"} | + | child-of-type-b | cs-identifier;nodimer-tetherton;{"language":"gsw"} | + + And I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"gsw"} + And I expect this node to have the following properties: + | Key | Value | + | commonDefaultText | "commonDefaultTextA" | + | defaultTextB | "defaultTextA" | + + And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"gsw"} + And I expect this node to have the following properties: + | Key | Value | + | commonDefaultText | "commonDefaultTextB" | + | defaultTextB | "defaultTextB" | + + Scenario: Change node type, recursively also changing the types of tethered descendants + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:ParentNodeType | {} | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + + When the command ChangeNodeAggregateType is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeTypeC" | + | strategy | "happypath" | + + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeC" + And I expect this node to have the following child nodes: + | Name | NodeDiscriminator | + | tethered | cs-identifier;nodewyn-tetherton;{"language":"de"} | + + And I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeC" + And I expect this node to have the following child nodes: + | Name | NodeDiscriminator | + | tethered | cs-identifier;nodimer-tetherton;{"language":"de"} | + + And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodimer-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeCCollection" + + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"gsw"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeC" + And I expect this node to have the following child nodes: + | Name | NodeDiscriminator | + | tethered | cs-identifier;nodewyn-tetherton;{"language":"gsw"} | + + And I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"gsw"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeC" + And I expect this node to have the following child nodes: + | Name | NodeDiscriminator | + | tethered | cs-identifier;nodimer-tetherton;{"language":"gsw"} | + And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodimer-tetherton;{"language":"gsw"} + And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeCCollection" # #missing default property values of target type must be set # #extra properties of source target type must be removed (TBD) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index 0398767e585..39797843b1f 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -39,11 +39,6 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; - -/** @codingStandardsIgnoreStart */ -/** @codingStandardsIgnoreEnd */ /** * @internal implementation detail of Command Handlers @@ -110,6 +105,7 @@ private function handleChangeNodeAggregateType( * Constraint checks **************/ // existence of content stream, node type and node aggregate + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $newNodeType = $this->requireNodeType($command->newNodeTypeName); @@ -121,6 +117,7 @@ private function handleChangeNodeAggregateType( // node type detail checks $this->requireNodeTypeToNotBeOfTypeRoot($newNodeType); + $this->requireNodeTypeToNotBeAbstract($newNodeType); $this->requireTetheredDescendantNodeTypesToExist($newNodeType); $this->requireTetheredDescendantNodeTypesToNotBeOfTypeRoot($newNodeType); @@ -262,6 +259,18 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( $this->requireNodeType($grandchildNodeAggregate->nodeTypeName) ); } + + foreach ($newNodeType->tetheredNodeTypeDefinitions as $tetheredNodeTypeDefinition) { + foreach ($childNodeAggregates as $childNodeAggregate) { + if ($childNodeAggregate->nodeName?->equals($tetheredNodeTypeDefinition->name)) { + $this->requireConstraintsImposedByHappyPathStrategyAreMet( + $contentGraph, + $childNodeAggregate, + $this->requireNodeType($tetheredNodeTypeDefinition->nodeTypeName) + ); + } + } + } } } From 9df594891eb0d04a4df18f8cc5db356be379d64c Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Wed, 23 Oct 2024 16:07:12 +0200 Subject: [PATCH 04/11] 4191 - Properly resolve outgoing hierarchy relations for node --- .../Domain/Repository/ProjectionContentGraph.php | 4 ++-- ...2-RemoveNodeAggregate_WithoutDimensions.feature | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index b8ff909b5cc..b1effb68e62 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -427,7 +427,7 @@ public function findIngoingHierarchyRelationsForNode( } /** - * @return array indexed by the dimension space point hash: ['' => HierarchyRelation, ...] + * @return array */ public function findOutgoingHierarchyRelationsForNode( NodeRelationAnchorPoint $parentAnchorPoint, @@ -461,7 +461,7 @@ public function findOutgoingHierarchyRelationsForNode( } $relations = []; foreach ($rows as $row) { - $relations[(string)$row['dimensionspacepointhash']] = $this->mapRawDataToHierarchyRelation($row); + $relations[] = $this->mapRawDataToHierarchyRelation($row); } return $relations; } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/02-RemoveNodeAggregate_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/02-RemoveNodeAggregate_WithoutDimensions.feature index a3bda47101c..c8651f43cbf 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/02-RemoveNodeAggregate_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/02-RemoveNodeAggregate_WithoutDimensions.feature @@ -122,3 +122,17 @@ Feature: Remove NodeAggregate And I expect node aggregate identifier "nodingers-cat" and node path "pet" to lead to node cs-identifier;nodingers-cat;{} And I expect this node to have no references And I expect node aggregate identifier "nodingers-kitten" and node path "pet/kitten" to lead to no node + + Scenario: Remove a node aggregate with descendants and expect all of them to be gone + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | + | nody-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | child | + | younger-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | younger-child | + When the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeVariantSelectionStrategy | "allVariants" | + + Then I expect node aggregate identifier "sir-david-nodenborough" and node path "document" to lead to no node + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child" to lead to no node + And I expect node aggregate identifier "younger-mc-nodeface" and node path "document/younger-child" to lead to no node From 84b74f3432d4a3c454526d58161a05af602059c1 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Wed, 23 Oct 2024 16:14:07 +0200 Subject: [PATCH 05/11] 4191 - Adjust node type change This - changes node types also recursively for tethered nodes - adds missing tethered nodes after node type change, also recursively - thoroughly cleans up disallowed children, also recursively - adds default values after node type change - removes obsolete property values after node type change --- ...NodeAggregateType_ConstraintChecks.feature | 40 +- ...geNodeAggregateType_DeleteStrategy.feature | 342 +++++++++++++++--- ...odeAggregateType_HappyPathStrategy.feature | 154 +++++++- .../Feature/Common/ConstraintChecks.php | 25 ++ .../Common/NodeTypeChangeInternals.php | 224 ++++++++++++ .../Feature/Common/TetheredNodeInternals.php | 214 +++++++++++ .../Feature/NodeTypeChange/NodeTypeChange.php | 313 ++++++---------- .../Projection/ContentGraph/NodeAggregate.php | 2 +- .../Exception/NodeAggregateIsUntethered.php | 25 ++ .../SharedModel/Node/PropertyNames.php | 7 +- .../Adjustment/TetheredNodeAdjustments.php | 23 ++ .../src/StructureAdjustmentService.php | 2 +- 12 files changed, 1080 insertions(+), 291 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/Feature/Common/NodeTypeChangeInternals.php create mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsUntethered.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature index 7c65b11d417..55148afde37 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/01-ChangeNodeAggregateType_ConstraintChecks.feature @@ -28,6 +28,10 @@ Feature: Change node aggregate type - basic error cases nodeTypes: '*': TRUE 'Neos.ContentRepository.Testing:NodeTypeB': false + 'Neos.ContentRepository.Testing:GrandParentNodeType': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:ParentNodeType' 'Neos.ContentRepository.Testing:ChildOfNodeTypeA': [] 'Neos.ContentRepository.Testing:ChildOfNodeTypeB': [] 'Neos.ContentRepository.Testing:NodeTypeA': @@ -61,8 +65,8 @@ Feature: Change node aggregate type - basic error cases And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | | sir-david-nodenborough | parent | lady-eleonode-rootford | Neos.ContentRepository.Testing:ParentNodeType | {"tethered": "nodewyn-tetherton"} | - | nody-mc-nodeface | null | sir-david-nodenborough | Neos.ContentRepository.Testing:Simple | {} | - | nodimus-prime | null | nodewyn-tetherton | Neos.ContentRepository.Testing:Simple | {} | + | nody-mc-nodeface | null | sir-david-nodenborough | Neos.ContentRepository.Testing:Simple | {} | + | nodimus-prime | null | nodewyn-tetherton | Neos.ContentRepository.Testing:Simple | {} | Scenario: Try to change the node aggregate type in a workspace that currently does not exist When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: @@ -99,7 +103,7 @@ Feature: Change node aggregate type - basic error cases | nodeAggregateId | "lady-eleonode-rootford" | | newNodeTypeName | "Neos.ContentRepository.Testing:AnotherRoot" | | strategy | "happypath" | - Then the last command should have thrown an exception of type "NodeTypeIsOfTypeRoot" + Then the last command should have thrown an exception of type "NodeAggregateIsRoot" Scenario: Try to change the type of a tethered node aggregate: When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: @@ -117,6 +121,14 @@ Feature: Change node aggregate type - basic error cases | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeTypeNotFound" + Scenario: Try to change node aggregate to a root type: + When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | newNodeTypeName | "Neos.ContentRepository.Testing:AnotherRoot" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeTypeIsOfTypeRoot" + Scenario: Try to change a node aggregate to an abstract type When the command ChangeNodeAggregateType is executed with payload and exceptions are caught: | Key | Value | @@ -171,3 +183,25 @@ Feature: Change node aggregate type - basic error cases | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeB" | | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Try to change a node to a type with a tethered node declaration, whose name is already occupied by a non-tethered node + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | + | oddnode-tetherton | tethered | nody-mc-nodeface | Neos.ContentRepository.Testing:Simple | + And the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeAggregateIsUntethered" + + Scenario: Try to change a node to a type with a descendant tethered node declaration, whose name is already occupied by a non-tethered node (recursive case) + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | + | oddnode-tetherton | tethered | nodewyn-tetherton | Neos.ContentRepository.Testing:Simple | + And the command ChangeNodeAggregateType is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeType" | + | strategy | "happypath" | + Then the last command should have thrown an exception of type "NodeAggregateIsUntethered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/02-ChangeNodeAggregateType_DeleteStrategy.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/02-ChangeNodeAggregateType_DeleteStrategy.feature index cc7a91baf52..f73a3a112b7 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/02-ChangeNodeAggregateType_DeleteStrategy.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/02-ChangeNodeAggregateType_DeleteStrategy.feature @@ -9,19 +9,28 @@ Feature: Change node aggregate type - behavior of DELETE strategy | language | de, gsw | gsw->de | And using the following node types: """yaml - 'Neos.ContentRepository.Testing:AutoCreated': [] + 'Neos.ContentRepository.Testing:Tethered': [] + 'Neos.ContentRepository.Testing:NodeTypeCCollection': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' + constraints: + nodeTypes: + '*': TRUE + 'Neos.ContentRepository.Testing:NodeTypeA': FALSE + 'Neos.ContentRepository.Testing:NodeTypeB': FALSE 'Neos.ContentRepository.Testing:ParentNodeType': childNodes: - autocreated: - type: 'Neos.ContentRepository.Testing:AutoCreated' + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' constraints: nodeTypes: '*': TRUE 'Neos.ContentRepository.Testing:NodeTypeB': FALSE 'Neos.ContentRepository.Testing:ParentNodeTypeB': childNodes: - autocreated: - type: 'Neos.ContentRepository.Testing:AutoCreated' + tethered: + type: 'Neos.ContentRepository.Testing:Tethered' constraints: nodeTypes: '*': TRUE @@ -30,6 +39,29 @@ Feature: Change node aggregate type - behavior of DELETE strategy nodeTypes: '*': TRUE 'Neos.ContentRepository.Testing:NodeTypeA': FALSE + 'Neos.ContentRepository.Testing:ParentNodeTypeC': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:NodeTypeCCollection' + constraints: + nodeTypes: + '*': TRUE + 'Neos.ContentRepository.Testing:NodeTypeA': FALSE + properties: + 'parentCText': + defaultValue: 'parentCTextDefault' + 'Neos.ContentRepository.Testing:GrandParentNodeTypeA': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:ParentNodeType' + 'Neos.ContentRepository.Testing:GrandParentNodeTypeB': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:ParentNodeTypeB' + 'Neos.ContentRepository.Testing:GrandParentNodeTypeC': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:ParentNodeTypeC' 'Neos.ContentRepository.Testing:ChildOfNodeTypeA': [] 'Neos.ContentRepository.Testing:ChildOfNodeTypeB': [] 'Neos.ContentRepository.Testing:NodeTypeA': @@ -60,32 +92,22 @@ Feature: Change node aggregate type - behavior of DELETE strategy 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" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | And I am in 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" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | {"language":"de"} | lady-eleonode-rootford | parent | Neos.ContentRepository.Testing:ParentNodeType | {"tethered": "tethered-nodenborough"} | - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "parent" | - | initialPropertyValues | {} | - - - Scenario: Try to change to a node type that disallows already present children with the delete conflict resolution strategy - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "sir-david-nodenborough" | + Scenario: Change to a node type that disallows already present children with the delete conflict resolution strategy + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | + | nody-mc-nodeface | {"language":"de"} | sir-david-nodenborough | parent | Neos.ContentRepository.Testing:NodeTypeA | When the command ChangeNodeAggregateType is executed with payload: | Key | Value | @@ -103,23 +125,11 @@ Feature: Change node aggregate type - behavior of DELETE strategy When I am in workspace "live" and dimension space point {"language":"gsw"} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node - Scenario: Try to change to a node type that disallows already present grandchildren with the delete conflict resolution strategy - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "parent2-na" | - | nodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "parent2" | - | tetheredDescendantNodeAggregateIds | {"autocreated": "autocreated-child"} | - - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "autocreated-child" | - | initialPropertyValues | {} | + Scenario: Change to a node type that disallows already present grandchildren with the delete conflict resolution strategy + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | tetheredDescendantNodeAggregateIds | + | parent2-na | {"language":"de"} | lady-eleonode-rootford | parent2 | Neos.ContentRepository.Testing:ParentNodeType | {"tethered": "tethered-child"} | + | nody-mc-nodeface | {"language":"de"} | tethered-child | null | Neos.ContentRepository.Testing:NodeTypeA | {} | When the command ChangeNodeAggregateType is executed with payload: | Key | Value | @@ -134,25 +144,242 @@ Feature: Change node aggregate type - behavior of DELETE strategy # the child nodes still exist When I am in workspace "live" and dimension space point {"language":"de"} - Then I expect node aggregate identifier "autocreated-child" to lead to node cs-identifier;autocreated-child;{"language":"de"} + Then I expect node aggregate identifier "tethered-child" to lead to node cs-identifier;tethered-child;{"language":"de"} When I am in workspace "live" and dimension space point {"language":"gsw"} - Then I expect node aggregate identifier "autocreated-child" to lead to node cs-identifier;autocreated-child;{"language":"de"} + Then I expect node aggregate identifier "tethered-child" to lead to node cs-identifier;tethered-child;{"language":"de"} # the grandchild nodes have been removed + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node When I am in workspace "live" and dimension space point {"language":"gsw"} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node + Scenario: Change to a node type with a differently typed tethered child that disallows already present (grand)children with the DELETE conflict resolution strategy + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | parent2 | Neos.ContentRepository.Testing:ParentNodeType | {} | {"tethered": "nodewyn-tetherton"} | + | nodimus-prime | {"language":"de"} | nodewyn-tetherton | null | Neos.ContentRepository.Testing:NodeTypeA | {} | {} | + + When the command ChangeNodeAggregateType is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeTypeC" | + | strategy | "delete" | + + # the type has changed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeC" + + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeC" + + # the tethered child nodes still exist and are now properly typed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeCCollection" + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeCCollection" + + # the now disallowed grandchild nodes have been removed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodimus-prime" to lead to no node + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodimus-prime" to lead to no node + + Scenario: Change to a node type whose tethered child that is also type-changed disallows already present children with the DELETE conflict resolution strategy (recursive case) + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:GrandParentNodeTypeA | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + | nodingers-cat | {"language": "de"} | nodewyn-tetherton | Neos.ContentRepository.Testing:NodeTypeA | {} | + + When the command ChangeNodeAggregateType is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeB" | + | strategy | "delete" | + + # the type has changed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeB" + + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeB" + + # the tethered child nodes still exist and are now properly typed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeB" + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeB" + + # the now disallowed grandchild nodes have been removed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodingers-cat" to lead to no node + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodingers-cat" to lead to no node + + Scenario: Change to a node type whose tethered child that is also type-changed disallows already present (grand)children with the DELETE conflict resolution strategy (recursive case) + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:GrandParentNodeTypeA | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + | nodingers-cat | {"language": "de"} | nodimer-tetherton | Neos.ContentRepository.Testing:NodeTypeA | {"child-of-type-a": "a-tetherton"} | + + When the command ChangeNodeAggregateType is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeB" | + | strategy | "delete" | + + # the type has changed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeB" + + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeB" + + # the tethered child nodes still exist and are now properly typed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeB" + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeB" + + # the now disallowed grandchild nodes and their descendants have been removed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodingers-cat" to lead to no node + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodingers-cat" to lead to no node + + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "a-tetherton" to lead to no node + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "a-tetherton" to lead to no node + + Scenario: Change to a node type whose tethered child that is also type-changed has a differently typed tethered child that disallows already present grandchildren with the DELETE conflict resolution strategy (recursive case) + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:GrandParentNodeTypeB | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + | nodingers-cat | {"language": "de"} | nodimer-tetherton | Neos.ContentRepository.Testing:NodeTypeB | {"child-of-type-a": "nodingers-tethered-a", "child-of-type-b": "nodingers-tethered-b"} | + + When the command ChangeNodeAggregateType is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeC" | + | strategy | "delete" | + | tetheredDescendantNodeAggregateIds | {"tethered/tethered/tethered": "nodimus-tetherton"} | + + Then I expect exactly 16 events to be published on stream "ContentStream:cs-identifier" + And event at index 10 is of type "NodeAggregateTypeWasChanged" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeC" | + And event at index 11 is of type "NodeAggregateTypeWasChanged" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | + | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeTypeC" | + And event at index 12 is of type "NodePropertiesWereSet" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | + | originDimensionSpacePoint | {"language":"de"} | + | affectedDimensionSpacePoints | [{"language":"de"},{"language":"gsw"}] | + | propertyValues | {"parentCText":{"value":"parentCTextDefault","type":"string"}} | + | propertyValues | {"parentCText":{"value":"parentCTextDefault","type":"string"}} | + | propertiesToUnset | [] | + And event at index 13 is of type "NodeAggregateWasRemoved" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodingers-cat" | + | affectedOccupiedDimensionSpacePoints | [{"language":"de"},{"language":"gsw"}] | + | affectedCoveredDimensionSpacePoints | [{"language":"de"},{"language":"gsw"}] | + | removalAttachmentPoint | null | + And event at index 14 is of type "NodeAggregateTypeWasChanged" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimer-tetherton" | + | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeCCollection" | + And event at index 15 is of type "NodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimus-tetherton" | + | nodeTypeName | "Neos.ContentRepository.Testing:Tethered" | + | originDimensionSpacePoint | {"language":"de"} | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null}] | + | parentNodeAggregateId | "nodimer-tetherton" | + | nodeName | "tethered" | + | initialPropertyValues | [] | + | nodeAggregateClassification | "tethered" | + + # the type has changed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeC" + + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeC" + + # the tethered child nodes still exist and are now properly typed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeC" + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeC" + + # the tethered grandchild nodes still exist and are now properly typed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodimer-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeCCollection" + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodimer-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeCCollection" + + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodimus-tetherton" to lead to node cs-identifier;nodimus-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:Tethered" + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodimus-tetherton" to lead to node cs-identifier;nodimus-tetherton;{"language":"de"} + And I expect this node to be of type "Neos.ContentRepository.Testing:Tethered" + + # the now disallowed grandchild nodes and their descendants have been removed + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodingers-cat" to lead to no node + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodingers-cat" to lead to no node + + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodingers-tethered-a" to lead to no node + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodingers-tethered-a" to lead to no node + + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect node aggregate identifier "nodingers-tethered-b" to lead to no node + When I am in workspace "live" and dimension space point {"language":"gsw"} + Then I expect node aggregate identifier "nodingers-tethered-b" to lead to no node + Scenario: Change node type successfully - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nodea-identifier-de" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | initialPropertyValues | {} | - | tetheredDescendantNodeAggregateIds | { "child-of-type-a": "child-of-type-a-id"} | + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nodea-identifier-de | {"language":"de"} | lady-eleonode-rootford | null | Neos.ContentRepository.Testing:NodeTypeA | { "child-of-type-a": "child-of-type-a-id"} | When the command CreateNodeVariant is executed with payload: | Key | Value | @@ -181,14 +408,9 @@ Feature: Change node aggregate type - behavior of DELETE strategy | child-of-type-b | cs-identifier;child-of-type-b-id;{"language":"gsw"} | Scenario: When changing node type, a non-allowed tethered node should stay (Tethered nodes are not taken into account when checking constraints) - When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: - | Key | Value | - | nodeAggregateId | "nodea-identifier-de" | - | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeA" | - | originDimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | initialPropertyValues | {} | - | tetheredDescendantNodeAggregateIds | { "child-of-type-a": "child-of-type-a-id"} | + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeName | nodeTypeName | tetheredDescendantNodeAggregateIds | + | nodea-identifier-de | {"language":"de"} | lady-eleonode-rootford | null | Neos.ContentRepository.Testing:NodeTypeA | { "child-of-type-a": "child-of-type-a-id"} | When the command CreateNodeVariant is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature index e18eee5e6af..fcb23133abc 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/11-NodeTypeChange/03-ChangeNodeAggregateType_HappyPathStrategy.feature @@ -47,6 +47,9 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy nodeTypes: '*': TRUE 'Neos.ContentRepository.Testing:NodeTypeA': FALSE + properties: + 'parentCText': + defaultValue: 'parentCTextDefault' 'Neos.ContentRepository.Testing:GrandParentNodeTypeA': childNodes: tethered: @@ -209,12 +212,58 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy | strategy | "happypath" | | tetheredDescendantNodeAggregateIds | { "child-of-type-b": "nodimer-tetherton"} | + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateTypeWasChanged" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:NodeTypeB" | + And event at index 9 is of type "NodePropertiesWereSet" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language":"gsw"} | + | affectedDimensionSpacePoints | [{"language":"gsw"}] | + | propertyValues | {"defaultTextB":{"value":"defaultTextB","type":"string"}} | + | propertiesToUnset | ["defaultTextA"] | + And event at index 10 is of type "NodePropertiesWereSet" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"language":"de"} | + | affectedDimensionSpacePoints | [{"language":"de"}] | + | propertyValues | {"defaultTextB":{"value":"defaultTextB","type":"string"}} | + | propertiesToUnset | ["defaultTextA"] | + And event at index 11 is of type "NodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimer-tetherton" | + | nodeTypeName | "Neos.ContentRepository.Testing:ChildOfNodeTypeB" | + | originDimensionSpacePoint | {"language":"gsw"} | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null}] | + | parentNodeAggregateId | "nody-mc-nodeface" | + | nodeName | "child-of-type-b" | + | initialPropertyValues | {"defaultTextB":{"value":"defaultTextB","type":"string"},"commonDefaultText":{"value":"commonDefaultTextB","type":"string"}} | + | nodeAggregateClassification | "tethered" | + And event at index 12 is of type "NodeGeneralizationVariantWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimer-tetherton" | + | sourceOrigin | {"language":"gsw"} | + | generalizationOrigin | {"language":"de"} | + | variantSucceedingSiblings | [{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null}] | + # the type has changed When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" And I expect this node to have the following properties: - | Key | Value | + | Key | Value | # Not modified because it was already present | commonDefaultText | "commonDefaultTextA" | | defaultTextB | "defaultTextB" | @@ -228,13 +277,13 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy And I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} And I expect this node to have the following properties: - | Key | Value | + | Key | Value | | commonDefaultText | "commonDefaultTextA" | - | defaultTextB | "defaultTextA" | + | defaultTextA | "defaultTextA" | - And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"de"} + And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodimer-tetherton;{"language":"de"} And I expect this node to have the following properties: - | Key | Value | + | Key | Value | | commonDefaultText | "commonDefaultTextB" | | defaultTextB | "defaultTextB" | @@ -250,20 +299,20 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy And I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"gsw"} And I expect this node to have the following properties: - | Key | Value | + | Key | Value | | commonDefaultText | "commonDefaultTextA" | - | defaultTextB | "defaultTextA" | + | defaultTextA | "defaultTextA" | - And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"gsw"} + And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodimer-tetherton;{"language":"gsw"} And I expect this node to have the following properties: - | Key | Value | + | Key | Value | | commonDefaultText | "commonDefaultTextB" | | defaultTextB | "defaultTextB" | Scenario: Change node type, recursively also changing the types of tethered descendants When the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | originDimensionSpacePoint | parentNodeAggregateId | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | - | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:ParentNodeType | {} | {"tethered": "nodewyn-tetherton", "tethered/tethered": "nodimer-tetherton"} | + | nody-mc-nodeface | {"language":"de"} | lady-eleonode-rootford | Neos.ContentRepository.Testing:ParentNodeType | {} | {"tethered": "nodewyn-tetherton"} | When the command CreateNodeVariant is executed with payload: | Key | Value | @@ -272,10 +321,83 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy | targetOrigin | {"language":"gsw"} | When the command ChangeNodeAggregateType is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeC" | + | strategy | "happypath" | + | tetheredDescendantNodeAggregateIds | {"tethered/tethered": "nodimer-tetherton", "tethered/tethered/tethered": "nodimus-tetherton"} | + + Then I expect exactly 16 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateTypeWasChanged" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeTypeName | "Neos.ContentRepository.Testing:GrandParentNodeTypeC" | + And event at index 9 is of type "NodeAggregateTypeWasChanged" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeTypeC" | - | strategy | "happypath" | + And event at index 10 is of type "NodePropertiesWereSet" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | + | originDimensionSpacePoint | {"language":"de"} | + | affectedDimensionSpacePoints | [{"language":"de"}] | + | propertyValues | {"parentCText":{"value":"parentCTextDefault","type":"string"}} | + | propertiesToUnset | [] | + And event at index 11 is of type "NodePropertiesWereSet" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | + | originDimensionSpacePoint | {"language":"gsw"} | + | affectedDimensionSpacePoints | [{"language":"gsw"}] | + | propertyValues | {"parentCText":{"value":"parentCTextDefault","type":"string"}} | + | propertiesToUnset | [] | + And event at index 12 is of type "NodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimer-tetherton" | + | nodeTypeName | "Neos.ContentRepository.Testing:NodeTypeCCollection" | + | originDimensionSpacePoint | {"language":"de"} | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null}] | + | parentNodeAggregateId | "nodewyn-tetherton" | + | nodeName | "tethered" | + | initialPropertyValues | [] | + | nodeAggregateClassification | "tethered" | + And event at index 13 is of type "NodeSpecializationVariantWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimer-tetherton" | + | sourceOrigin | {"language":"de"} | + | specializationOrigin | {"language":"gsw"} | + | specializationSiblings | [{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null}] | + And event at index 14 is of type "NodeAggregateWithNodeWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimus-tetherton" | + | nodeTypeName | "Neos.ContentRepository.Testing:Tethered" | + | originDimensionSpacePoint | {"language":"de"} | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId":null}] | + | parentNodeAggregateId | "nodimer-tetherton" | + | nodeName | "tethered" | + | initialPropertyValues | [] | + | nodeAggregateClassification | "tethered" | + And event at index 15 is of type "NodeSpecializationVariantWasCreated" with payload: + | Key | Expected | + | workspaceName | "live" | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodimus-tetherton" | + | sourceOrigin | {"language":"de"} | + | specializationOrigin | {"language":"gsw"} | + | specializationSiblings | [{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId":null}] | When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"de"} @@ -297,16 +419,14 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"language":"gsw"} And I expect this node to be of type "Neos.ContentRepository.Testing:GrandParentNodeTypeC" And I expect this node to have the following child nodes: - | Name | NodeDiscriminator | + | Name | NodeDiscriminator | | tethered | cs-identifier;nodewyn-tetherton;{"language":"gsw"} | And I expect node aggregate identifier "nodewyn-tetherton" to lead to node cs-identifier;nodewyn-tetherton;{"language":"gsw"} And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeC" And I expect this node to have the following child nodes: - | Name | NodeDiscriminator | + | Name | NodeDiscriminator | | tethered | cs-identifier;nodimer-tetherton;{"language":"gsw"} | And I expect node aggregate identifier "nodimer-tetherton" to lead to node cs-identifier;nodimer-tetherton;{"language":"gsw"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeCCollection" -# #missing default property values of target type must be set -# #extra properties of source target type must be removed (TBD) diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 428ac575ee8..1dccfbe42d2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -45,6 +45,7 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsNoSibling; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsRoot; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsTethered; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsUntethered; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; @@ -194,6 +195,30 @@ protected function requireTetheredDescendantNodeTypesToNotBeOfTypeRoot(NodeType } } + /** + * @throws NodeAggregateIsUntethered + */ + protected function requireExistingDeclaredTetheredDescendantsToBeTethered( + ContentGraphInterface $contentGraph, + NodeAggregate $nodeAggregate, + NodeType $nodeType + ): void { + foreach ($nodeType->tetheredNodeTypeDefinitions as $tetheredNodeTypeDefinition) { + $tetheredNodeAggregate = $contentGraph->findChildNodeAggregateByName($nodeAggregate->nodeAggregateId, $tetheredNodeTypeDefinition->name); + if ($tetheredNodeAggregate === null) { + continue; + } + if (!$tetheredNodeAggregate->classification->isTethered()) { + throw new NodeAggregateIsUntethered( + 'Node name ' . $tetheredNodeTypeDefinition->name->value . ' is occupied by untethered node aggregate ' . $tetheredNodeAggregate->nodeAggregateId->value, + 1729592202 + ); + } + $tetheredNodeType = $this->requireNodeType($tetheredNodeTypeDefinition->nodeTypeName); + $this->requireExistingDeclaredTetheredDescendantsToBeTethered($contentGraph, $tetheredNodeAggregate, $tetheredNodeType); + } + } + protected function requireNodeTypeToDeclareProperty(NodeTypeName $nodeTypeName, PropertyName $propertyName): void { $nodeType = $this->requireNodeType($nodeTypeName); diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeTypeChangeInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeTypeChangeInternals.php new file mode 100644 index 00000000000..31d944fe121 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeTypeChangeInternals.php @@ -0,0 +1,224 @@ +findChildNodeAggregates( + $nodeAggregate->nodeAggregateId + ); + foreach ($childNodeAggregates as $childNodeAggregate) { + /* @var $childNodeAggregate NodeAggregate */ + // the "parent" of the $childNode is $node; so we use $newNodeType + // (the target node type of $node after the operation) here. + if ( + !$childNodeAggregate->classification->isTethered() + && !$this->areNodeTypeConstraintsImposedByParentValid( + $newNodeType, + $this->requireNodeType($childNodeAggregate->nodeTypeName) + ) + // descendants might be disallowed by both parent and grandparent after NodeTypeChange, but must be deleted only once + && !$alreadyRemovedNodeAggregateIds->contain($childNodeAggregate->nodeAggregateId) + ) { + // this aggregate (or parts thereof) are DISALLOWED according to constraints. + // We now need to find out which edges we need to remove, + $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( + $contentGraph, + $nodeAggregate, + $childNodeAggregate + ); + // AND REMOVE THEM + $events[] = $this->removeNodeInDimensionSpacePointSet( + $contentGraph, + $childNodeAggregate, + $dimensionSpacePointsToBeRemoved, + ); + $alreadyRemovedNodeAggregateIds = $alreadyRemovedNodeAggregateIds->merge( + NodeAggregateIds::create($childNodeAggregate->nodeAggregateId) + ); + } + + // we do not need to test for grandparents here, as we did not modify the grandparents. + // Thus, if it was allowed before, it is allowed now. + // additionally, we need to look one level down to the grandchildren as well + // - as it could happen that these are affected by our constraint checks as well. + $grandchildNodeAggregates = $contentGraph->findChildNodeAggregates($childNodeAggregate->nodeAggregateId); + foreach ($grandchildNodeAggregates as $grandchildNodeAggregate) { + // we do not need to test for the parent of grandchild (=child), + // as we do not change the child's node type. + // we however need to check for the grandparent node type. + if ( + $childNodeAggregate->nodeName !== null + && !$this->areNodeTypeConstraintsImposedByGrandparentValid( + $newNodeType, // the grandparent node type changes + $childNodeAggregate->nodeName, + $this->requireNodeType($grandchildNodeAggregate->nodeTypeName) + ) + // descendants might be disallowed by both parent and grandparent after NodeTypeChange, but must be deleted only once + && !$alreadyRemovedNodeAggregateIds->contain($grandchildNodeAggregate->nodeAggregateId) + ) { + // this aggregate (or parts thereof) are DISALLOWED according to constraints. + // We now need to find out which edges we need to remove, + $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( + $contentGraph, + $childNodeAggregate, + $grandchildNodeAggregate + ); + // AND REMOVE THEM + $events[] = $this->removeNodeInDimensionSpacePointSet( + $contentGraph, + $grandchildNodeAggregate, + $dimensionSpacePointsToBeRemoved, + ); + $alreadyRemovedNodeAggregateIds = $alreadyRemovedNodeAggregateIds->merge( + NodeAggregateIds::create($grandchildNodeAggregate->nodeAggregateId) + ); + } + } + } + + return Events::fromArray($events); + } + + private function deleteObsoleteTetheredNodesWhenChangingNodeType( + ContentGraphInterface $contentGraph, + NodeAggregate $nodeAggregate, + NodeType $newNodeType, + NodeAggregateIds &$alreadyRemovedNodeAggregateIds, + ): Events { + $events = []; + // find disallowed tethered nodes + $tetheredNodeAggregates = $contentGraph->findTetheredChildNodeAggregates($nodeAggregate->nodeAggregateId); + + foreach ($tetheredNodeAggregates as $tetheredNodeAggregate) { + /* @var $tetheredNodeAggregate NodeAggregate */ + if ( + $tetheredNodeAggregate->nodeName !== null + && !$newNodeType->tetheredNodeTypeDefinitions->contain($tetheredNodeAggregate->nodeName) + && !$alreadyRemovedNodeAggregateIds->contain($tetheredNodeAggregate->nodeAggregateId) + ) { + // this aggregate (or parts thereof) are DISALLOWED according to constraints. + // We now need to find out which edges we need to remove, + $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( + $contentGraph, + $nodeAggregate, + $tetheredNodeAggregate + ); + // AND REMOVE THEM + $events[] = $this->removeNodeInDimensionSpacePointSet( + $contentGraph, + $tetheredNodeAggregate, + $dimensionSpacePointsToBeRemoved, + ); + $alreadyRemovedNodeAggregateIds = $alreadyRemovedNodeAggregateIds->merge( + NodeAggregateIds::create($tetheredNodeAggregate->nodeAggregateId) + ); + } + } + + return Events::fromArray($events); + } + + /** + * Find all dimension space points which connect two Node Aggregates. + * + * After we found wrong node type constraints between two aggregates, we need to remove exactly the edges where the + * aggregates are connected as parent and child. + * + * Example: In this case, we want to find exactly the bold edge between PAR1 and A. + * + * ╔══════╗ <------ $parentNodeAggregate (PAR1) + * ┌──────┐ ║ PAR1 ║ ┌──────┐ + * │ PAR3 │ ╚══════╝ │ PAR2 │ + * └──────┘ ║ └──────┘ + * ╲ ║ ╱ + * ╲ ║ ╱ + * ▼──▼──┐ ┌───▼─┐ + * │ A │ │ A' │ <------ $childNodeAggregate (A+A') + * └─────┘ └─────┘ + * + * How do we do this? + * - we iterate over each covered dimension space point of the full aggregate + * - in each dimension space point, we check whether the parent node is "our" $nodeAggregate (where + * we originated from) + */ + private function findDimensionSpacePointsConnectingParentAndChildAggregate( + ContentGraphInterface $contentGraph, + NodeAggregate $parentNodeAggregate, + NodeAggregate $childNodeAggregate + ): DimensionSpacePointSet { + $points = []; + foreach ($childNodeAggregate->coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { + $parentNode = $contentGraph->getSubgraph($coveredDimensionSpacePoint, VisibilityConstraints::withoutRestrictions())->findParentNode( + $childNodeAggregate->nodeAggregateId + ); + if ( + $parentNode + && $parentNode->aggregateId->equals($parentNodeAggregate->nodeAggregateId) + ) { + $points[] = $coveredDimensionSpacePoint; + } + } + + return new DimensionSpacePointSet($points); + } + + private function removeNodeInDimensionSpacePointSet( + ContentGraphInterface $contentGraph, + NodeAggregate $nodeAggregate, + DimensionSpacePointSet $coveredDimensionSpacePointsToBeRemoved, + ): NodeAggregateWasRemoved { + return new NodeAggregateWasRemoved( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregate->nodeAggregateId, + // TODO: we also use the covered dimension space points as OCCUPIED dimension space points + // - however the OCCUPIED dimension space points are not really used by now + // (except for the change projector, which needs love anyways...) + OriginDimensionSpacePointSet::fromDimensionSpacePointSet( + $coveredDimensionSpacePointsToBeRemoved + ), + $coveredDimensionSpacePointsToBeRemoved, + ); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php index a8717cb2088..a38dbe78646 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php @@ -15,18 +15,30 @@ */ use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; +use Neos\ContentRepository\Core\DimensionSpace\VariantType; use Neos\ContentRepository\Core\EventStore\Events; +use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; +use Neos\ContentRepository\Core\Feature\NodeTypeChange\Dto\NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy; +use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged; +use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; +use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\NodeType\TetheredNodeTypeDefinition; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\CoverageByOrigin; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; +use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; /** * @internal implementation details of command handlers @@ -148,4 +160,206 @@ protected function createEventsForMissingTetheredNode( $parentNodeAggregate ); } + + protected function createEventsForMissingTetheredNodeAggregate( + ContentGraphInterface $contentGraph, + TetheredNodeTypeDefinition $tetheredNodeTypeDefinition, + OriginDimensionSpacePointSet $affectedOriginDimensionSpacePoints, + CoverageByOrigin $coverageByOrigin, + NodeAggregateId $parentNodeAggregateId, + ?NodeAggregateId $succeedingSiblingNodeAggregateId, + NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePaths, + NodePath $currentNodePath, + ): Events { + $events = []; + $tetheredNodeType = $this->requireNodeType($tetheredNodeTypeDefinition->nodeTypeName); + $nodeAggregateId = $nodeAggregateIdsByNodePaths->getNodeAggregateId($currentNodePath) ?? NodeAggregateId::create(); + $defaultValues = SerializedPropertyValues::defaultFromNodeType( + $tetheredNodeType, + $this->getPropertyConverter() + ); + $creationOrigin = null; + foreach ($affectedOriginDimensionSpacePoints as $originDimensionSpacePoint) { + $coverage = $coverageByOrigin->getCoverage($originDimensionSpacePoint); + if (!$coverage) { + throw new \RuntimeException('Missing coverage for origin dimension space point ' . \json_encode($originDimensionSpacePoint)); + } + $interdimensionalSiblings = InterdimensionalSiblings::fromDimensionSpacePointSetWithSingleSucceedingSiblings( + $coverage, + $succeedingSiblingNodeAggregateId, + ); + $events[] = $creationOrigin + ? match ( + $this->interDimensionalVariationGraph->getVariantType( + $originDimensionSpacePoint->toDimensionSpacePoint(), + $creationOrigin->toDimensionSpacePoint(), + ) + ) { + VariantType::TYPE_SPECIALIZATION => new NodeSpecializationVariantWasCreated( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregateId, + $creationOrigin, + $originDimensionSpacePoint, + $interdimensionalSiblings, + ), + VariantType::TYPE_GENERALIZATION => new NodeGeneralizationVariantWasCreated( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregateId, + $creationOrigin, + $originDimensionSpacePoint, + $interdimensionalSiblings, + ), + default => new NodePeerVariantWasCreated( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregateId, + $creationOrigin, + $originDimensionSpacePoint, + $interdimensionalSiblings, + ), + } + : new NodeAggregateWithNodeWasCreated( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregateId, + $tetheredNodeTypeDefinition->nodeTypeName, + $originDimensionSpacePoint, + $interdimensionalSiblings, + $parentNodeAggregateId, + $tetheredNodeTypeDefinition->name, + $defaultValues, + NodeAggregateClassification::CLASSIFICATION_TETHERED, + ); + + $creationOrigin ??= $originDimensionSpacePoint; + } + + foreach ($tetheredNodeType->tetheredNodeTypeDefinitions as $childTetheredNodeTypeDefinition) { + $events = array_merge( + $events, + iterator_to_array( + $this->createEventsForMissingTetheredNodeAggregate( + $contentGraph, + $childTetheredNodeTypeDefinition, + $affectedOriginDimensionSpacePoints, + $coverageByOrigin, + $nodeAggregateId, + null, + $nodeAggregateIdsByNodePaths, + $currentNodePath->appendPathSegment($childTetheredNodeTypeDefinition->name), + ) + ) + ); + } + + return Events::fromArray($events); + } + + protected function createEventsForWronglyTypedNodeAggregate( + ContentGraphInterface $contentGraph, + NodeAggregate $nodeAggregate, + NodeTypeName $newNodeTypeName, + NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePaths, + NodePath $currentNodePath, + NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy $conflictResolutionStrategy, + NodeAggregateIds $alreadyRemovedNodeAggregateIds, + ): Events { + $events = []; + + $tetheredNodeType = $this->requireNodeType($newNodeTypeName); + + $events[] = new NodeAggregateTypeWasChanged( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregate->nodeAggregateId, + $newNodeTypeName, + ); + + # Handle property adjustments + foreach ($nodeAggregate->getNodes() as $node) { + $presentPropertyKeys = array_keys(iterator_to_array($node->properties->serialized())); + $complementaryPropertyValues = SerializedPropertyValues::defaultFromNodeType( + $tetheredNodeType, + $this->propertyConverter + ) + ->unsetProperties(PropertyNames::fromArray($presentPropertyKeys)); + $obsoletePropertyNames = PropertyNames::fromArray( + array_diff( + $presentPropertyKeys, + array_keys($tetheredNodeType->getProperties()), + ) + ); + + if (count($complementaryPropertyValues->values) > 0 || count($obsoletePropertyNames) > 0) { + $events[] = new NodePropertiesWereSet( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregate->nodeAggregateId, + $node->originDimensionSpacePoint, + $nodeAggregate->getCoverageByOccupant($node->originDimensionSpacePoint), + $complementaryPropertyValues, + $obsoletePropertyNames + ); + } + } + + // remove disallowed nodes + if ($conflictResolutionStrategy === NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE) { + array_push($events, ...iterator_to_array( + $this->deleteDisallowedNodesWhenChangingNodeType( + $contentGraph, + $nodeAggregate, + $tetheredNodeType, + $alreadyRemovedNodeAggregateIds + ) + )); + array_push($events, ...iterator_to_array( + $this->deleteObsoleteTetheredNodesWhenChangingNodeType( + $contentGraph, + $nodeAggregate, + $tetheredNodeType, + $alreadyRemovedNodeAggregateIds + ) + )); + } + + # Handle descendant nodes + foreach ($tetheredNodeType->tetheredNodeTypeDefinitions as $childTetheredNodeTypeDefinition) { + $tetheredChildNodeAggregate = $contentGraph->findChildNodeAggregateByName( + $nodeAggregate->nodeAggregateId, + $childTetheredNodeTypeDefinition->name + ); + if ($tetheredChildNodeAggregate === null) { + $events = array_merge( + $events, + iterator_to_array($this->createEventsForMissingTetheredNodeAggregate( + $contentGraph, + $childTetheredNodeTypeDefinition, + $nodeAggregate->occupiedDimensionSpacePoints, + $nodeAggregate->coverageByOccupant, + $nodeAggregate->nodeAggregateId, + null, + $nodeAggregateIdsByNodePaths, + $currentNodePath->appendPathSegment($childTetheredNodeTypeDefinition->name), + )) + ); + } elseif (!$tetheredChildNodeAggregate->nodeTypeName->equals($childTetheredNodeTypeDefinition->nodeTypeName)) { + $events = array_merge($events, iterator_to_array( + $this->createEventsForWronglyTypedNodeAggregate( + $contentGraph, + $tetheredChildNodeAggregate, + $childTetheredNodeTypeDefinition->nodeTypeName, + $nodeAggregateIdsByNodePaths, + $currentNodePath->appendPathSegment($childTetheredNodeTypeDefinition->name), + $conflictResolutionStrategy, + $alreadyRemovedNodeAggregateIds, + ) + )); + } + } + + return Events::fromArray($events); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index 39797843b1f..6cc77a9d6bb 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -15,36 +15,44 @@ namespace Neos\ContentRepository\Core\Feature\NodeTypeChange; use Neos\ContentRepository\Core\CommandHandlingDependencies; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; +use Neos\ContentRepository\Core\Feature\Common\NodeTypeChangeInternals; +use Neos\ContentRepository\Core\Feature\Common\TetheredNodeInternals; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved; +use Neos\ContentRepository\Core\Feature\NodeCreation\Dto\NodeAggregateIdsByNodePaths; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; +use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Dto\NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\NodeType\TetheredNodeTypeDefinition; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\CoverageByOrigin; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; -use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; /** * @internal implementation detail of Command Handlers */ trait NodeTypeChange { + use TetheredNodeInternals; + use NodeTypeChangeInternals; + abstract protected function getNodeTypeManager(): NodeTypeManager; abstract protected function requireNodeAggregateToBeUntethered(NodeAggregate $nodeAggregate): void; @@ -82,6 +90,27 @@ abstract protected function areNodeTypeConstraintsImposedByGrandparentValid( NodeType $nodeType ): bool; + abstract protected function createEventsForMissingTetheredNodeAggregate( + ContentGraphInterface $contentGraph, + TetheredNodeTypeDefinition $tetheredNodeTypeDefinition, + OriginDimensionSpacePointSet $affectedOriginDimensionSpacePoints, + CoverageByOrigin $coverageByOrigin, + NodeAggregateId $parentNodeAggregateId, + ?NodeAggregateId $succeedingSiblingNodeAggregateId, + NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePaths, + NodePath $currentNodePath, + ): Events; + + abstract protected function createEventsForWronglyTypedNodeAggregate( + ContentGraphInterface $contentGraph, + NodeAggregate $nodeAggregate, + NodeTypeName $newNodeTypeName, + NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePaths, + NodePath $currentNodePath, + NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy $conflictResolutionStrategy, + NodeAggregateIds $alreadyRemovedNodeAggregates, + ): Events; + abstract protected function createEventsForMissingTetheredNode( ContentGraphInterface $contentGraph, NodeAggregate $parentNodeAggregate, @@ -113,6 +142,7 @@ private function handleChangeNodeAggregateType( $contentGraph, $command->nodeAggregateId ); + $this->requireNodeAggregateToNotBeRoot($nodeAggregate); $this->requireNodeAggregateToBeUntethered($nodeAggregate); // node type detail checks @@ -120,6 +150,7 @@ private function handleChangeNodeAggregateType( $this->requireNodeTypeToNotBeAbstract($newNodeType); $this->requireTetheredDescendantNodeTypesToExist($newNodeType); $this->requireTetheredDescendantNodeTypesToNotBeOfTypeRoot($newNodeType); + $this->requireExistingDeclaredTetheredDescendantsToBeTethered($contentGraph, $nodeAggregate, $newNodeType); // the new node type must be allowed at this position in the tree $parentNodeAggregates = $contentGraph->findParentNodeAggregates( @@ -134,13 +165,15 @@ private function handleChangeNodeAggregateType( ); } - /** @codingStandardsIgnoreStart */ match ($command->strategy) { NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_HAPPY_PATH - => $this->requireConstraintsImposedByHappyPathStrategyAreMet($contentGraph, $nodeAggregate, $newNodeType), + => $this->requireConstraintsImposedByHappyPathStrategyAreMet( + $contentGraph, + $nodeAggregate, + $newNodeType + ), NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE => null }; - /** @codingStandardsIgnoreStop */ /************** * Preparation - make the command fully deterministic in case of rebase @@ -161,48 +194,86 @@ private function handleChangeNodeAggregateType( $contentGraph->getWorkspaceName(), $contentGraph->getContentStreamId(), $command->nodeAggregateId, - $command->newNodeTypeName + $command->newNodeTypeName, ), ]; + # Handle property adjustments + $newNodeType = $this->requireNodeType($command->newNodeTypeName); + foreach ($nodeAggregate->getNodes() as $node) { + $presentPropertyKeys = array_keys(iterator_to_array($node->properties->serialized())); + $complementaryPropertyValues = SerializedPropertyValues::defaultFromNodeType( + $newNodeType, + $this->propertyConverter + ) + ->unsetProperties(PropertyNames::fromArray($presentPropertyKeys)); + $obsoletePropertyNames = PropertyNames::fromArray( + array_diff( + $presentPropertyKeys, + array_keys($newNodeType->getProperties()), + ) + ); + + if (count($complementaryPropertyValues->values) > 0 || count($obsoletePropertyNames) > 0) { + $events[] = new NodePropertiesWereSet( + $contentGraph->getWorkspaceName(), + $contentGraph->getContentStreamId(), + $nodeAggregate->nodeAggregateId, + $node->originDimensionSpacePoint, + $nodeAggregate->getCoverageByOccupant($node->originDimensionSpacePoint), + $complementaryPropertyValues, + $obsoletePropertyNames + ); + } + } + // remove disallowed nodes + $alreadyRemovedNodeAggregateIds = NodeAggregateIds::createEmpty(); if ($command->strategy === NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE) { array_push($events, ...iterator_to_array($this->deleteDisallowedNodesWhenChangingNodeType( $contentGraph, $nodeAggregate, - $newNodeType + $newNodeType, + $alreadyRemovedNodeAggregateIds, ))); array_push($events, ...iterator_to_array($this->deleteObsoleteTetheredNodesWhenChangingNodeType( $contentGraph, $nodeAggregate, - $newNodeType + $newNodeType, + $alreadyRemovedNodeAggregateIds ))); } - // new tethered child nodes - foreach ($nodeAggregate->getNodes() as $node) { - assert($node instanceof Node); - foreach ($newNodeType->tetheredNodeTypeDefinitions as $tetheredNodeTypeDefinition) { - $tetheredNode = $contentGraph->getSubgraph( - $node->originDimensionSpacePoint->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - )->findNodeByPath( - $tetheredNodeTypeDefinition->name, - $node->aggregateId, - ); - - if ($tetheredNode === null) { - $tetheredNodeAggregateId = $command->tetheredDescendantNodeAggregateIds - ->getNodeAggregateId(NodePath::fromNodeNames($tetheredNodeTypeDefinition->name)) - ?: NodeAggregateId::create(); - array_push($events, ...iterator_to_array($this->createEventsForMissingTetheredNode( - $contentGraph, - $nodeAggregate, - $node->originDimensionSpacePoint, - $tetheredNodeTypeDefinition, - $tetheredNodeAggregateId - ))); - } + // handle (missing) tethered node aggregates + $nextSibling = null; + $succeedingSiblingIds = []; + foreach (array_reverse(iterator_to_array($newNodeType->tetheredNodeTypeDefinitions)) as $tetheredNodeTypeDefinition) { + $succeedingSiblingIds[$tetheredNodeTypeDefinition->name->value] = $nextSibling; + $nextSibling = $command->tetheredDescendantNodeAggregateIds->getNodeAggregateId(NodePath::fromNodeNames($tetheredNodeTypeDefinition->name)); + } + foreach ($newNodeType->tetheredNodeTypeDefinitions as $tetheredNodeTypeDefinition) { + $tetheredNodeAggregate = $contentGraph->findChildNodeAggregateByName($nodeAggregate->nodeAggregateId, $tetheredNodeTypeDefinition->name); + if ($tetheredNodeAggregate === null) { + $events = array_merge($events, iterator_to_array($this->createEventsForMissingTetheredNodeAggregate( + $contentGraph, + $tetheredNodeTypeDefinition, + $nodeAggregate->occupiedDimensionSpacePoints, + $nodeAggregate->coverageByOccupant, + $nodeAggregate->nodeAggregateId, + $succeedingSiblingIds[$tetheredNodeTypeDefinition->nodeTypeName->value] ?? null, + $command->tetheredDescendantNodeAggregateIds, + NodePath::fromNodeNames($tetheredNodeTypeDefinition->name) + ))); + } elseif (!$tetheredNodeAggregate->nodeTypeName->equals($tetheredNodeTypeDefinition->nodeTypeName)) { + $events = array_merge($events, iterator_to_array($this->createEventsForWronglyTypedNodeAggregate( + $contentGraph, + $tetheredNodeAggregate, + $tetheredNodeTypeDefinition->nodeTypeName, + $command->tetheredDescendantNodeAggregateIds, + NodePath::fromNodeNames($tetheredNodeTypeDefinition->name), + $command->strategy, + $alreadyRemovedNodeAggregateIds + ))); } } @@ -216,7 +287,6 @@ private function handleChangeNodeAggregateType( ); } - /** * NOTE: when changing this method, {@see NodeTypeChange::deleteDisallowedNodesWhenChangingNodeType} * needs to be modified as well (as they are structurally the same) @@ -273,177 +343,4 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( } } } - - /** - * NOTE: when changing this method, {@see NodeTypeChange::requireConstraintsImposedByHappyPathStrategyAreMet} - * needs to be modified as well (as they are structurally the same) - */ - private function deleteDisallowedNodesWhenChangingNodeType( - ContentGraphInterface $contentGraph, - NodeAggregate $nodeAggregate, - NodeType $newNodeType - ): Events { - $events = []; - // if we have children, we need to check whether they are still allowed - // after we changed the node type of the $nodeAggregate to $newNodeType. - $childNodeAggregates = $contentGraph->findChildNodeAggregates( - $nodeAggregate->nodeAggregateId - ); - foreach ($childNodeAggregates as $childNodeAggregate) { - /* @var $childNodeAggregate NodeAggregate */ - // the "parent" of the $childNode is $node; so we use $newNodeType - // (the target node type of $node after the operation) here. - if ( - !$childNodeAggregate->classification->isTethered() - && !$this->areNodeTypeConstraintsImposedByParentValid( - $newNodeType, - $this->requireNodeType($childNodeAggregate->nodeTypeName) - ) - ) { - // this aggregate (or parts thereof) are DISALLOWED according to constraints. - // We now need to find out which edges we need to remove, - $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( - $contentGraph, - $nodeAggregate, - $childNodeAggregate - ); - // AND REMOVE THEM - $events[] = $this->removeNodeInDimensionSpacePointSet( - $contentGraph, - $childNodeAggregate, - $dimensionSpacePointsToBeRemoved, - ); - } - - // we do not need to test for grandparents here, as we did not modify the grandparents. - // Thus, if it was allowed before, it is allowed now. - // additionally, we need to look one level down to the grandchildren as well - // - as it could happen that these are affected by our constraint checks as well. - $grandchildNodeAggregates = $contentGraph->findChildNodeAggregates($childNodeAggregate->nodeAggregateId); - foreach ($grandchildNodeAggregates as $grandchildNodeAggregate) { - /* @var $grandchildNodeAggregate NodeAggregate */ - // we do not need to test for the parent of grandchild (=child), - // as we do not change the child's node type. - // we however need to check for the grandparent node type. - if ( - $childNodeAggregate->nodeName !== null - && !$this->areNodeTypeConstraintsImposedByGrandparentValid( - $newNodeType, // the grandparent node type changes - $childNodeAggregate->nodeName, - $this->requireNodeType($grandchildNodeAggregate->nodeTypeName) - ) - ) { - // this aggregate (or parts thereof) are DISALLOWED according to constraints. - // We now need to find out which edges we need to remove, - $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( - $contentGraph, - $childNodeAggregate, - $grandchildNodeAggregate - ); - // AND REMOVE THEM - $events[] = $this->removeNodeInDimensionSpacePointSet( - $contentGraph, - $grandchildNodeAggregate, - $dimensionSpacePointsToBeRemoved, - ); - } - } - } - - return Events::fromArray($events); - } - - private function deleteObsoleteTetheredNodesWhenChangingNodeType( - ContentGraphInterface $contentGraph, - NodeAggregate $nodeAggregate, - NodeType $newNodeType - ): Events { - $events = []; - // find disallowed tethered nodes - $tetheredNodeAggregates = $contentGraph->findTetheredChildNodeAggregates($nodeAggregate->nodeAggregateId); - - foreach ($tetheredNodeAggregates as $tetheredNodeAggregate) { - /* @var $tetheredNodeAggregate NodeAggregate */ - if ($tetheredNodeAggregate->nodeName !== null && !$newNodeType->tetheredNodeTypeDefinitions->contain($tetheredNodeAggregate->nodeName)) { - // this aggregate (or parts thereof) are DISALLOWED according to constraints. - // We now need to find out which edges we need to remove, - $dimensionSpacePointsToBeRemoved = $this->findDimensionSpacePointsConnectingParentAndChildAggregate( - $contentGraph, - $nodeAggregate, - $tetheredNodeAggregate - ); - // AND REMOVE THEM - $events[] = $this->removeNodeInDimensionSpacePointSet( - $contentGraph, - $tetheredNodeAggregate, - $dimensionSpacePointsToBeRemoved, - ); - } - } - - return Events::fromArray($events); - } - - /** - * Find all dimension space points which connect two Node Aggregates. - * - * After we found wrong node type constraints between two aggregates, we need to remove exactly the edges where the - * aggregates are connected as parent and child. - * - * Example: In this case, we want to find exactly the bold edge between PAR1 and A. - * - * ╔══════╗ <------ $parentNodeAggregate (PAR1) - * ┌──────┐ ║ PAR1 ║ ┌──────┐ - * │ PAR3 │ ╚══════╝ │ PAR2 │ - * └──────┘ ║ └──────┘ - * ╲ ║ ╱ - * ╲ ║ ╱ - * ▼──▼──┐ ┌───▼─┐ - * │ A │ │ A' │ <------ $childNodeAggregate (A+A') - * └─────┘ └─────┘ - * - * How do we do this? - * - we iterate over each covered dimension space point of the full aggregate - * - in each dimension space point, we check whether the parent node is "our" $nodeAggregate (where - * we originated from) - */ - private function findDimensionSpacePointsConnectingParentAndChildAggregate( - ContentGraphInterface $contentGraph, - NodeAggregate $parentNodeAggregate, - NodeAggregate $childNodeAggregate - ): DimensionSpacePointSet { - $points = []; - foreach ($childNodeAggregate->coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { - $parentNode = $contentGraph->getSubgraph($coveredDimensionSpacePoint, VisibilityConstraints::withoutRestrictions())->findParentNode( - $childNodeAggregate->nodeAggregateId - ); - if ( - $parentNode - && $parentNode->aggregateId->equals($parentNodeAggregate->nodeAggregateId) - ) { - $points[] = $coveredDimensionSpacePoint; - } - } - - return new DimensionSpacePointSet($points); - } - - private function removeNodeInDimensionSpacePointSet( - ContentGraphInterface $contentGraph, - NodeAggregate $nodeAggregate, - DimensionSpacePointSet $coveredDimensionSpacePointsToBeRemoved, - ): NodeAggregateWasRemoved { - return new NodeAggregateWasRemoved( - $contentGraph->getWorkspaceName(), - $contentGraph->getContentStreamId(), - $nodeAggregate->nodeAggregateId, - // TODO: we also use the covered dimension space points as OCCUPIED dimension space points - // - however the OCCUPIED dimension space points are not really used by now - // (except for the change projector, which needs love anyways...) - OriginDimensionSpacePointSet::fromDimensionSpacePointSet( - $coveredDimensionSpacePointsToBeRemoved - ), - $coveredDimensionSpacePointsToBeRemoved, - ); - } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php index c949bac0f57..33497841f00 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php @@ -77,7 +77,7 @@ private function __construct( public ?NodeName $nodeName, public OriginDimensionSpacePointSet $occupiedDimensionSpacePoints, private array $nodesByOccupiedDimensionSpacePoint, - private CoverageByOrigin $coverageByOccupant, + public CoverageByOrigin $coverageByOccupant, public DimensionSpacePointSet $coveredDimensionSpacePoints, private array $nodesByCoveredDimensionSpacePoint, private OriginByCoverage $occupationByCovered, diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsUntethered.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsUntethered.php new file mode 100644 index 00000000000..fe0688363cb --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsUntethered.php @@ -0,0 +1,25 @@ + * @api */ -final readonly class PropertyNames implements \IteratorAggregate, \JsonSerializable +final readonly class PropertyNames implements \IteratorAggregate, \Countable, \JsonSerializable { /** * @var array @@ -58,4 +58,9 @@ public function getIterator(): \Traversable { yield from $this->values; } + + public function count(): int + { + return count($this->values); + } } diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php index 95a7e5336a5..9d985cc2399 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php @@ -5,21 +5,25 @@ namespace Neos\ContentRepository\StructureAdjustment\Adjustment; use Neos\ContentRepository\Core\DimensionSpace; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSibling; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; +use Neos\ContentRepository\Core\Feature\Common\NodeTypeChangeInternals; use Neos\ContentRepository\Core\Feature\Common\NodeVariationInternals; use Neos\ContentRepository\Core\Feature\Common\TetheredNodeInternals; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; +use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Model\EventStream\ExpectedVersion; @@ -29,6 +33,7 @@ class TetheredNodeAdjustments use NodeVariationInternals; use RemoveNodeAggregateTrait; use TetheredNodeInternals; + use NodeTypeChangeInternals; public function __construct( private readonly ContentGraphInterface $contentGraph, @@ -189,6 +194,14 @@ private function ensureNodeIsOfType(Node $node, NodeTypeName $expectedNodeTypeNa } } + protected function requireNodeType(NodeTypeName $nodeTypeName): NodeType + { + return $this->nodeTypeManager->getNodeType($nodeTypeName) ?? throw new NodeTypeNotFound( + 'Node type "' . $nodeTypeName->value . '" is unknown to the node type manager.', + 1729600849 + ); + } + protected function getInterDimensionalVariationGraph(): DimensionSpace\InterDimensionalVariationGraph { return $this->interDimensionalVariationGraph; @@ -248,4 +261,14 @@ private function reorderNodes( ExpectedVersion::ANY() ); } + + protected function getNodeTypeManager(): NodeTypeManager + { + return $this->nodeTypeManager; + } + + protected function getAllowedDimensionSubspace(): DimensionSpacePointSet + { + return $this->interDimensionalVariationGraph->getDimensionSpacePoints(); + } } diff --git a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php index 6012fe31428..bf48a9d602f 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php +++ b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php @@ -51,7 +51,7 @@ public function __construct( $this->liveContentGraph, $nodeTypeManager, $interDimensionalVariationGraph, - $propertyConverter + $propertyConverter, ); $this->unknownNodeTypeAdjustment = new UnknownNodeTypeAdjustment( From 7ad90e7419403c6f835c1e1584b75222129bc14d Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Wed, 23 Oct 2024 16:46:44 +0200 Subject: [PATCH 06/11] 4191 - WIP: Add NodeAggregateName/TypeWasChanged to ChangeProjection --- .../PendingChangesProjection/Change.php | 3 +- .../ChangeProjection.php | 86 +++++++++++++++++-- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/Neos.Neos/Classes/PendingChangesProjection/Change.php b/Neos.Neos/Classes/PendingChangesProjection/Change.php index acee1cda5e6..467cc2c1a82 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/Change.php +++ b/Neos.Neos/Classes/PendingChangesProjection/Change.php @@ -36,7 +36,8 @@ final class Change public function __construct( public ContentStreamId $contentStreamId, public NodeAggregateId $nodeAggregateId, - public OriginDimensionSpacePoint $originDimensionSpacePoint, + // null for aggregate scoped changes (e.g. NodeAggregateNameWasChanged, NodeAggregateTypeWasChanged) + public ?OriginDimensionSpacePoint $originDimensionSpacePoint, public bool $created, public bool $changed, public bool $moved, diff --git a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php index 22cd97b9e7f..30f47541e28 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php +++ b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php @@ -30,6 +30,8 @@ use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved; +use Neos\ContentRepository\Core\Feature\NodeRenaming\Event\NodeAggregateNameWasChanged; +use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; @@ -124,12 +126,12 @@ private function determineRequiredSqlStatements(): array (new Column('created', Type::getType(Types::BOOLEAN)))->setNotnull(true), (new Column('changed', Type::getType(Types::BOOLEAN)))->setNotnull(true), (new Column('moved', Type::getType(Types::BOOLEAN)))->setNotnull(true), - DbalSchemaFactory::columnForNodeAggregateId('nodeAggregateId')->setNotNull(true), - DbalSchemaFactory::columnForDimensionSpacePoint('originDimensionSpacePoint')->setNotNull(false), - DbalSchemaFactory::columnForDimensionSpacePointHash('originDimensionSpacePointHash')->setNotNull(true), + DbalSchemaFactory::columnForNodeAggregateId('nodeAggregateId')->setNotnull(true), + DbalSchemaFactory::columnForDimensionSpacePoint('originDimensionSpacePoint')->setNotnull(false), + DbalSchemaFactory::columnForDimensionSpacePointHash('originDimensionSpacePointHash')->setNotnull(false), (new Column('deleted', Type::getType(Types::BOOLEAN)))->setNotnull(true), // Despite the name suggesting this might be an anchor point of sorts, this is a nodeAggregateId type - DbalSchemaFactory::columnForNodeAggregateId('removalAttachmentPoint')->setNotNull(false) + DbalSchemaFactory::columnForNodeAggregateId('removalAttachmentPoint')->setNotnull(false) ]); $changeTable->setPrimaryKey([ @@ -167,7 +169,9 @@ public function canHandle(EventInterface $event): bool DimensionSpacePointWasMoved::class, NodeGeneralizationVariantWasCreated::class, NodeSpecializationVariantWasCreated::class, - NodePeerVariantWasCreated::class + NodePeerVariantWasCreated::class, + NodeAggregateTypeWasChanged::class, + NodeAggregateNameWasChanged::class, ]); } @@ -185,6 +189,8 @@ public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void NodeSpecializationVariantWasCreated::class => $this->whenNodeSpecializationVariantWasCreated($event), NodeGeneralizationVariantWasCreated::class => $this->whenNodeGeneralizationVariantWasCreated($event), NodePeerVariantWasCreated::class => $this->whenNodePeerVariantWasCreated($event), + NodeAggregateTypeWasChanged::class => $this->whenNodeAggregateTypeWasChanged($event), + NodeAggregateNameWasChanged::class => $this->whenNodeAggregateNameWasChanged($event), default => throw new \InvalidArgumentException(sprintf('Unsupported event %s', get_debug_type($event))), }; } @@ -404,6 +410,28 @@ private function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event) ); } + private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event): void + { + if ($event->workspaceName->isLive()) { + return; + } + $this->markAggregateAsChanged( + $event->contentStreamId, + $event->nodeAggregateId, + ); + } + + private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event): void + { + if ($event->workspaceName->isLive()) { + return; + } + $this->markAggregateAsChanged( + $event->contentStreamId, + $event->nodeAggregateId, + ); + } + private function markAsChanged( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, @@ -419,6 +447,19 @@ static function (Change $change) { ); } + private function markAggregateAsChanged( + ContentStreamId $contentStreamId, + NodeAggregateId $nodeAggregateId, + ): void { + $this->modifyChangeForAggregate( + $contentStreamId, + $nodeAggregateId, + static function (Change $change) { + $change->changed = true; + } + ); + } + private function markAsCreated( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, @@ -468,6 +509,23 @@ private function modifyChange( } } + private function modifyChangeForAggregate( + ContentStreamId $contentStreamId, + NodeAggregateId $nodeAggregateId, + callable $modifyFn + ): void { + $change = $this->getChangeForAggregate($contentStreamId, $nodeAggregateId); + + if ($change === null) { + $change = new Change($contentStreamId, $nodeAggregateId, null, false, false, false, false); + $modifyFn($change); + $change->addToDatabase($this->dbal, $this->tableNamePrefix); + } else { + $modifyFn($change); + $change->updateToDatabase($this->dbal, $this->tableNamePrefix); + } + } + private function getChange( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, @@ -488,4 +546,22 @@ private function getChange( // We always allow root nodes return $changeRow ? Change::fromDatabaseRow($changeRow) : null; } + + private function getChangeForAggregate( + ContentStreamId $contentStreamId, + NodeAggregateId $nodeAggregateId, + ): ?Change { + $changeRow = $this->dbal->executeQuery( + 'SELECT n.* FROM ' . $this->tableNamePrefix . ' n +WHERE n.contentStreamId = :contentStreamId +AND n.nodeAggregateId = :nodeAggregateId +AND n.origindimensionspacepointhash = NULL', + [ + 'contentStreamId' => $contentStreamId->value, + 'nodeAggregateId' => $nodeAggregateId->value, + ] + )->fetchAssociative(); + + return $changeRow ? Change::fromDatabaseRow($changeRow) : null; + } } From 4fe6b983a32bf559c6a78f5e74038c4d9de16a78 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Wed, 23 Oct 2024 18:55:42 +0200 Subject: [PATCH 07/11] 4191 - Adjust to aggregate scoped changes --- ...gregateWithNodeAndSerializedProperties.php | 2 +- .../Command/DisableNodeAggregate.php | 2 +- .../Command/CopyNodesRecursively.php | 2 +- .../Command/SetSerializedNodeProperties.php | 2 +- .../Command/SetSerializedNodeReferences.php | 2 +- .../Command/CreateNodeVariant.php | 2 +- .../SubtreeTagging/Command/TagSubtree.php | 2 +- .../SubtreeTagging/Command/UntagSubtree.php | 2 +- .../Dto/NodeIdToPublishOrDiscard.php | 7 ++- .../CatchUpHook/AssetUsageCatchUpHook.php | 4 ++ .../Service/WorkspacePublishingService.php | 47 +++++++++++++------ .../PendingChangesProjection/Change.php | 12 +++-- .../ChangeProjection.php | 2 +- .../Controller/WorkspaceController.php | 8 +++- 14 files changed, 64 insertions(+), 32 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php index f295cd6dabe..79f5412eaa5 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/Command/CreateNodeAggregateWithNodeAndSerializedProperties.php @@ -157,7 +157,7 @@ public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return ( $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) - && $this->originDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint) + && $nodeIdToPublish->dimensionSpacePoint?->equals($this->originDimensionSpacePoint) ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php index 454b7c2ccb2..f92f938df9c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Command/DisableNodeAggregate.php @@ -84,7 +84,7 @@ public function jsonSerialize(): array public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return ( - $this->coveredDimensionSpacePoint === $nodeIdToPublish->dimensionSpacePoint + $this->coveredDimensionSpacePoint === $nodeIdToPublish->dimensionSpacePoint && $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php index 52355fd5e4a..ba31a07c601 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/Command/CopyNodesRecursively.php @@ -133,7 +133,7 @@ public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool ); return ( !is_null($targetNodeAggregateId) - && $this->targetDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint) + && $nodeIdToPublish->dimensionSpacePoint?->equals($this->targetDimensionSpacePoint) && $targetNodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index ca589337e85..a2d684f6035 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -103,7 +103,7 @@ public function jsonSerialize(): array public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return ( - $this->originDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint) + $nodeIdToPublish->dimensionSpacePoint?->equals($this->originDimensionSpacePoint) && $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php index 3f635af08e0..1edf8409040 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/Command/SetSerializedNodeReferences.php @@ -91,7 +91,7 @@ public function jsonSerialize(): array public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { - return ($this->sourceOriginDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint) + return ($nodeIdToPublish->dimensionSpacePoint?->equals($this->sourceOriginDimensionSpacePoint) && $this->sourceNodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php index 001f9bd66e9..54873f489dc 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php @@ -84,7 +84,7 @@ public function jsonSerialize(): array public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) - && $this->targetOrigin->equals($nodeIdToPublish->dimensionSpacePoint); + && $nodeIdToPublish->dimensionSpacePoint?->equals($this->targetOrigin); } public function createCopyForWorkspace( diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php index 71fb012b0cc..d4d37c8b8cb 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php @@ -92,7 +92,7 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) - && $this->coveredDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint); + && $nodeIdToPublish->dimensionSpacePoint === $this->coveredDimensionSpacePoint; } /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php index cca49333c95..1ae9b4624a2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php @@ -93,7 +93,7 @@ public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName): self public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool { return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) - && $this->coveredDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint); + && $this->coveredDimensionSpacePoint === $nodeIdToPublish->dimensionSpacePoint; } /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Dto/NodeIdToPublishOrDiscard.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Dto/NodeIdToPublishOrDiscard.php index a9a7e1339e2..b2b5c346be7 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Dto/NodeIdToPublishOrDiscard.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspacePublication/Dto/NodeIdToPublishOrDiscard.php @@ -30,7 +30,8 @@ { public function __construct( public NodeAggregateId $nodeAggregateId, - public DimensionSpacePoint $dimensionSpacePoint, + /** Can be null for aggregate scoped changes, e.g. ChangeNodeAggregateName or ChangeNodeAggregateName */ + public ?DimensionSpacePoint $dimensionSpacePoint, ) { } @@ -41,7 +42,9 @@ public static function fromArray(array $array): self { return new self( NodeAggregateId::fromString($array['nodeAggregateId']), - DimensionSpacePoint::fromArray($array['dimensionSpacePoint']), + is_array($array['dimensionSpacePoint'] ?? null) + ? DimensionSpacePoint::fromArray($array['dimensionSpacePoint']) + : null, ); } diff --git a/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php index e240adb41bc..7867326e1ea 100644 --- a/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php +++ b/Neos.Neos/Classes/AssetUsage/CatchUpHook/AssetUsageCatchUpHook.php @@ -146,6 +146,10 @@ private function discardWorkspace(WorkspaceName $workspaceName): void private function discardNodes(WorkspaceName $workspaceName, NodeIdsToPublishOrDiscard $nodeIds): void { foreach ($nodeIds as $nodeId) { + if (!$nodeId->dimensionSpacePoint) { + // NodeAggregateTypeWasChanged and NodeAggregateNameWasChanged don't impact asset usage + continue; + } $this->assetUsageIndexingService->removeIndexForWorkspaceNameNodeAggregateIdAndDimensionSpacePoint( $this->contentRepository->id, $workspaceName, diff --git a/Neos.Neos/Classes/Domain/Service/WorkspacePublishingService.php b/Neos.Neos/Classes/Domain/Service/WorkspacePublishingService.php index 31836d383d2..cb27d2b92db 100644 --- a/Neos.Neos/Classes/Domain/Service/WorkspacePublishingService.php +++ b/Neos.Neos/Classes/Domain/Service/WorkspacePublishingService.php @@ -26,9 +26,11 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace as ContentRepositoryWorkspace; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist; @@ -339,7 +341,7 @@ private function resolveNodeIdsToPublishOrDiscard( $nodeIdsToPublishOrDiscard[] = new NodeIdToPublishOrDiscard( $change->nodeAggregateId, - $change->originDimensionSpacePoint->toDimensionSpacePoint() + $change->originDimensionSpacePoint?->toDimensionSpacePoint() ); } @@ -358,7 +360,6 @@ private function countPendingWorkspaceChangesInternal(ContentRepository $content return $contentRepository->projectionState(ChangeFinder::class)->countByContentStreamId($crWorkspace->currentContentStreamId); } - private function isChangePublishableWithinAncestorScope( ContentRepository $contentRepository, WorkspaceName $workspaceName, @@ -374,20 +375,38 @@ private function isChangePublishableWithinAncestorScope( } } - $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph( - $change->originDimensionSpacePoint->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); + if ($change->originDimensionSpacePoint) { + $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph( + $change->originDimensionSpacePoint->toDimensionSpacePoint(), + VisibilityConstraints::withoutRestrictions() + ); - // A Change is publishable if the respective node (or the respective - // removal attachment point) has a closest ancestor that matches our - // current ancestor scope (Document/Site) - $actualAncestorNode = $subgraph->findClosestNode( - $change->removalAttachmentPoint ?? $change->nodeAggregateId, - FindClosestNodeFilter::create(nodeTypes: $ancestorNodeTypeName->value) - ); + // A Change is publishable if the respective node (or the respective + // removal attachment point) has a closest ancestor that matches our + // current ancestor scope (Document/Site) + $actualAncestorNode = $subgraph->findClosestNode( + $change->removalAttachmentPoint ?? $change->nodeAggregateId, + FindClosestNodeFilter::create(nodeTypes: $ancestorNodeTypeName->value) + ); + + return $actualAncestorNode?->aggregateId->equals($ancestorId) ?? false; + } else { + return $this->findAncestorAggregateIds( + $contentRepository->getContentGraph($workspaceName), + $change->nodeAggregateId + )->contain($ancestorId); + } + } + + private function findAncestorAggregateIds(ContentGraphInterface $contentGraph, NodeAggregateId $descendantNodeAggregateId): NodeAggregateIds + { + $nodeAggregateIds = NodeAggregateIds::create($descendantNodeAggregateId); + foreach ($contentGraph->findParentNodeAggregates($descendantNodeAggregateId) as $parentNodeAggregate) { + $nodeAggregateIds = $nodeAggregateIds->merge(NodeAggregateIds::create($parentNodeAggregate->nodeAggregateId)); + $nodeAggregateIds = $nodeAggregateIds->merge($this->findAncestorAggregateIds($contentGraph, $parentNodeAggregate->nodeAggregateId)); + } - return $actualAncestorNode?->aggregateId->equals($ancestorId) ?? false; + return $nodeAggregateIds; } /** diff --git a/Neos.Neos/Classes/PendingChangesProjection/Change.php b/Neos.Neos/Classes/PendingChangesProjection/Change.php index 467cc2c1a82..7b616e47b6c 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/Change.php +++ b/Neos.Neos/Classes/PendingChangesProjection/Change.php @@ -56,8 +56,8 @@ public function addToDatabase(Connection $databaseConnection, string $tableName) $databaseConnection->insert($tableName, [ 'contentStreamId' => $this->contentStreamId->value, 'nodeAggregateId' => $this->nodeAggregateId->value, - 'originDimensionSpacePoint' => $this->originDimensionSpacePoint->toJson(), - 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint->hash, + 'originDimensionSpacePoint' => $this->originDimensionSpacePoint?->toJson(), + 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint?->hash ?: 'AGGREGATE', 'created' => (int)$this->created, 'changed' => (int)$this->changed, 'moved' => (int)$this->moved, @@ -84,8 +84,8 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa [ 'contentStreamId' => $this->contentStreamId->value, 'nodeAggregateId' => $this->nodeAggregateId->value, - 'originDimensionSpacePoint' => $this->originDimensionSpacePoint->toJson(), - 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint->hash, + 'originDimensionSpacePoint' => $this->originDimensionSpacePoint?->toJson(), + 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint?->hash ?: 'AGGREGATE', ] ); } catch (DbalException $e) { @@ -101,7 +101,9 @@ public static function fromDatabaseRow(array $databaseRow): self return new self( ContentStreamId::fromString($databaseRow['contentStreamId']), NodeAggregateId::fromString($databaseRow['nodeAggregateId']), - OriginDimensionSpacePoint::fromJsonString($databaseRow['originDimensionSpacePoint']), + $databaseRow['originDimensionSpacePoint'] ?? null + ? OriginDimensionSpacePoint::fromJsonString($databaseRow['originDimensionSpacePoint']) + : null, (bool)$databaseRow['created'], (bool)$databaseRow['changed'], (bool)$databaseRow['moved'], diff --git a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php index 30f47541e28..1544b774835 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php +++ b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php @@ -128,7 +128,7 @@ private function determineRequiredSqlStatements(): array (new Column('moved', Type::getType(Types::BOOLEAN)))->setNotnull(true), DbalSchemaFactory::columnForNodeAggregateId('nodeAggregateId')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePoint('originDimensionSpacePoint')->setNotnull(false), - DbalSchemaFactory::columnForDimensionSpacePointHash('originDimensionSpacePointHash')->setNotnull(false), + DbalSchemaFactory::columnForDimensionSpacePointHash('originDimensionSpacePointHash')->setNotnull(true), (new Column('deleted', Type::getType(Types::BOOLEAN)))->setNotnull(true), // Despite the name suggesting this might be an anchor point of sorts, this is a nodeAggregateId type DbalSchemaFactory::columnForNodeAggregateId('removalAttachmentPoint')->setNotnull(false) diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index b6f7b4553a5..7e64c6a70c5 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -16,6 +16,7 @@ use Doctrine\DBAL\Exception as DBALException; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\WorkspaceAlreadyExists; use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardIndividualNodesFromWorkspace; @@ -684,6 +685,9 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos ->findByContentStreamId( $selectedWorkspace->currentContentStreamId ); + $dimensionSpacePoints = iterator_to_array($contentRepository->getVariationGraph()->getDimensionSpacePoints()); + /** @var DimensionSpacePoint $arbitraryDimensionSpacePoint */ + $arbitraryDimensionSpacePoint = reset($dimensionSpacePoints); foreach ($changes as $change) { $workspaceName = $selectedWorkspace->workspaceName; @@ -697,7 +701,7 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos $workspaceName = $baseWorkspace->workspaceName; } $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph( - $change->originDimensionSpacePoint->toDimensionSpacePoint(), + $change->originDimensionSpacePoint?->toDimensionSpacePoint() ?: $arbitraryDimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); @@ -765,7 +769,7 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos $nodeAddress = NodeAddress::create( $contentRepository->id, $selectedWorkspace->workspaceName, - $change->originDimensionSpacePoint->toDimensionSpacePoint(), + $change->originDimensionSpacePoint?->toDimensionSpacePoint() ?: $arbitraryDimensionSpacePoint, $change->nodeAggregateId ); From c8f13d35ac1ffce6eb327c67418459d5dae79ac6 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 24 Oct 2024 14:45:25 +0200 Subject: [PATCH 08/11] BUGFIX: Use aggregate dimensionspacepoint hash placeholder for fetching changes for aggregates --- Neos.Neos/Classes/PendingChangesProjection/Change.php | 4 +++- .../Classes/PendingChangesProjection/ChangeProjection.php | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/PendingChangesProjection/Change.php b/Neos.Neos/Classes/PendingChangesProjection/Change.php index 7b616e47b6c..0fb47251d20 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/Change.php +++ b/Neos.Neos/Classes/PendingChangesProjection/Change.php @@ -30,6 +30,8 @@ */ final class Change { + public const AGGREGATE_DIMENSIONSPACEPOINT_HASH_PLACEHOLDER = 'AGGREGATE'; + /** * @param NodeAggregateId|null $removalAttachmentPoint {@see RemoveNodeAggregate::$removalAttachmentPoint} for docs */ @@ -57,7 +59,7 @@ public function addToDatabase(Connection $databaseConnection, string $tableName) 'contentStreamId' => $this->contentStreamId->value, 'nodeAggregateId' => $this->nodeAggregateId->value, 'originDimensionSpacePoint' => $this->originDimensionSpacePoint?->toJson(), - 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint?->hash ?: 'AGGREGATE', + 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint?->hash ?: self::AGGREGATE_DIMENSIONSPACEPOINT_HASH_PLACEHOLDER, 'created' => (int)$this->created, 'changed' => (int)$this->changed, 'moved' => (int)$this->moved, diff --git a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php index 1544b774835..1d173895789 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php +++ b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php @@ -555,10 +555,11 @@ private function getChangeForAggregate( 'SELECT n.* FROM ' . $this->tableNamePrefix . ' n WHERE n.contentStreamId = :contentStreamId AND n.nodeAggregateId = :nodeAggregateId -AND n.origindimensionspacepointhash = NULL', +AND n.origindimensionspacepointhash = :origindimensionspacepointhash', [ 'contentStreamId' => $contentStreamId->value, 'nodeAggregateId' => $nodeAggregateId->value, + 'origindimensionspacepointhash' => Change::AGGREGATE_DIMENSIONSPACEPOINT_HASH_PLACEHOLDER ] )->fetchAssociative(); From 39b8d1caac8c4dc670c82baba124e1066143b89b Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 24 Oct 2024 15:08:08 +0200 Subject: [PATCH 09/11] TASK: Use constant instead of string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christian Müller --- Neos.Neos/Classes/PendingChangesProjection/Change.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/PendingChangesProjection/Change.php b/Neos.Neos/Classes/PendingChangesProjection/Change.php index 0fb47251d20..798902f5517 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/Change.php +++ b/Neos.Neos/Classes/PendingChangesProjection/Change.php @@ -87,7 +87,7 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa 'contentStreamId' => $this->contentStreamId->value, 'nodeAggregateId' => $this->nodeAggregateId->value, 'originDimensionSpacePoint' => $this->originDimensionSpacePoint?->toJson(), - 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint?->hash ?: 'AGGREGATE', + 'originDimensionSpacePointHash' => $this->originDimensionSpacePoint?->hash ?: self::AGGREGATE_DIMENSIONSPACEPOINT_HASH_PLACEHOLDER, ] ); } catch (DbalException $e) { From eb987039edbb3a173dcc51e5b9990b672960d18f Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 24 Oct 2024 15:33:13 +0200 Subject: [PATCH 10/11] TASK: Optimize contentGraph fetching --- .../Controller/WorkspaceController.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index 7e64c6a70c5..be53eeff49e 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -689,18 +689,18 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos /** @var DimensionSpacePoint $arbitraryDimensionSpacePoint */ $arbitraryDimensionSpacePoint = reset($dimensionSpacePoints); + $selectedWorkspaceContentGraph = $contentRepository->getContentGraph($selectedWorkspace->workspaceName); + // If we deleted a node, there is no way for us to anymore find the deleted node in the ContentStream + // where the node was deleted. + // Thus, to figure out the rootline for display, we check the *base workspace* Content Stream. + // + // This is safe because the UI basically shows what would be removed once the deletion is published. + $baseWorkspace = $this->getBaseWorkspaceWhenSureItExists($selectedWorkspace, $contentRepository); + $baseWorkspaceContentGraph = $contentRepository->getContentGraph($baseWorkspace->workspaceName); + foreach ($changes as $change) { - $workspaceName = $selectedWorkspace->workspaceName; - if ($change->deleted) { - // If we deleted a node, there is no way for us to anymore find the deleted node in the ContentStream - // where the node was deleted. - // Thus, to figure out the rootline for display, we check the *base workspace* Content Stream. - // - // This is safe because the UI basically shows what would be removed once the deletion is published. - $baseWorkspace = $this->getBaseWorkspaceWhenSureItExists($selectedWorkspace, $contentRepository); - $workspaceName = $baseWorkspace->workspaceName; - } - $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph( + $contentGraph = $change->deleted ? $baseWorkspaceContentGraph : $selectedWorkspaceContentGraph; + $subgraph = $contentGraph->getSubgraph( $change->originDimensionSpacePoint?->toDimensionSpacePoint() ?: $arbitraryDimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); @@ -836,7 +836,7 @@ protected function renderContentChanges( ); $originalNode = null; if ($currentWorkspace !== null) { - $baseWorkspace = $this->getBaseWorkspaceWhenSureItExists($currentWorkspace, $contentRepository); + $baseWorkspace = $this->getBaseWorkspaceWhengetContentGraphSureItExists($currentWorkspace, $contentRepository); $originalNode = $this->getOriginalNode($changedNode, $baseWorkspace->workspaceName, $contentRepository); } @@ -886,8 +886,8 @@ protected function renderContentChanges( 'diff' => $diffArray ]; } - // The && in belows condition is on purpose as creating a thumbnail for comparison only works - // if actually BOTH are ImageInterface (or NULL). + // The && in belows condition is on purpose as creating a thumbnail for comparison only works + // if actually BOTH are ImageInterface (or NULL). } elseif ( ($originalPropertyValue instanceof ImageInterface || $originalPropertyValue === null) && ($changedPropertyValue instanceof ImageInterface || $changedPropertyValue === null) From b410ab338469e40712c2e7fbdeb805391779bc83 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Thu, 24 Oct 2024 15:44:31 +0200 Subject: [PATCH 11/11] BUGIFX: Rename accidentally renamed method call --- Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index be53eeff49e..1b80ebbf636 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -836,7 +836,7 @@ protected function renderContentChanges( ); $originalNode = null; if ($currentWorkspace !== null) { - $baseWorkspace = $this->getBaseWorkspaceWhengetContentGraphSureItExists($currentWorkspace, $contentRepository); + $baseWorkspace = $this->getBaseWorkspaceWhenSureItExists($currentWorkspace, $contentRepository); $originalNode = $this->getOriginalNode($changedNode, $baseWorkspace->workspaceName, $contentRepository); }