diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index cb43d749e47..873a700ea17 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -17,11 +17,12 @@ use Behat\Gherkin\Node\TableNode; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Exception\InvalidArgumentException; -use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap\Helpers\TestingNodeAggregateId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -52,9 +53,9 @@ trait ProjectionIntegrityViolationDetectionTrait */ abstract private function getObject(string $className): object; - protected function getTableNamePrefix(): string + private function tableNames(): ContentGraphTableNames { - return DoctrineDbalContentGraphProjectionFactory::graphProjectionTableNamePrefix( + return ContentGraphTableNames::create( $this->currentContentRepository->id ); } @@ -80,7 +81,7 @@ public function iRemoveTheFollowingSubtreeTag(TableNode $payloadTable): void throw new \RuntimeException(sprintf('Failed to remove subtree tag "%s" because that tag is not set', $subtreeTagToRemove->value), 1708618267); } $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_hierarchyrelation', + $this->tableNames()->hierarchyRelation(), [ 'subtreetags' => json_encode($subtreeTags->without($subtreeTagToRemove), JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), ], @@ -97,7 +98,7 @@ public function iAddTheFollowingHierarchyRelation(TableNode $payloadTable): void $dataset = $this->transformPayloadTableToDataset($payloadTable); $record = $this->transformDatasetToHierarchyRelationRecord($dataset); $this->dbalClient->getConnection()->insert( - $this->getTableNamePrefix() . '_hierarchyrelation', + $this->tableNames()->hierarchyRelation(), $record ); } @@ -114,7 +115,7 @@ public function iChangeTheFollowingHierarchyRelationsDimensionSpacePointHash(Tab unset($record['position']); $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_hierarchyrelation', + $this->tableNames()->hierarchyRelation(), [ 'dimensionspacepointhash' => $dataset['newDimensionSpacePointHash'] ], @@ -123,22 +124,35 @@ public function iChangeTheFollowingHierarchyRelationsDimensionSpacePointHash(Tab } /** - * @When /^I change the following hierarchy relation's name:$/ + * @When /^I change the following node's name:$/ * @param TableNode $payloadTable * @throws DBALException */ - public function iChangeTheFollowingHierarchyRelationsEdgeName(TableNode $payloadTable): void + public function iChangeTheFollowingNodesName(TableNode $payloadTable): void { $dataset = $this->transformPayloadTableToDataset($payloadTable); - $record = $this->transformDatasetToHierarchyRelationRecord($dataset); - unset($record['position']); + + $relationAnchorPoint = $this->dbalClient->getConnection()->executeQuery( + 'SELECT n.relationanchorpoint FROM ' . $this->tableNames()->node() . ' n + JOIN ' . $this->tableNames()->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint + WHERE h.contentstreamid = :contentStreamId + AND n.nodeaggregateId = :nodeAggregateId + AND n.origindimensionspacepointhash = :originDimensionSpacePointHash', + [ + 'contentStreamId' => $dataset['contentStreamId'], + 'nodeAggregateId' => $dataset['nodeAggregateId'], + 'originDimensionSpacePointHash' => OriginDimensionSpacePoint::fromArray($dataset['originDimensionSpacePoint'])->hash, + ] + )->fetchOne(); $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_hierarchyrelation', + $this->tableNames()->node(), [ 'name' => $dataset['newName'] ], - $record + [ + 'relationanchorpoint' => $relationAnchorPoint + ] ); } @@ -158,7 +172,7 @@ public function iSetTheFollowingPosition(TableNode $payloadTable): void ]; $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_hierarchyrelation', + $this->tableNames()->hierarchyRelation(), [ 'position' => $dataset['newPosition'] ], @@ -176,7 +190,7 @@ public function iDetachTheFollowingReferenceRelationFromItsSource(TableNode $pay $dataset = $this->transformPayloadTableToDataset($payloadTable); $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_referencerelation', + $this->tableNames()->referenceRelation(), [ 'nodeanchorpoint' => 7777777 ], @@ -194,7 +208,7 @@ public function iSetTheFollowingReferencePosition(TableNode $payloadTable): void $dataset = $this->transformPayloadTableToDataset($payloadTable); $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_referencerelation', + $this->tableNames()->referenceRelation(), [ 'position' => $dataset['newPosition'] ], @@ -265,8 +279,8 @@ private function findHierarchyRelationByIds( ): array { $nodeRecord = $this->dbalClient->getConnection()->executeQuery( 'SELECT h.* - FROM ' . $this->getTableNamePrefix() . '_node n - INNER JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation h + FROM ' . $this->tableNames()->node() . ' n + INNER JOIN ' . $this->tableNames()->hierarchyRelation() . ' h ON n.relationanchorpoint = h.childnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId AND h.contentstreamid = :contentStreamId diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/HierarchyIntegrityIsProvided.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/HierarchyIntegrityIsProvided.feature index bb7eeccc08b..29e8723a50c 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/HierarchyIntegrityIsProvided.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/HierarchyIntegrityIsProvided.feature @@ -20,7 +20,7 @@ Feature: Run integrity violation detection regarding hierarchy relations and nod | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/NodesHaveAtMostOneParentPerSubgraph.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/NodesHaveAtMostOneParentPerSubgraph.feature index 0cb73f49e4b..72ced95d05a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/NodesHaveAtMostOneParentPerSubgraph.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/NodesHaveAtMostOneParentPerSubgraph.feature @@ -20,7 +20,7 @@ Feature: Run integrity violation detection regarding parent relations | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature index 3267be5d085..3e02af08377 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature @@ -23,7 +23,7 @@ Feature: Run integrity violation detection regarding reference relations | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SiblingsAreDistinctlySorted.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SiblingsAreDistinctlySorted.feature index 75f7032d08c..433d1a30c88 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SiblingsAreDistinctlySorted.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SiblingsAreDistinctlySorted.feature @@ -20,7 +20,7 @@ Feature: Run integrity violation detection regarding sibling sorting | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SubtreeTagsAreInherited.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SubtreeTagsAreInherited.feature index 1c424425d1f..c9a2769dfb2 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SubtreeTagsAreInherited.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SubtreeTagsAreInherited.feature @@ -20,7 +20,7 @@ Feature: Run integrity violation detection regarding subtree tag inheritance | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature index 3068aaa7edf..921f197fb20 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature @@ -14,48 +14,47 @@ Feature: Run projection integrity violation detection regarding naming of tether And using identifier "default", I define a content repository And I am in content repository "default" And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {"language":"de"} | + | coveredDimensionSpacePoints | [{"language":"de"}] | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + | nodeAggregateClassification | "regular" | And the graph projection is fully up to date - Scenario: Create node variants of different type + Scenario: Remove tethered node's name When the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nodewyn-tetherton" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "to-be-hacked-to-null" | - | nodeAggregateClassification | "tethered" | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {"language":"de"} | + | coveredDimensionSpacePoints | [{"language":"de"}] | + | parentNodeAggregateId | "sir-david-nodenborough" | + | nodeName | "to-be-hacked-to-null" | + | nodeAggregateClassification | "tethered" | And the graph projection is fully up to date - And I change the following hierarchy relation's name: - | Key | Value | - | contentStreamId | "cs-identifier" | - | dimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "sir-david-nodenborough" | - | childNodeAggregateId | "nodewyn-tetherton" | - | newName | null | + And I change the following node's name: + | Key | Value | + | contentStreamId | "cs-identifier" | + | originDimensionSpacePoint | {"language":"de"} | + | nodeAggregateId | "nodewyn-tetherton" | + | newName | null | And I run integrity violation detection Then I expect the integrity violation detection result to contain exactly 1 errors And I expect integrity violation detection result error number 1 to have code 1597923103 diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/SiblingPositions.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/SiblingPositions.feature new file mode 100644 index 00000000000..6ace4b14ce3 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/SiblingPositions.feature @@ -0,0 +1,110 @@ +@contentrepository +Feature: Sibling positions are properly resolved + + In the general DBAL adapter, hierarchy relations are sorted by an integer field. It defaults to a distance of 128, + which is reduced each time a node is inserted between two siblings. Once the number becomes uneven, the siblings positions are recalculated. + These are the test cases for this behavior. + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, peer, spec | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example": "general"} + 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 | nodeTypeName | parentNodeAggregateId | nodeName | + | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | esquire | + | lady-nodette-nodington-i | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-i | + | lady-nodette-nodington-x | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-x | + | lady-nodette-nodington-ix | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-ix | + | lady-nodette-nodington-viii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-viii | + | lady-nodette-nodington-vii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-vii | + | lady-nodette-nodington-vi | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-vi | + | lady-nodette-nodington-v | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-v | + | lady-nodette-nodington-iv | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-iv | + | lady-nodette-nodington-iii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-iii | + | lady-nodette-nodington-ii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-ii | + + + Scenario: Trigger position update in DBAL graph + Given I am in workspace "live" and dimension space point {"example": "general"} + # distance i to x: 128 + # distance ii to x: 64 + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-ii" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + # distance iii to x: 32 + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-iii" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + # distance iv to x: 16 + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-iv" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + # distance v to x: 8 + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-v" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + # distance vi to x: 4 + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-vi" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + # distance vii to x: 2 + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-vii" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + # distance viii to x: 1 -> reorder -> 128 + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-viii" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + # distance ix to x: 64 after reorder + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-nodette-nodington-ix" | + | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | + And the graph projection is fully up to date + + Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} + And I expect this node to have the following child nodes: + | Name | NodeDiscriminator | + | esquire | cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} | + | nodington-i | cs-identifier;lady-nodette-nodington-i;{"example": "general"} | + | nodington-ii | cs-identifier;lady-nodette-nodington-ii;{"example": "general"} | + | nodington-iii | cs-identifier;lady-nodette-nodington-iii;{"example": "general"} | + | nodington-iv | cs-identifier;lady-nodette-nodington-iv;{"example": "general"} | + | nodington-v | cs-identifier;lady-nodette-nodington-v;{"example": "general"} | + | nodington-vi | cs-identifier;lady-nodette-nodington-vi;{"example": "general"} | + | nodington-vii | cs-identifier;lady-nodette-nodington-vii;{"example": "general"} | + | nodington-viii | cs-identifier;lady-nodette-nodington-viii;{"example": "general"} | + | nodington-ix | cs-identifier;lady-nodette-nodington-ix;{"example": "general"} | + | nodington-x | cs-identifier;lady-nodette-nodington-x;{"example": "general"} | diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphFactory.php new file mode 100644 index 00000000000..66ae7720899 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphFactory.php @@ -0,0 +1,71 @@ +contentRepositoryId->value, + 'workspace' + )); + + $row = $this->client->getConnection()->executeQuery( + ' + SELECT * FROM ' . $tableName . ' + WHERE workspaceName = :workspaceName + LIMIT 1 + ', + [ + 'workspaceName' => $workspaceName->value, + ] + )->fetchAssociative(); + + if ($row === false) { + throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); + } + + return $this->buildForWorkspaceAndContentStream($workspaceName, ContentStreamId::fromString($row['currentcontentstreamid'])); + } + + public function buildForWorkspaceAndContentStream(WorkspaceName $workspaceName, ContentStreamId $contentStreamId): ContentGraph + { + return new ContentGraph($this->client, $this->nodeFactory, $this->contentRepositoryId, $this->nodeTypeManager, $this->tableNames, $workspaceName, $contentStreamId); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php new file mode 100644 index 00000000000..0804d77e7e0 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php @@ -0,0 +1,47 @@ +value)); + } + + public function node(): string + { + return $this->tableNamePrefix . '_node'; + } + + public function hierarchyRelation(): string + { + return $this->tableNamePrefix . '_hierarchyrelation'; + } + + public function dimensionSpacePoints(): string + { + return $this->tableNamePrefix . '_dimensionspacepoints'; + } + + public function referenceRelation(): string + { + return $this->tableNamePrefix . '_referencerelation'; + } + + public function checkpoint(): string + { + return $this->tableNamePrefix . '_checkpoint'; + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index a3a92150aa8..ade91c5d169 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -14,10 +14,9 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentGraph; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; +use Neos\ContentRepository\Core\ContentGraphFinder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; @@ -47,15 +46,12 @@ use Neos\ContentRepository\Core\Infrastructure\DbalCheckpointStorage; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\CheckpointStorageStatusType; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStatus; -use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; -use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -64,10 +60,10 @@ use Neos\EventStore\Model\EventEnvelope; /** - * @implements ProjectionInterface + * @implements ProjectionInterface * @internal but the graph projection is api */ -final class DoctrineDbalContentGraphProjection implements ProjectionInterface, WithMarkStaleInterface +final class DoctrineDbalContentGraphProjection implements ProjectionInterface { use NodeVariation; use SubtreeTagging; @@ -77,26 +73,18 @@ final class DoctrineDbalContentGraphProjection implements ProjectionInterface, W public const RELATION_DEFAULT_OFFSET = 128; - /** - * @var ContentGraph|null Cache for the content graph returned by {@see getState()}, - * so that always the same instance is returned - */ - private ?ContentGraph $contentGraph = null; - private DbalCheckpointStorage $checkpointStorage; public function __construct( private readonly DbalClientInterface $dbalClient, - private readonly NodeFactory $nodeFactory, - private readonly ContentRepositoryId $contentRepositoryId, - private readonly NodeTypeManager $nodeTypeManager, private readonly ProjectionContentGraph $projectionContentGraph, - private readonly string $tableNamePrefix, - private readonly DimensionSpacePointsRepository $dimensionSpacePointsRepository + private readonly ContentGraphTableNames $tableNames, + private readonly DimensionSpacePointsRepository $dimensionSpacePointsRepository, + private readonly ContentGraphFinder $contentGraphFinder ) { $this->checkpointStorage = new DbalCheckpointStorage( $this->dbalClient->getConnection(), - $this->tableNamePrefix . '_checkpoint', + $this->tableNames->checkpoint(), self::class ); } @@ -106,11 +94,6 @@ protected function getProjectionContentGraph(): ProjectionContentGraph return $this->projectionContentGraph; } - protected function getTableNamePrefix(): string - { - return $this->tableNamePrefix; - } - public function setUp(): void { foreach ($this->determineRequiredSqlStatements() as $statement) { @@ -129,7 +112,7 @@ private function determineRequiredSqlStatements(): array if (!$schemaManager instanceof AbstractSchemaManager) { throw new \RuntimeException('Failed to retrieve Schema Manager', 1625653914); } - $schema = (new DoctrineDbalContentGraphSchemaBuilder($this->tableNamePrefix))->buildSchema($schemaManager); + $schema = (new DoctrineDbalContentGraphSchemaBuilder($this->tableNames))->buildSchema($schemaManager); return DbalSchemaDiff::determineRequiredSqlStatements($connection, $schema); } @@ -164,20 +147,16 @@ public function reset(): void $this->checkpointStorage->acquireLock(); $this->checkpointStorage->updateAndReleaseLock(SequenceNumber::none()); - - $contentGraph = $this->getState(); - foreach ($contentGraph->getSubgraphs() as $subgraph) { - $subgraph->inMemoryCache->enable(); - } + $this->getState()->forgetInstances(); } private function truncateDatabaseTables(): void { $connection = $this->dbalClient->getConnection(); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_node'); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_hierarchyrelation'); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_referencerelation'); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_dimensionspacepoints'); + $connection->executeQuery('TRUNCATE table ' . $this->tableNames->node()); + $connection->executeQuery('TRUNCATE table ' . $this->tableNames->hierarchyRelation()); + $connection->executeQuery('TRUNCATE table ' . $this->tableNames->referenceRelation()); + $connection->executeQuery('TRUNCATE table ' . $this->tableNames->dimensionSpacePoints()); } public function canHandle(EventInterface $event): bool @@ -234,26 +213,9 @@ public function getCheckpointStorage(): DbalCheckpointStorage return $this->checkpointStorage; } - public function getState(): ContentGraph + public function getState(): ContentGraphFinder { - if (!$this->contentGraph) { - $this->contentGraph = new ContentGraph( - $this->dbalClient, - $this->nodeFactory, - $this->contentRepositoryId, - $this->nodeTypeManager, - $this->tableNamePrefix - ); - } - return $this->contentGraph; - } - - public function markStale(): void - { - $contentGraph = $this->getState(); - foreach ($contentGraph->getSubgraphs() as $subgraph) { - $subgraph->inMemoryCache->disable(); - } + return $this->contentGraphFinder; } /** @@ -265,7 +227,7 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo $originDimensionSpacePoint = OriginDimensionSpacePoint::createWithoutDimensions(); $node = NodeRecord::createNewInDatabase( $this->getDatabaseConnection(), - $this->tableNamePrefix, + $this->tableNames, $event->nodeAggregateId, $originDimensionSpacePoint->coordinates, $originDimensionSpacePoint->hash, @@ -311,7 +273,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim $this->transactional(function () use ($rootNodeAnchorPoint, $event) { // delete all hierarchy edges of the root node $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_hierarchyrelation + DELETE FROM ' . $this->tableNames->hierarchyRelation() . ' WHERE parentnodeanchor = :parentNodeAnchor AND childnodeanchor = :childNodeAnchor @@ -359,28 +321,24 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event, EventEnvelope $eventEnvelope): void { $this->transactional(function () use ($event, $eventEnvelope) { - $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h - INNER JOIN ' . $this->tableNamePrefix . '_node n on - h.childnodeanchor = n.relationanchorpoint - SET - h.name = :newName, - n.lastmodified = :lastModified, - n.originallastmodified = :originalLastModified - - WHERE - n.nodeaggregateid = :nodeAggregateId - and h.contentstreamid = :contentStreamId - ', [ - 'newName' => $event->newNodeName->value, - 'nodeAggregateId' => $event->nodeAggregateId->value, - 'contentStreamId' => $event->contentStreamId->value, - 'lastModified' => $eventEnvelope->recordedAt, - 'originalLastModified' => self::initiatingDateTime($eventEnvelope), - ], [ - 'lastModified' => Types::DATETIME_IMMUTABLE, - 'originalLastModified' => Types::DATETIME_IMMUTABLE, - ]); + foreach ( + $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( + $event->nodeAggregateId, + $event->contentStreamId, + ) as $anchorPoint + ) { + $this->updateNodeRecordWithCopyOnWrite( + $event->contentStreamId, + $anchorPoint, + function (NodeRecord $node) use ($event, $eventEnvelope) { + $node->nodeName = $event->newNodeName; + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); + } + ); + } }); } @@ -401,7 +359,7 @@ private function createNodeWithHierarchy( ): void { $node = NodeRecord::createNewInDatabase( $this->getDatabaseConnection(), - $this->tableNamePrefix, + $this->tableNames, $nodeAggregateId, $originDimensionSpacePoint->jsonSerialize(), $originDimensionSpacePoint->hash, @@ -446,7 +404,6 @@ private function createNodeWithHierarchy( $node->relationAnchorPoint, new DimensionSpacePointSet([$dimensionSpacePoint]), $succeedingSibling?->relationAnchorPoint, - $nodeName ); } } @@ -457,7 +414,6 @@ private function createNodeWithHierarchy( * @param NodeRelationAnchorPoint $parentNodeAnchorPoint * @param NodeRelationAnchorPoint $childNodeAnchorPoint * @param NodeRelationAnchorPoint|null $succeedingSiblingNodeAnchorPoint - * @param NodeName|null $relationName * @param ContentStreamId $contentStreamId * @param DimensionSpacePointSet $dimensionSpacePointSet * @throws \Doctrine\DBAL\DBALException @@ -468,7 +424,6 @@ private function connectHierarchy( NodeRelationAnchorPoint $childNodeAnchorPoint, DimensionSpacePointSet $dimensionSpacePointSet, ?NodeRelationAnchorPoint $succeedingSiblingNodeAnchorPoint, - NodeName $relationName = null ): void { foreach ($dimensionSpacePointSet as $dimensionSpacePoint) { $position = $this->getRelationPosition( @@ -485,7 +440,6 @@ private function connectHierarchy( $hierarchyRelation = new HierarchyRelation( $parentNodeAnchorPoint, $childNodeAnchorPoint, - $relationName, $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, @@ -493,7 +447,7 @@ private function connectHierarchy( $inheritedSubtreeTags, ); - $hierarchyRelation->addToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); + $hierarchyRelation->addToDatabase($this->getDatabaseConnection(), $this->tableNames); } } @@ -580,7 +534,7 @@ private function getRelationPositionAfterRecalculation( $position = $offset; $offset += self::RELATION_DEFAULT_OFFSET; } - $relation->assignNewPosition($offset, $this->getDatabaseConnection(), $this->tableNamePrefix); + $relation->assignNewPosition($offset, $this->getDatabaseConnection(), $this->tableNames); } return $position; @@ -597,10 +551,9 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void // 1) Copy HIERARCHY RELATIONS (this is the MAIN OPERATION here) // $this->getDatabaseConnection()->executeUpdate(' - INSERT INTO ' . $this->tableNamePrefix . '_hierarchyrelation ( + INSERT INTO ' . $this->tableNames->hierarchyRelation() . ' ( parentnodeanchor, childnodeanchor, - `name`, position, dimensionspacepointhash, subtreetags, @@ -609,13 +562,12 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void SELECT h.parentnodeanchor, h.childnodeanchor, - h.name, h.position, h.dimensionspacepointhash, h.subtreetags, "' . $event->newContentStreamId->value . '" AS contentstreamid FROM - ' . $this->tableNamePrefix . '_hierarchyrelation h + ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.contentstreamid = :sourceContentStreamId ', [ 'sourceContentStreamId' => $event->sourceContentStreamId->value @@ -632,7 +584,7 @@ private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): vo // Drop hierarchy relations $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_hierarchyrelation + DELETE FROM ' . $this->tableNames->hierarchyRelation() . ' WHERE contentstreamid = :contentStreamId ', [ @@ -641,23 +593,23 @@ private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): vo // Drop non-referenced nodes (which do not have a hierarchy relation anymore) $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_node + DELETE FROM ' . $this->tableNames->node() . ' WHERE NOT EXISTS ( - SELECT 1 FROM ' . $this->tableNamePrefix . '_hierarchyrelation - WHERE ' . $this->tableNamePrefix . '_hierarchyrelation.childnodeanchor - = ' . $this->tableNamePrefix . '_node.relationanchorpoint + SELECT 1 FROM ' . $this->tableNames->hierarchyRelation() . ' + WHERE ' . $this->tableNames->hierarchyRelation() . '.childnodeanchor + = ' . $this->tableNames->node() . '.relationanchorpoint ) '); // Drop non-referenced reference relations (i.e. because the referenced nodes are gone by now) $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_referencerelation + DELETE FROM ' . $this->tableNames->referenceRelation() . ' WHERE NOT EXISTS ( - SELECT 1 FROM ' . $this->tableNamePrefix . '_node - WHERE ' . $this->tableNamePrefix . '_node.relationanchorpoint - = ' . $this->tableNamePrefix . '_referencerelation.nodeanchorpoint + SELECT 1 FROM ' . $this->tableNames->node() . ' + WHERE ' . $this->tableNames->node() . '.relationanchorpoint + = ' . $this->tableNames->referenceRelation() . '.nodeanchorpoint ) '); }); @@ -742,7 +694,7 @@ function (NodeRecord $node) use ($eventEnvelope) { ); // remove old - $this->getDatabaseConnection()->delete($this->tableNamePrefix . '_referencerelation', [ + $this->getDatabaseConnection()->delete($this->tableNames->referenceRelation(), [ 'nodeanchorpoint' => $nodeAnchorPoint?->value, 'name' => $event->referenceName->value ]); @@ -751,7 +703,7 @@ function (NodeRecord $node) use ($eventEnvelope) { $position = 0; /** @var SerializedNodeReference $reference */ foreach ($event->references as $reference) { - $this->getDatabaseConnection()->insert($this->tableNamePrefix . '_referencerelation', [ + $this->getDatabaseConnection()->insert($this->tableNames->referenceRelation(), [ 'name' => $event->referenceName->value, 'position' => $position, 'nodeanchorpoint' => $nodeAnchorPoint?->value, @@ -782,7 +734,6 @@ protected function copyHierarchyRelationToDimensionSpacePoint( $copy = new HierarchyRelation( $newParent, $newChild, - $sourceHierarchyRelation->name, $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, @@ -795,7 +746,7 @@ protected function copyHierarchyRelationToDimensionSpacePoint( ), $inheritedSubtreeTags, ); - $copy->addToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); + $copy->addToDatabase($this->getDatabaseConnection(), $this->tableNames); return $copy; } @@ -810,7 +761,7 @@ protected function copyNodeToDimensionSpacePoint( ): NodeRecord { return NodeRecord::createNewInDatabase( $this->getDatabaseConnection(), - $this->tableNamePrefix, + $this->tableNames, $sourceNode->nodeAggregateId, $originDimensionSpacePoint->coordinates, $originDimensionSpacePoint->hash, @@ -862,15 +813,15 @@ private function updateNodeRecordWithCopyOnWrite( // 1) fetch node, adjust properties, assign new Relation Anchor Point /** @var NodeRecord $originalNode The anchor point appears in a content stream, so there must be a node */ $originalNode = $this->projectionContentGraph->getNodeByAnchorPoint($anchorPoint); - $copiedNode = NodeRecord::createCopyFromNodeRecord($this->getDatabaseConnection(), $this->tableNamePrefix, $originalNode); + $copiedNode = NodeRecord::createCopyFromNodeRecord($this->getDatabaseConnection(), $this->tableNames, $originalNode); $result = $operations($copiedNode); - $copiedNode->updateToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); + $copiedNode->updateToDatabase($this->getDatabaseConnection(), $this->tableNames); // 2) reconnect all edges belonging to this content stream to the new "copied node". // IMPORTANT: We need to reconnect BOTH the incoming and outgoing edges. $this->getDatabaseConnection()->executeStatement( ' - UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h + UPDATE ' . $this->tableNames->hierarchyRelation() . ' h SET -- if our (copied) node is the child, we update h.childNodeAnchor h.childnodeanchor @@ -903,7 +854,7 @@ private function updateNodeRecordWithCopyOnWrite( } $result = $operations($node); - $node->updateToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); + $node->updateToDatabase($this->getDatabaseConnection(), $this->tableNames); } return $result; } @@ -914,7 +865,7 @@ private function copyReferenceRelations( NodeRelationAnchorPoint $destinationRelationAnchorPoint ): void { $this->getDatabaseConnection()->executeStatement(' - INSERT INTO ' . $this->tableNamePrefix . '_referencerelation ( + INSERT INTO ' . $this->tableNames->referenceRelation() . ' ( nodeanchorpoint, name, position, @@ -926,7 +877,7 @@ private function copyReferenceRelations( ref.position, ref.destinationnodeaggregateid FROM - ' . $this->tableNamePrefix . '_referencerelation ref + ' . $this->tableNames->referenceRelation() . ' ref WHERE ref.nodeanchorpoint = :sourceNodeAnchorPoint ', [ 'sourceNodeAnchorPoint' => $sourceRelationAnchorPoint->value, @@ -945,8 +896,8 @@ private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $ev // 1) originDimensionSpacePoint on Node $rel = $this->getDatabaseConnection()->executeQuery( 'SELECT n.relationanchorpoint, n.origindimensionspacepointhash - FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint AND h.contentstreamid = :contentStreamId @@ -975,7 +926,7 @@ function (NodeRecord $nodeRecord) use ($event) { // 2) hierarchy relations $this->getDatabaseConnection()->executeStatement( ' - UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h + UPDATE ' . $this->tableNames->hierarchyRelation() . ' h SET h.dimensionspacepointhash = :newDimensionSpacePointHash WHERE @@ -999,10 +950,9 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded // 1) hierarchy relations $this->getDatabaseConnection()->executeStatement( ' - INSERT INTO ' . $this->tableNamePrefix . '_hierarchyrelation ( + INSERT INTO ' . $this->tableNames->hierarchyRelation() . ' ( parentnodeanchor, childnodeanchor, - `name`, position, subtreetags, dimensionspacepointhash, @@ -1011,13 +961,12 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded SELECT h.parentnodeanchor, h.childnodeanchor, - h.name, h.position, h.subtreetags, :newDimensionSpacePointHash AS dimensionspacepointhash, h.contentstreamid FROM - ' . $this->tableNamePrefix . '_hierarchyrelation h + ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :sourceDimensionSpacePointHash', [ diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php index 64891e2cc33..402d767abcf 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php @@ -7,6 +7,7 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; +use Neos\ContentRepository\Core\ContentGraphFinder; use Neos\ContentRepository\Core\Factory\ProjectionFactoryDependencies; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjection; @@ -27,39 +28,41 @@ public function __construct( ) { } - public static function graphProjectionTableNamePrefix( - ContentRepositoryId $contentRepositoryId - ): string { - return sprintf('cr_%s_p_graph', $contentRepositoryId->value); - } - public function build( ProjectionFactoryDependencies $projectionFactoryDependencies, array $options, ): ContentGraphProjection { - $tableNamePrefix = self::graphProjectionTableNamePrefix( + $tableNames = ContentGraphTableNames::create( $projectionFactoryDependencies->contentRepositoryId ); - $dimensionSpacePointsRepository = new DimensionSpacePointsRepository($this->dbalClient->getConnection(), $tableNamePrefix); + $dimensionSpacePointsRepository = new DimensionSpacePointsRepository($this->dbalClient->getConnection(), $tableNames); + + $nodeFactory = new NodeFactory( + $projectionFactoryDependencies->contentRepositoryId, + $projectionFactoryDependencies->nodeTypeManager, + $projectionFactoryDependencies->propertyConverter, + $dimensionSpacePointsRepository + ); + + $contentGraphFactory = new ContentGraphFactory( + $this->dbalClient, + $nodeFactory, + $projectionFactoryDependencies->contentRepositoryId, + $projectionFactoryDependencies->nodeTypeManager, + $tableNames + ); return new ContentGraphProjection( new DoctrineDbalContentGraphProjection( $this->dbalClient, - new NodeFactory( - $projectionFactoryDependencies->contentRepositoryId, - $projectionFactoryDependencies->nodeTypeManager, - $projectionFactoryDependencies->propertyConverter, - $dimensionSpacePointsRepository - ), - $projectionFactoryDependencies->contentRepositoryId, - $projectionFactoryDependencies->nodeTypeManager, new ProjectionContentGraph( $this->dbalClient, - $tableNamePrefix + $tableNames ), - $tableNamePrefix, - $dimensionSpacePointsRepository + $tableNames, + $dimensionSpacePointsRepository, + new ContentGraphFinder($contentGraphFactory) ) ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 06cc35ff431..e6bc7fc028e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -18,7 +18,7 @@ class DoctrineDbalContentGraphSchemaBuilder private const DEFAULT_TEXT_COLLATION = 'utf8mb4_unicode_520_ci'; public function __construct( - private readonly string $tableNamePrefix + private readonly ContentGraphTableNames $contentGraphTableNames ) { } @@ -34,11 +34,12 @@ public function buildSchema(AbstractSchemaManager $schemaManager): Schema private function createNodeTable(): Table { - $table = new Table($this->tableNamePrefix . '_node', [ + $table = new Table($this->contentGraphTableNames->node(), [ DbalSchemaFactory::columnForNodeAnchorPoint('relationanchorpoint')->setAutoincrement(true), DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotnull(false), DbalSchemaFactory::columnForDimensionSpacePointHash('origindimensionspacepointhash')->setNotnull(false), DbalSchemaFactory::columnForNodeTypeName('nodetypename'), + (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('properties', Type::getType(Types::TEXT)))->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION), (new Column('classification', Type::getType(Types::BINARY)))->setLength(20)->setNotnull(true), (new Column('created', Type::getType(Types::DATETIME_IMMUTABLE)))->setDefault('CURRENT_TIMESTAMP')->setNotnull(true), @@ -55,8 +56,7 @@ private function createNodeTable(): Table private function createHierarchyRelationTable(): Table { - $table = new Table($this->tableNamePrefix . '_hierarchyrelation', [ - (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), + $table = new Table($this->contentGraphTableNames->hierarchyRelation(), [ (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true), @@ -66,13 +66,16 @@ private function createHierarchyRelationTable(): Table ]); return $table - ->setPrimaryKey(['childnodeanchor', 'contentstreamid', 'dimensionspacepointhash', 'parentnodeanchor']) + ->addIndex(['childnodeanchor']) + ->addIndex(['contentstreamid']) + ->addIndex(['parentnodeanchor']) + ->addIndex(['contentstreamid', 'childnodeanchor', 'dimensionspacepointhash']) ->addIndex(['contentstreamid', 'dimensionspacepointhash']); } private function createDimensionSpacePointsTable(): Table { - $table = new Table($this->tableNamePrefix . '_dimensionspacepoints', [ + $table = new Table($this->contentGraphTableNames->dimensionSpacePoints(), [ DbalSchemaFactory::columnForDimensionSpacePointHash('hash')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotnull(true) ]); @@ -83,7 +86,7 @@ private function createDimensionSpacePointsTable(): Table private function createReferenceRelationTable(): Table { - $table = new Table($this->tableNamePrefix . '_referencerelation', [ + $table = new Table($this->contentGraphTableNames->referenceRelation(), [ (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(true)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForNodeAnchorPoint('nodeanchorpoint'), diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory.php index 2808fdf6301..2daf3c6a5b9 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory.php @@ -25,7 +25,7 @@ public function build( return new ProjectionIntegrityViolationDetectionRunner( new ProjectionIntegrityViolationDetector( $this->dbalClient, - DoctrineDbalContentGraphProjectionFactory::graphProjectionTableNamePrefix( + ContentGraphTableNames::create( $serviceFactoryDependencies->contentRepositoryId ) ) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index f779ad9ff64..efa4f0badd9 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -6,17 +6,14 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Driver\Exception; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\EventCouldNotBeAppliedToContentGraph; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; +use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSibling; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\ParentNodeMoveDestination; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -30,8 +27,6 @@ trait NodeMove { abstract protected function getProjectionContentGraph(): ProjectionContentGraph; - abstract protected function getTableNamePrefix(): string; - /** * @param NodeAggregateWasMoved $event * @throws \Throwable @@ -39,44 +34,36 @@ abstract protected function getTableNamePrefix(): string; private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void { $this->transactional(function () use ($event) { - foreach ($event->nodeMoveMappings as $moveNodeMapping) { - // for each materialized node in the DB which we want to adjust, we have one MoveNodeMapping. - $nodeToBeMoved = $this->getProjectionContentGraph()->findNodeByIds( + foreach ($event->succeedingSiblingsForCoverage as $succeedingSiblingForCoverage) { + $nodeToBeMoved = $this->getProjectionContentGraph()->findNodeInAggregate( $event->contentStreamId, $event->nodeAggregateId, - $moveNodeMapping->movedNodeOrigin + $succeedingSiblingForCoverage->dimensionSpacePoint ); if (is_null($nodeToBeMoved)) { throw EventCouldNotBeAppliedToContentGraph::becauseTheSourceNodeIsMissing(get_class($event)); } - foreach ($moveNodeMapping->newLocations as $newLocation) { - assert($newLocation instanceof CoverageNodeMoveMapping); - - $affectedDimensionSpacePoints = new DimensionSpacePointSet([ - $newLocation->coveredDimensionSpacePoint - ]); - - // do the move (depending on how the move target is specified) - $newParentNodeAggregateId = match ($newLocation->destination::class) { - SucceedingSiblingNodeMoveDestination::class => $this->moveNodeBeforeSucceedingSibling( - $event->contentStreamId, - $nodeToBeMoved, - $newLocation->coveredDimensionSpacePoint, - $newLocation->destination - ), - ParentNodeMoveDestination::class => $newLocation->destination->nodeAggregateId, - }; - if ($newLocation->destination instanceof ParentNodeMoveDestination) { - $this->moveNodeIntoParent( - $event->contentStreamId, - $nodeToBeMoved, - $newLocation->coveredDimensionSpacePoint, - $newLocation->destination - ); - } - $this->moveSubtreeTags($event->contentStreamId, $event->nodeAggregateId, $newParentNodeAggregateId, $newLocation->coveredDimensionSpacePoint); + if ($event->newParentNodeAggregateId) { + $this->moveNodeBeneathParent( + $event->contentStreamId, + $nodeToBeMoved, + $event->newParentNodeAggregateId, + $succeedingSiblingForCoverage + ); + $this->moveSubtreeTags( + $event->contentStreamId, + $event->newParentNodeAggregateId, + $succeedingSiblingForCoverage->dimensionSpacePoint + ); + } else { + $this->moveNodeBeforeSucceedingSibling( + $event->contentStreamId, + $nodeToBeMoved, + $succeedingSiblingForCoverage, + ); + // subtree tags stay the same if the parent doesn't change } } }); @@ -84,90 +71,72 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void /** * This helper is responsible for moving a single incoming HierarchyRelation of $nodeToBeMoved - * to a new location. $coveredDimensionSpacePointWhereMoveShouldHappen specifies which incoming HierarchyRelation - * should be moved. + * to a new location without changing the parent. $succeedingSiblingForCoverage specifies + * which incoming HierarchyRelation should be moved and where exactly. * * The move target is given as $succeedingSiblingNodeMoveTarget. This also specifies the new parent node. * @throws \Exception - * @throws Exception - * @return NodeAggregateId the PARENT's NodeAggregateId */ private function moveNodeBeforeSucceedingSibling( ContentStreamId $contentStreamId, NodeRecord $nodeToBeMoved, - DimensionSpacePoint $coveredDimensionSpacePointWhereMoveShouldHappen, - SucceedingSiblingNodeMoveDestination $succeedingSiblingNodeMoveDestination, - ): NodeAggregateId { + InterdimensionalSibling $succeedingSiblingForCoverage, + ): void { $projectionContentGraph = $this->getProjectionContentGraph(); // find the single ingoing hierarchy relation which we want to move $ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved( $nodeToBeMoved, $contentStreamId, - $coveredDimensionSpacePointWhereMoveShouldHappen + $succeedingSiblingForCoverage->dimensionSpacePoint ); - // find the new succeeding sibling NodeRecord; and the new parent NodeRecord (which is the - // succeeding sibling's parent). We need these records because we'll use their RelationAnchorPoints - // later. - $newSucceedingSibling = $projectionContentGraph->findNodeByIds( - $contentStreamId, - $succeedingSiblingNodeMoveDestination->nodeAggregateId, - $succeedingSiblingNodeMoveDestination->originDimensionSpacePoint - ); - if ($newSucceedingSibling === null) { - throw EventCouldNotBeAppliedToContentGraph::becauseTheTargetSucceedingSiblingNodeIsMissing( - MoveNodeAggregate::class - ); - } - - $newParent = $projectionContentGraph->findNodeByIds( - $contentStreamId, - $succeedingSiblingNodeMoveDestination->parentNodeAggregateId, - $succeedingSiblingNodeMoveDestination->parentOriginDimensionSpacePoint, - ); - if ($newParent === null) { - // this should NEVER happen; because this would mean $newSucceedingSibling - // wouldn't have a parent => invariant violation. - throw EventCouldNotBeAppliedToContentGraph::becauseTheTargetSucceedingSiblingNodesParentIsMissing( - MoveNodeAggregate::class + $newSucceedingSibling = null; + if ($succeedingSiblingForCoverage->nodeAggregateId) { + // find the new succeeding sibling NodeRecord; We need this record because we'll use its RelationAnchorPoint later. + $newSucceedingSibling = $projectionContentGraph->findNodeInAggregate( + $contentStreamId, + $succeedingSiblingForCoverage->nodeAggregateId, + $succeedingSiblingForCoverage->dimensionSpacePoint ); + if ($newSucceedingSibling === null) { + throw EventCouldNotBeAppliedToContentGraph::becauseTheTargetSucceedingSiblingNodeIsMissing( + MoveNodeAggregate::class + ); + } } - // assign new position + // fetch... $newPosition = $this->getRelationPosition( - $newParent->relationAnchorPoint, + $ingoingHierarchyRelation->parentNodeAnchor, null, - $newSucceedingSibling->relationAnchorPoint, + $newSucceedingSibling?->relationAnchorPoint, $contentStreamId, - $coveredDimensionSpacePointWhereMoveShouldHappen + $succeedingSiblingForCoverage->dimensionSpacePoint ); - // this is the actual move - $ingoingHierarchyRelation->assignNewParentNode( - $newParent->relationAnchorPoint, + // ...and assign the new position + $ingoingHierarchyRelation->assignNewPosition( $newPosition, $this->getDatabaseConnection(), - $this->getTableNamePrefix() + $this->tableNames ); - - return $newParent->nodeAggregateId; } /** * This helper is responsible for moving a single incoming HierarchyRelation of $nodeToBeMoved - * to a new location. $coveredDimensionSpacePointWhereMoveShouldHappen specifies which incoming HierarchyRelation - * should be moved. + * to a new location including a change of parent. $succeedingSiblingForCoverage specifies + * which incoming HierarchyRelation should be moved and where exactly. * - * The move target is given as $parentNodeMoveTarget. We always move to the END of the children list of the - * given parent. + * The move target is given as $parentNodeAggregateId and $succeedingSiblingForCoverage. + * We always move beneath the parent before the succeeding sibling if given (or to the end) * @throws DBALException */ - private function moveNodeIntoParent( + private function moveNodeBeneathParent( ContentStreamId $contentStreamId, NodeRecord $nodeToBeMoved, - DimensionSpacePoint $coveredDimensionSpacePointWhereMoveShouldHappen, - ParentNodeMoveDestination $parentNodeMoveDestination + NodeAggregateId $parentNodeAggregateId, + InterdimensionalSibling $succeedingSiblingForCoverage, ): void { $projectionContentGraph = $this->getProjectionContentGraph(); @@ -175,15 +144,14 @@ private function moveNodeIntoParent( $ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved( $nodeToBeMoved, $contentStreamId, - $coveredDimensionSpacePointWhereMoveShouldHappen + $succeedingSiblingForCoverage->dimensionSpacePoint ); - // find the new parent NodeRecord (specified by $parentNodeMoveTarget). - // We need this record because we'll use its RelationAnchorPoints later. - $newParent = $projectionContentGraph->findNodeByIds( + // find the new parent NodeRecord; We need this record because we'll use its RelationAnchorPoints later. + $newParent = $projectionContentGraph->findNodeInAggregate( $contentStreamId, - $parentNodeMoveDestination->nodeAggregateId, - $parentNodeMoveDestination->originDimensionSpacePoint + $parentNodeAggregateId, + $succeedingSiblingForCoverage->dimensionSpacePoint ); if ($newParent === null) { throw EventCouldNotBeAppliedToContentGraph::becauseTheTargetParentNodeIsMissing( @@ -191,14 +159,28 @@ private function moveNodeIntoParent( ); } + $newSucceedingSibling = null; + if ($succeedingSiblingForCoverage->nodeAggregateId) { + // find the new succeeding sibling NodeRecord; We need this record because we'll use its RelationAnchorPoint later. + $newSucceedingSibling = $projectionContentGraph->findNodeInAggregate( + $contentStreamId, + $succeedingSiblingForCoverage->nodeAggregateId, + $succeedingSiblingForCoverage->dimensionSpacePoint + ); + if ($newSucceedingSibling === null) { + throw EventCouldNotBeAppliedToContentGraph::becauseTheTargetSucceedingSiblingNodeIsMissing( + MoveNodeAggregate::class + ); + } + } + // assign new position $newPosition = $this->getRelationPosition( $newParent->relationAnchorPoint, null, - // move to end of children - null, + $newSucceedingSibling?->relationAnchorPoint, $contentStreamId, - $coveredDimensionSpacePointWhereMoveShouldHappen + $succeedingSiblingForCoverage->dimensionSpacePoint ); // this is the actual move @@ -206,7 +188,7 @@ private function moveNodeIntoParent( $newParent->relationAnchorPoint, $newPosition, $this->getDatabaseConnection(), - $this->getTableNamePrefix() + $this->tableNames ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php index 86fbb00b696..41f00af02fa 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php @@ -20,8 +20,6 @@ trait NodeRemoval { abstract protected function getProjectionContentGraph(): ProjectionContentGraph; - abstract protected function getTableNamePrefix(): string; - protected LoggerInterface $systemLogger; /** @@ -51,7 +49,7 @@ private function whenNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): vo protected function removeRelationRecursivelyFromDatabaseIncludingNonReferencedNodes( HierarchyRelation $ingoingRelation ): void { - $ingoingRelation->removeFromDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); + $ingoingRelation->removeFromDatabase($this->getDatabaseConnection(), $this->tableNames); foreach ( $this->getProjectionContentGraph()->findOutgoingHierarchyRelationsForNode( @@ -67,11 +65,11 @@ protected function removeRelationRecursivelyFromDatabaseIncludingNonReferencedNo // also remove outbound reference relations $this->getDatabaseConnection()->executeStatement( ' - DELETE n, r FROM ' . $this->getTableNamePrefix() . '_node n - LEFT JOIN ' . $this->getTableNamePrefix() . '_referencerelation r + DELETE n, r FROM ' . $this->tableNames->node() . ' n + LEFT JOIN ' . $this->tableNames->referenceRelation() . ' r ON r.nodeanchorpoint = n.relationanchorpoint LEFT JOIN - ' . $this->getTableNamePrefix() . '_hierarchyrelation h + ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE n.relationanchorpoint = :anchorPointForNode diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php index 27b2322512e..53a1a5004eb 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php @@ -31,8 +31,6 @@ trait NodeVariation { abstract protected function getProjectionContentGraph(): ProjectionContentGraph; - abstract protected function getTableNamePrefix(): string; - /** * @param NodeSpecializationVariantWasCreated $event * @throws \Exception @@ -68,7 +66,7 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria $hierarchyRelation->assignNewChildNode( $specializedNode->relationAnchorPoint, $this->getDatabaseConnection(), - $this->tableNamePrefix + $this->tableNames ); unset($uncoveredDimensionSpacePoints[$hierarchyRelation->dimensionSpacePointHash]); } @@ -107,7 +105,6 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria $hierarchyRelation = new HierarchyRelation( $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, - $sourceNode->nodeName, $event->contentStreamId, $uncoveredDimensionSpacePoint, $uncoveredDimensionSpacePoint->hash, @@ -120,7 +117,7 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria ), NodeTags::create(SubtreeTags::createEmpty(), $parentSubtreeTags->all()), ); - $hierarchyRelation->addToDatabase($this->getDatabaseConnection(), $this->getTableNamePrefix()); + $hierarchyRelation->addToDatabase($this->getDatabaseConnection(), $this->tableNames); } } @@ -135,7 +132,7 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria $specializedNode->relationAnchorPoint, null, $this->getDatabaseConnection(), - $this->getTableNamePrefix() + $this->tableNames ); } @@ -189,7 +186,7 @@ public function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVarian $existingIngoingHierarchyRelation->assignNewChildNode( $generalizedNode->relationAnchorPoint, $this->getDatabaseConnection(), - $this->tableNamePrefix + $this->tableNames ); $unassignedIngoingDimensionSpacePoints = $unassignedIngoingDimensionSpacePoints->getDifference( new DimensionSpacePointSet([ @@ -209,7 +206,7 @@ public function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVarian $generalizedNode->relationAnchorPoint, null, $this->getDatabaseConnection(), - $this->getTableNamePrefix() + $this->tableNames ); } @@ -301,7 +298,7 @@ public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, $existingIngoingHierarchyRelation->assignNewChildNode( $peerNode->relationAnchorPoint, $this->getDatabaseConnection(), - $this->tableNamePrefix + $this->tableNames ); $unassignedIngoingDimensionSpacePoints = $unassignedIngoingDimensionSpacePoints->getDifference( new DimensionSpacePointSet([ @@ -321,7 +318,7 @@ public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, $peerNode->relationAnchorPoint, null, $this->getDatabaseConnection(), - $this->getTableNamePrefix() + $this->tableNames ); } @@ -360,7 +357,6 @@ public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, $peerNode->relationAnchorPoint, new DimensionSpacePointSet([$coveredDimensionSpacePoint]), $peerSucceedingSiblingNode?->relationAnchorPoint, - $sourceNode->nodeName ); } @@ -393,7 +389,6 @@ abstract protected function connectHierarchy( NodeRelationAnchorPoint $childNodeAnchorPoint, DimensionSpacePointSet $dimensionSpacePointSet, ?NodeRelationAnchorPoint $succeedingSiblingNodeAnchorPoint, - NodeName $relationName = null ): void; abstract protected function copyReferenceRelations( diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php index 976526a10de..2564a0a36ef 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php @@ -21,21 +21,19 @@ */ trait SubtreeTagging { - abstract protected function getTableNamePrefix(): string; - /** * @throws \Throwable */ private function whenSubtreeWasTagged(SubtreeWasTagged $event): void { $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h + UPDATE ' . $this->tableNames->hierarchyRelation() . ' h SET h.subtreetags = JSON_INSERT(h.subtreetags, :tagPath, null) WHERE h.childnodeanchor IN ( WITH RECURSIVE cte (id) AS ( SELECT ch.childnodeanchor - FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation ch - INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.parentnodeanchor + FROM ' . $this->tableNames->hierarchyRelation() . ' ch + INNER JOIN ' . $this->tableNames->node() . ' n ON n.relationanchorpoint = ch.parentnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId AND ch.contentstreamid = :contentStreamId @@ -46,7 +44,7 @@ private function whenSubtreeWasTagged(SubtreeWasTagged $event): void dh.childnodeanchor FROM cte - JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation dh ON dh.parentnodeanchor = cte.id + JOIN ' . $this->tableNames->hierarchyRelation() . ' dh ON dh.parentnodeanchor = cte.id WHERE NOT JSON_CONTAINS_PATH(dh.subtreetags, \'one\', :tagPath) ) @@ -64,8 +62,8 @@ private function whenSubtreeWasTagged(SubtreeWasTagged $event): void ]); $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h - INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = h.childnodeanchor + UPDATE ' . $this->tableNames->hierarchyRelation() . ' h + INNER JOIN ' . $this->tableNames->node() . ' n ON n.relationanchorpoint = h.childnodeanchor SET h.subtreetags = JSON_SET(h.subtreetags, :tagPath, true) WHERE n.nodeaggregateid = :nodeAggregateId @@ -87,15 +85,15 @@ private function whenSubtreeWasTagged(SubtreeWasTagged $event): void private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void { $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h - INNER JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation ph ON ph.childnodeanchor = h.parentnodeanchor + UPDATE ' . $this->tableNames->hierarchyRelation() . ' h + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' ph ON ph.childnodeanchor = h.parentnodeanchor SET h.subtreetags = IF(( SELECT JSON_CONTAINS_PATH(ph.subtreetags, \'one\', :tagPath) FROM - ' . $this->getTableNamePrefix() . '_hierarchyrelation ph - INNER JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation ch ON ch.parentnodeanchor = ph.childnodeanchor - INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.childnodeanchor + ' . $this->tableNames->hierarchyRelation() . ' ph + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' ch ON ch.parentnodeanchor = ph.childnodeanchor + INNER JOIN ' . $this->tableNames->node() . ' n ON n.relationanchorpoint = ch.childnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId AND ph.contentstreamid = :contentStreamId @@ -105,8 +103,8 @@ private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void WHERE h.childnodeanchor IN ( WITH RECURSIVE cte (id) AS ( SELECT ch.childnodeanchor - FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation ch - INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.childnodeanchor + FROM ' . $this->tableNames->hierarchyRelation() . ' ch + INNER JOIN ' . $this->tableNames->node() . ' n ON n.relationanchorpoint = ch.childnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId AND ch.contentstreamid = :contentStreamId @@ -116,7 +114,7 @@ private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void dh.childnodeanchor FROM cte - JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation dh ON dh.parentnodeanchor = cte.id + JOIN ' . $this->tableNames->hierarchyRelation() . ' dh ON dh.parentnodeanchor = cte.id WHERE JSON_EXTRACT(dh.subtreetags, :tagPath) != TRUE ) @@ -134,81 +132,62 @@ private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void ]); } - private function moveSubtreeTags(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, NodeAggregateId $newParentNodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint): void - { - $nodeTags = $this->nodeTagsForNode($nodeAggregateId, $contentStreamId, $coveredDimensionSpacePoint); - $newParentSubtreeTags = $this->nodeTagsForNode($newParentNodeAggregateId, $contentStreamId, $coveredDimensionSpacePoint); - $newSubtreeTags = []; - foreach ($nodeTags->withoutInherited() as $tag) { - $newSubtreeTags[$tag->value] = true; - } - foreach ($newParentSubtreeTags as $tag) { - $newSubtreeTags[$tag->value] = null; - } - if ($newSubtreeTags === [] && $nodeTags->isEmpty()) { - return; - } + private function moveSubtreeTags( + ContentStreamId $contentStreamId, + NodeAggregateId $newParentNodeAggregateId, + DimensionSpacePoint $coveredDimensionSpacePoint + ): void { $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h - SET h.subtreetags = JSON_MERGE_PATCH(:newParentTags, JSON_MERGE_PATCH(\'{}\', h.subtreetags)) - WHERE h.childnodeanchor IN ( - WITH RECURSIVE cte (id) AS ( - SELECT ch.childnodeanchor - FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation ch - INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.parentnodeanchor + UPDATE ' . $this->tableNames->hierarchyRelation() . ' h, + ( + WITH RECURSIVE cte AS ( + SELECT + JSON_KEYS(th.subtreetags) subtreeTagsToInherit, th.childnodeanchor + FROM + ' . $this->tableNames->hierarchyRelation() . ' th + INNER JOIN ' . $this->tableNames->node() . ' tn ON tn.relationanchorpoint = th.childnodeanchor WHERE - n.nodeaggregateid = :nodeAggregateId - AND ch.contentstreamid = :contentStreamId - AND ch.dimensionspacepointhash = :dimensionSpacePointHash - UNION ALL + tn.nodeaggregateid = :newParentNodeAggregateId + AND th.contentstreamid = :contentStreamId + AND th.dimensionspacepointhash = :dimensionSpacePointHash + UNION SELECT - dh.childnodeanchor + JSON_MERGE_PRESERVE( + cte.subtreeTagsToInherit, + JSON_KEYS(JSON_MERGE_PATCH( + \'{}\', + dh.subtreetags + )) + ) subtreeTagsToInherit, + dh.childnodeanchor FROM cte - JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation dh ON dh.parentnodeanchor = cte.id + JOIN ' . $this->tableNames->hierarchyRelation() . ' dh + ON + dh.parentnodeanchor = cte.childnodeanchor + AND dh.contentstreamid = :contentStreamId + AND dh.dimensionspacepointhash = :dimensionSpacePointHash ) - SELECT id FROM cte + SELECT * FROM cte + ) AS r + SET h.subtreetags = ( + SELECT + JSON_MERGE_PATCH( + IFNULL(JSON_OBJECTAGG(htk.k, null), \'{}\'), + JSON_MERGE_PATCH(\'{}\', h.subtreetags) + ) + FROM + JSON_TABLE(r.subtreeTagsToInherit, \'$[*]\' COLUMNS (k VARCHAR(36) PATH \'$\')) htk ) - ', [ - 'contentStreamId' => $contentStreamId->value, - 'nodeAggregateId' => $nodeAggregateId->value, - 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash, - 'newParentTags' => json_encode($newSubtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), - ]); - $this->getDatabaseConnection()->executeStatement(' - UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h - INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = h.childnodeanchor - SET h.subtreetags = :newParentTags WHERE - n.nodeaggregateid = :nodeAggregateId + h.childnodeanchor = r.childnodeanchor AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash - ', [ - 'contentStreamId' => $contentStreamId->value, - 'nodeAggregateId' => $nodeAggregateId->value, - 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash, - 'newParentTags' => json_encode($newSubtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), - ]); - } - - private function nodeTagsForNode(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): NodeTags - { - $subtreeTagsJson = $this->getDatabaseConnection()->fetchOne(' - SELECT h.subtreetags FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation h - INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = h.childnodeanchor - WHERE - n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamid = :contentStreamId - AND h.dimensionspacepointhash = :dimensionSpacePointHash ', [ - 'nodeAggregateId' => $nodeAggregateId->value, 'contentStreamId' => $contentStreamId->value, - 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, + 'newParentNodeAggregateId' => $newParentNodeAggregateId->value, + 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash, ]); - if (!is_string($subtreeTagsJson)) { - throw new \RuntimeException(sprintf('Failed to fetch SubtreeTags for node "%s" in content subgraph "%s@%s"', $nodeAggregateId->value, $dimensionSpacePoint->toJson(), $contentStreamId->value), 1698838865); - } - return NodeFactory::extractNodeTagsFromJson($subtreeTagsJson); } private function subtreeTagsForHierarchyRelation(ContentStreamId $contentStreamId, NodeRelationAnchorPoint $parentNodeAnchorPoint, DimensionSpacePoint $dimensionSpacePoint): NodeTags @@ -217,7 +196,7 @@ private function subtreeTagsForHierarchyRelation(ContentStreamId $contentStreamI return NodeTags::createEmpty(); } $subtreeTagsJson = $this->getDatabaseConnection()->fetchOne(' - SELECT h.subtreetags FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation h + SELECT h.subtreetags FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.childnodeanchor = :parentNodeAnchorPoint AND h.contentstreamid = :contentStreamId diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php index 8bc02f1e7d3..81d4218d56e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php @@ -15,10 +15,10 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; use Doctrine\DBAL\Connection; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @@ -31,7 +31,6 @@ public function __construct( public NodeRelationAnchorPoint $parentNodeAnchor, public NodeRelationAnchorPoint $childNodeAnchor, - public ?NodeName $name, public ContentStreamId $contentStreamId, public DimensionSpacePoint $dimensionSpacePoint, public string $dimensionSpacePointHash, @@ -43,18 +42,17 @@ public function __construct( /** * @param Connection $databaseConnection */ - public function addToDatabase(Connection $databaseConnection, string $tableNamePrefix): void + public function addToDatabase(Connection $databaseConnection, ContentGraphTableNames $tableNames): void { $databaseConnection->transactional(function ($databaseConnection) use ( - $tableNamePrefix + $tableNames ) { - $dimensionSpacePoints = new DimensionSpacePointsRepository($databaseConnection, $tableNamePrefix); + $dimensionSpacePoints = new DimensionSpacePointsRepository($databaseConnection, $tableNames); $dimensionSpacePoints->insertDimensionSpacePoint($this->dimensionSpacePoint); - $databaseConnection->insert($tableNamePrefix . '_hierarchyrelation', [ + $databaseConnection->insert($tableNames->hierarchyRelation(), [ 'parentnodeanchor' => $this->parentNodeAnchor->value, 'childnodeanchor' => $this->childNodeAnchor->value, - 'name' => $this->name?->value, 'contentstreamid' => $this->contentStreamId->value, 'dimensionspacepointhash' => $this->dimensionSpacePointHash, 'position' => $this->position, @@ -66,9 +64,9 @@ public function addToDatabase(Connection $databaseConnection, string $tableNameP /** * @param Connection $databaseConnection */ - public function removeFromDatabase(Connection $databaseConnection, string $tableNamePrefix): void + public function removeFromDatabase(Connection $databaseConnection, ContentGraphTableNames $tableNames): void { - $databaseConnection->delete($tableNamePrefix . '_hierarchyrelation', $this->getDatabaseId()); + $databaseConnection->delete($tableNames->hierarchyRelation(), $this->getDatabaseId()); } /** @@ -78,10 +76,10 @@ public function removeFromDatabase(Connection $databaseConnection, string $table public function assignNewChildNode( NodeRelationAnchorPoint $childAnchorPoint, Connection $databaseConnection, - string $tableNamePrefix + ContentGraphTableNames $tableNames ): void { $databaseConnection->update( - $tableNamePrefix . '_hierarchyrelation', + $tableNames->hierarchyRelation(), [ 'childnodeanchor' => $childAnchorPoint->value ], @@ -96,7 +94,7 @@ public function assignNewParentNode( NodeRelationAnchorPoint $parentAnchorPoint, ?int $position, Connection $databaseConnection, - string $tableNamePrefix + ContentGraphTableNames $tableNames ): void { $data = [ 'parentnodeanchor' => $parentAnchorPoint->value @@ -105,16 +103,16 @@ public function assignNewParentNode( $data['position'] = $position; } $databaseConnection->update( - $tableNamePrefix . '_hierarchyrelation', + $tableNames->hierarchyRelation(), $data, $this->getDatabaseId() ); } - public function assignNewPosition(int $position, Connection $databaseConnection, string $tableNamePrefix): void + public function assignNewPosition(int $position, Connection $databaseConnection, ContentGraphTableNames $tableNames): void { $databaseConnection->update( - $tableNamePrefix . '_hierarchyrelation', + $tableNames->hierarchyRelation(), [ 'position' => $position ], diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index fbc75314807..5af86607ea4 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -15,8 +15,8 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Types\Types; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\NodeType\NodeTypeName; @@ -41,25 +41,24 @@ public function __construct( public SerializedPropertyValues $properties, public NodeTypeName $nodeTypeName, public NodeAggregateClassification $classification, - /** Transient node name to store a node name after fetching a node with hierarchy (not always available) */ public ?NodeName $nodeName, public Timestamps $timestamps, ) { } /** - * @param Connection $databaseConnection * @throws \Doctrine\DBAL\DBALException */ - public function updateToDatabase(Connection $databaseConnection, string $tableNamePrefix): void + public function updateToDatabase(Connection $databaseConnection, ContentGraphTableNames $tableNames): void { $databaseConnection->update( - $tableNamePrefix . '_node', + $tableNames->node(), [ 'nodeaggregateid' => $this->nodeAggregateId->value, 'origindimensionspacepointhash' => $this->originDimensionSpacePointHash, 'properties' => json_encode($this->properties), 'nodetypename' => $this->nodeTypeName->value, + 'name' => $this->nodeName?->value, 'classification' => $this->classification->value, 'lastmodified' => $this->timestamps->lastModified, 'originallastmodified' => $this->timestamps->originalLastModified, @@ -74,18 +73,6 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa ); } - /** - * @param Connection $databaseConnection - * @throws \Doctrine\DBAL\DBALException - * @throws \Doctrine\DBAL\Exception\InvalidArgumentException - */ - public function removeFromDatabase(Connection $databaseConnection, string $tableNamePrefix): void - { - $databaseConnection->delete($tableNamePrefix . '_node', [ - 'relationanchorpoint' => $this->relationAnchorPoint->value - ]); - } - /** * @param array $databaseRow * @throws \Exception @@ -113,22 +100,12 @@ public static function fromDatabaseRow(array $databaseRow): self /** * Insert a node record with the given data and return it. * - * @param Connection $databaseConnection - * @param string $tableNamePrefix - * @param NodeAggregateId $nodeAggregateId * @param array $originDimensionSpacePoint - * @param string $originDimensionSpacePointHash - * @param SerializedPropertyValues $properties - * @param NodeTypeName $nodeTypeName - * @param NodeAggregateClassification $classification - * @param NodeName|null $nodeName - * @param Timestamps $timestamps - * @return self * @throws \Doctrine\DBAL\Exception */ public static function createNewInDatabase( Connection $databaseConnection, - string $tableNamePrefix, + ContentGraphTableNames $tableNames, NodeAggregateId $nodeAggregateId, array $originDimensionSpacePoint, string $originDimensionSpacePointHash, @@ -140,23 +117,25 @@ public static function createNewInDatabase( Timestamps $timestamps, ): self { $relationAnchorPoint = $databaseConnection->transactional(function ($databaseConnection) use ( - $tableNamePrefix, + $tableNames, $nodeAggregateId, $originDimensionSpacePoint, $originDimensionSpacePointHash, $properties, $nodeTypeName, + $nodeName, $classification, $timestamps ) { - $dimensionSpacePoints = new DimensionSpacePointsRepository($databaseConnection, $tableNamePrefix); + $dimensionSpacePoints = new DimensionSpacePointsRepository($databaseConnection, $tableNames); $dimensionSpacePoints->insertDimensionSpacePointByHashAndCoordinates($originDimensionSpacePointHash, $originDimensionSpacePoint); - $databaseConnection->insert($tableNamePrefix . '_node', [ + $databaseConnection->insert($tableNames->node(), [ 'nodeaggregateid' => $nodeAggregateId->value, 'origindimensionspacepointhash' => $originDimensionSpacePointHash, 'properties' => json_encode($properties), 'nodetypename' => $nodeTypeName->value, + 'name' => $nodeName?->value, 'classification' => $classification->value, 'created' => $timestamps->created, 'originalcreated' => $timestamps->originalCreated, @@ -188,20 +167,16 @@ public static function createNewInDatabase( /** * Creates a copy of this NodeRecord with a new anchor point. * - * @param Connection $databaseConnection - * @param string $tableNamePrefix - * @param NodeRecord $copyFrom - * @return self * @throws \Doctrine\DBAL\Exception */ public static function createCopyFromNodeRecord( Connection $databaseConnection, - string $tableNamePrefix, + ContentGraphTableNames $tableNames, NodeRecord $copyFrom ): self { return self::createNewInDatabase( $databaseConnection, - $tableNamePrefix, + $tableNames, $copyFrom->nodeAggregateId, $copyFrom->originDimensionSpacePoint, $copyFrom->originDimensionSpacePointHash, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php index adb394fb6cd..78722fbff69 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php @@ -14,6 +14,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; @@ -33,7 +34,7 @@ final class ProjectionIntegrityViolationDetector implements ProjectionIntegrityV { public function __construct( private readonly DbalClientInterface $client, - private readonly string $tableNamePrefix + private readonly ContentGraphTableNames $tableNames ) { } @@ -45,9 +46,9 @@ public function hierarchyIntegrityIsProvided(): Result $result = new Result(); $disconnectedHierarchyRelationRecords = $this->client->getConnection()->executeQuery( - 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h - LEFT JOIN ' . $this->tableNamePrefix . '_node p ON h.parentnodeanchor = p.relationanchorpoint - LEFT JOIN ' . $this->tableNamePrefix . '_node c ON h.childnodeanchor = c.relationanchorpoint + 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h + LEFT JOIN ' . $this->tableNames->node() . ' p ON h.parentnodeanchor = p.relationanchorpoint + LEFT JOIN ' . $this->tableNames->node() . ' c ON h.childnodeanchor = c.relationanchorpoint WHERE h.parentnodeanchor != :rootNodeAnchor AND ( p.relationanchorpoint IS NULL @@ -67,7 +68,7 @@ public function hierarchyIntegrityIsProvided(): Result } $invalidlyHashedHierarchyRelationRecords = $this->client->getConnection()->executeQuery( - 'SELECT * FROM ' . $this->tableNamePrefix . '_hierarchyrelation h LEFT JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON dsp.hash = h.dimensionspacepointhash + 'SELECT * FROM ' . $this->tableNames->hierarchyRelation() . ' h LEFT JOIN ' . $this->tableNames->dimensionSpacePoints() . ' dsp ON dsp.hash = h.dimensionspacepointhash HAVING dsp.dimensionspacepoint IS NULL' )->fetchAllAssociative(); @@ -80,9 +81,9 @@ public function hierarchyIntegrityIsProvided(): Result } $hierarchyRelationRecordsAppearingMultipleTimes = $this->client->getConnection()->executeQuery( - 'SELECT COUNT(*) as uniquenessCounter, h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h - LEFT JOIN ' . $this->tableNamePrefix . '_node p ON h.parentnodeanchor = p.relationanchorpoint - LEFT JOIN ' . $this->tableNamePrefix . '_node c ON h.childnodeanchor = c.relationanchorpoint + 'SELECT COUNT(*) as uniquenessCounter, h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h + LEFT JOIN ' . $this->tableNames->node() . ' p ON h.parentnodeanchor = p.relationanchorpoint + LEFT JOIN ' . $this->tableNames->node() . ' c ON h.childnodeanchor = c.relationanchorpoint WHERE h.parentnodeanchor != :rootNodeAnchor GROUP BY p.nodeaggregateid, c.nodeaggregateid, h.dimensionspacepointhash, h.contentstreamid @@ -113,7 +114,7 @@ public function siblingsAreDistinctlySorted(): Result $ambiguouslySortedHierarchyRelationRecords = $this->client->getConnection()->executeQuery( 'SELECT *, COUNT(position) - FROM ' . $this->tableNamePrefix . '_hierarchyrelation + FROM ' . $this->tableNames->hierarchyRelation() . ' GROUP BY position, parentnodeanchor, contentstreamid, dimensionspacepointhash HAVING COUNT(position) > 1' ); @@ -127,7 +128,7 @@ public function siblingsAreDistinctlySorted(): Result foreach ($ambiguouslySortedHierarchyRelationRecords as $hierarchyRelationRecord) { $ambiguouslySortedNodeRecords = $this->client->getConnection()->executeQuery( 'SELECT nodeaggregateid - FROM ' . $this->tableNamePrefix . '_node + FROM ' . $this->tableNames->node() . ' WHERE relationanchorpoint = :relationAnchorPoint', [ 'relationAnchorPoint' => $hierarchyRelationRecord['childnodeanchor'] @@ -155,11 +156,11 @@ public function tetheredNodesAreNamed(): Result $result = new Result(); $unnamedTetheredNodeRecords = $this->client->getConnection()->executeQuery( 'SELECT n.nodeaggregateid, h.contentstreamid - FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE n.classification = :tethered - AND h.name IS NULL + AND n.name IS NULL GROUP BY n.nodeaggregateid, h.contentstreamid', [ 'tethered' => NodeAggregateClassification::CLASSIFICATION_TETHERED->value @@ -190,10 +191,10 @@ public function subtreeTagsAreInherited(): Result // This could probably be solved with JSON_ARRAY_INTERSECT(JSON_KEYS(ph.subtreetags), JSON_KEYS(h.subtreetags) but unfortunately that's only available with MariaDB 11.2+ according to https://mariadb.com/kb/en/json_array_intersect/ $hierarchyRelationsWithMissingSubtreeTags = $this->client->getConnection()->executeQuery( 'SELECT - ph.name + ph.* FROM - ' . $this->tableNamePrefix . '_hierarchyrelation h - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph + ' . $this->tableNames->hierarchyRelation() . ' h + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' ph ON ph.childnodeanchor = h.parentnodeanchor AND ph.contentstreamid = h.contentstreamid AND ph.dimensionspacepointhash = h.dimensionspacepointhash @@ -222,9 +223,9 @@ public function referenceIntegrityIsProvided(): Result $result = new Result(); $referenceRelationRecordsDetachedFromSource = $this->client->getConnection()->executeQuery( - 'SELECT * FROM ' . $this->tableNamePrefix . '_referencerelation + 'SELECT * FROM ' . $this->tableNames->referenceRelation() . ' WHERE nodeanchorpoint NOT IN ( - SELECT relationanchorpoint FROM ' . $this->tableNamePrefix . '_node + SELECT relationanchorpoint FROM ' . $this->tableNames->node() . ' )' )->fetchAllAssociative(); @@ -240,13 +241,13 @@ public function referenceIntegrityIsProvided(): Result 'SELECT sh.contentstreamid AS contentstreamId, s.nodeaggregateid AS sourceNodeAggregateId, r.destinationnodeaggregateid AS destinationNodeAggregateId - FROM ' . $this->tableNamePrefix . '_referencerelation r - INNER JOIN ' . $this->tableNamePrefix . '_node s ON r.nodeanchorpoint = s.relationanchorpoint - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation sh + FROM ' . $this->tableNames->referenceRelation() . ' r + INNER JOIN ' . $this->tableNames->node() . ' s ON r.nodeanchorpoint = s.relationanchorpoint + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' sh ON r.nodeanchorpoint = sh.childnodeanchor LEFT JOIN ( - ' . $this->tableNamePrefix . '_node d - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation dh + ' . $this->tableNames->node() . ' d + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' dh ON d.relationanchorpoint = dh.childnodeanchor ) ON r.destinationnodeaggregateid = d.nodeaggregateid AND sh.contentstreamid = dh.contentstreamid @@ -291,7 +292,7 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result SELECT h.childnodeanchor FROM - ' . $this->tableNamePrefix . '_hierarchyrelation h + ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.parentnodeanchor = :rootAnchorPoint AND h.contentstreamid = :contentStreamId @@ -304,14 +305,14 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result h.childnodeanchor FROM subgraph p - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h on h.parentnodeanchor = p.childnodeanchor WHERE h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash ) -SELECT nodeaggregateid FROM ' . $this->tableNamePrefix . '_node n -INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h +SELECT nodeaggregateid FROM ' . $this->tableNames->node() . ' n +INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE h.contentstreamid = :contentStreamId @@ -364,8 +365,8 @@ public function nodeAggregateIdsAreUniquePerSubgraph(): Result foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { $ambiguousNodeAggregateRecords = $this->client->getConnection()->executeQuery( 'SELECT n.nodeaggregateid, COUNT(n.relationanchorpoint) - FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash @@ -401,8 +402,8 @@ public function allNodesHaveAtMostOneParentPerSubgraph(): Result foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { $nodeRecordsWithMultipleParents = $this->client->getConnection()->executeQuery( 'SELECT c.nodeaggregateid - FROM ' . $this->tableNamePrefix . '_node c - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + FROM ' . $this->tableNames->node() . ' c + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = c.relationanchorpoint WHERE h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash @@ -441,8 +442,8 @@ public function nodeAggregatesAreConsistentlyTypedPerContentStream(): Result ) as $nodeAggregateId ) { $nodeAggregateRecords = $this->client->getConnection()->executeQuery( - 'SELECT DISTINCT n.nodetypename FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT DISTINCT n.nodetypename FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE h.contentstreamid = :contentStreamId AND n.nodeaggregateid = :nodeAggregateId', @@ -484,8 +485,8 @@ public function nodeAggregatesAreConsistentlyClassifiedPerContentStream(): Resul ) as $nodeAggregateId ) { $nodeAggregateRecords = $this->client->getConnection()->executeQuery( - 'SELECT DISTINCT n.classification FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT DISTINCT n.classification FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE h.contentstreamid = :contentStreamId AND n.nodeaggregateid = :nodeAggregateId', @@ -523,10 +524,10 @@ public function childNodeCoverageIsASubsetOfParentNodeCoverage(): Result foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { $excessivelyCoveringNodeRecords = $this->client->getConnection()->executeQuery( 'SELECT n.nodeaggregateid, c.dimensionspacepointhash - FROM ' . $this->tableNamePrefix . '_hierarchyrelation c - INNER JOIN ' . $this->tableNamePrefix . '_node n + FROM ' . $this->tableNames->hierarchyRelation() . ' c + INNER JOIN ' . $this->tableNames->node() . ' n ON c.childnodeanchor = n.relationanchorpoint - LEFT JOIN ' . $this->tableNamePrefix . '_hierarchyrelation p + LEFT JOIN ' . $this->tableNames->hierarchyRelation() . ' p ON c.parentnodeanchor = p.childnodeanchor WHERE c.contentstreamid = :contentStreamId AND p.contentstreamid = :contentStreamId @@ -560,16 +561,16 @@ public function allNodesCoverTheirOrigin(): Result foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { $nodeRecordsWithMissingOriginCoverage = $this->client->getConnection()->executeQuery( 'SELECT nodeaggregateid, origindimensionspacepointhash - FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h + FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE h.contentstreamid = :contentStreamId AND nodeaggregateid NOT IN ( -- this query finds all nodes whose origin *IS COVERED* by an incoming hierarchy relation. SELECT n.nodeaggregateid - FROM ' . $this->tableNamePrefix . '_node n - LEFT JOIN ' . $this->tableNamePrefix . '_hierarchyrelation p + FROM ' . $this->tableNames->node() . ' n + LEFT JOIN ' . $this->tableNames->hierarchyRelation() . ' p ON p.childnodeanchor = n.relationanchorpoint AND p.dimensionspacepointhash = n.origindimensionspacepointhash WHERE p.contentstreamid = :contentStreamId @@ -605,7 +606,7 @@ protected function findProjectedContentStreamIds(): iterable $connection = $this->client->getConnection(); $rows = $connection->executeQuery( - 'SELECT DISTINCT contentstreamid FROM ' . $this->tableNamePrefix . '_hierarchyrelation' + 'SELECT DISTINCT contentstreamid FROM ' . $this->tableNames->hierarchyRelation() )->fetchAllAssociative(); return array_map(function (array $row) { @@ -621,7 +622,7 @@ protected function findProjectedContentStreamIds(): iterable protected function findProjectedDimensionSpacePoints(): DimensionSpacePointSet { $records = $this->client->getConnection()->executeQuery( - 'SELECT dimensionspacepoint FROM ' . $this->tableNamePrefix . '_dimensionspacepoints' + 'SELECT dimensionspacepoint FROM ' . $this->tableNames->dimensionSpacePoints() )->fetchAllAssociative(); $records = array_map(function (array $record) { @@ -639,7 +640,7 @@ protected function findProjectedNodeAggregateIdsInContentStream( ContentStreamId $contentStreamId ): array { $records = $this->client->getConnection()->executeQuery( - 'SELECT DISTINCT nodeaggregateid FROM ' . $this->tableNamePrefix . '_node' + 'SELECT DISTINCT nodeaggregateid FROM ' . $this->tableNames->node() )->fetchAllAssociative(); return array_map(function (array $record) { diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 1df35d06fa3..54f5799f5ea 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -19,8 +19,8 @@ use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; -use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; +use Neos\ContentGraph\DoctrineDbalAdapter\NodeQueryBuilder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; @@ -40,6 +40,7 @@ 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; /** * The Doctrine DBAL adapter content graph @@ -64,6 +65,8 @@ */ final class ContentGraph implements ContentGraphInterface { + private readonly NodeQueryBuilder $nodeQueryBuilder; + /** * @var array */ @@ -74,27 +77,40 @@ public function __construct( private readonly NodeFactory $nodeFactory, private readonly ContentRepositoryId $contentRepositoryId, private readonly NodeTypeManager $nodeTypeManager, - private readonly string $tableNamePrefix + private readonly ContentGraphTableNames $tableNames, + public readonly WorkspaceName $workspaceName, + public readonly ContentStreamId $contentStreamId ) { + $this->nodeQueryBuilder = new NodeQueryBuilder($this->client->getConnection(), $this->tableNames); + } + + public function getContentRepositoryId(): ContentRepositoryId + { + return $this->contentRepositoryId; + } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; } - final public function getSubgraph( - ContentStreamId $contentStreamId, + public function getSubgraph( DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints ): ContentSubgraphInterface { - $index = $contentStreamId->value . '-' . $dimensionSpacePoint->hash . '-' . $visibilityConstraints->getHash(); + $index = $dimensionSpacePoint->hash . '-' . $visibilityConstraints->getHash(); if (!isset($this->subgraphs[$index])) { $this->subgraphs[$index] = new ContentSubgraphWithRuntimeCaches( new ContentSubgraph( $this->contentRepositoryId, - $contentStreamId, + $this->workspaceName, + $this->contentStreamId, $dimensionSpacePoint, $visibilityConstraints, $this->client, $this->nodeFactory, $this->nodeTypeManager, - $this->tableNamePrefix + $this->tableNames ) ); } @@ -106,11 +122,9 @@ final public function getSubgraph( * @throws RootNodeAggregateDoesNotExist */ public function findRootNodeAggregateByType( - ContentStreamId $contentStreamId, NodeTypeName $nodeTypeName ): NodeAggregate { $rootNodeAggregates = $this->findRootNodeAggregates( - $contentStreamId, FindRootNodeAggregatesFilter::create(nodeTypeName: $nodeTypeName) ); @@ -127,7 +141,6 @@ public function findRootNodeAggregateByType( } $rootNodeAggregate = $rootNodeAggregates->first(); - if ($rootNodeAggregate === null) { throw RootNodeAggregateDoesNotExist::butWasExpectedTo($nodeTypeName); } @@ -136,199 +149,148 @@ public function findRootNodeAggregateByType( } public function findRootNodeAggregates( - ContentStreamId $contentStreamId, FindRootNodeAggregatesFilter $filter, ): NodeAggregates { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('h.contentstreamid = :contentStreamId') - ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId') - ->setParameters([ - 'contentStreamId' => $contentStreamId->value, - 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value, - ]); - - if ($filter->nodeTypeName !== null) { - $queryBuilder - ->andWhere('n.nodetypename = :nodeTypeName') - ->setParameter('nodeTypeName', $filter->nodeTypeName->value); - } - return NodeAggregates::fromArray(iterator_to_array($this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId))); + $rootNodeAggregateQueryBuilder = $this->nodeQueryBuilder->buildFindRootNodeAggregatesQuery($this->contentStreamId, $filter); + return NodeAggregates::fromArray(iterator_to_array($this->mapQueryBuilderToNodeAggregates($rootNodeAggregateQueryBuilder))); } public function findNodeAggregatesByType( - ContentStreamId $contentStreamId, NodeTypeName $nodeTypeName ): iterable { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('h.contentstreamid = :contentStreamId') + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery(); + $queryBuilder ->andWhere('n.nodetypename = :nodeTypeName') ->setParameters([ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamId' => $this->contentStreamId->value, 'nodeTypeName' => $nodeTypeName->value, ]); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } public function findNodeAggregateById( - ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId ): ?NodeAggregate { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') - ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.childnodeanchor') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('n.nodeaggregateid = :nodeAggregateId') - ->andWhere('h.contentstreamid = :contentStreamId') + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery() + ->andWhere('n.nodeaggregateid = :nodeAggregateId') + ->orderBy('n.relationanchorpoint', 'DESC') ->setParameters([ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value + 'contentStreamId' => $this->contentStreamId->value ]); return $this->nodeFactory->mapNodeRowsToNodeAggregate( $this->fetchRows($queryBuilder), - $contentStreamId, + $this->workspaceName, + $this->contentStreamId, VisibilityConstraints::withoutRestrictions() ); } /** + * Parent node aggregates can have a greater dimension space coverage than the given child. + * Thus, it is not enough to just resolve them from the nodes and edges connected to the given child node aggregate. + * Instead, we resolve all parent node aggregate ids instead and fetch the complete aggregates from there. + * * @return iterable */ public function findParentNodeAggregates( - ContentStreamId $contentStreamId, NodeAggregateId $childNodeAggregateId ): iterable { - $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ph.name, ph.contentstreamid, ph.subtreetags, pdsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->innerJoin('ph', $this->tableNamePrefix . '_dimensionspacepoints', 'pdsp', 'pdsp.hash = ph.dimensionspacepointhash') - ->where('cn.nodeaggregateid = :nodeAggregateId') - ->andWhere('ph.contentstreamid = :contentStreamId') + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery() + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') + ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') ->andWhere('ch.contentstreamid = :contentStreamId') + ->andWhere('cn.nodeaggregateid = :nodeAggregateId') ->setParameters([ 'nodeAggregateId' => $childNodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value + 'contentStreamId' => $this->contentStreamId->value ]); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } - public function findParentNodeAggregateByChildOriginDimensionSpacePoint( - ContentStreamId $contentStreamId, - NodeAggregateId $childNodeAggregateId, - OriginDimensionSpacePoint $childOriginDimensionSpacePoint - ): ?NodeAggregate { + /** + * @return iterable + */ + public function findChildNodeAggregates( + NodeAggregateId $parentNodeAggregateId + ): iterable { + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId); + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); + } + + public function findParentNodeAggregateByChildOriginDimensionSpacePoint(NodeAggregateId $childNodeAggregateId, OriginDimensionSpacePoint $childOriginDimensionSpacePoint): ?NodeAggregate + { $subQueryBuilder = $this->createQueryBuilder() ->select('pn.nodeaggregateid') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') + ->from($this->nodeQueryBuilder->tableNames->node(), 'pn') + ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') ->where('ch.contentstreamid = :contentStreamId') ->andWhere('ch.dimensionspacepointhash = :childOriginDimensionSpacePointHash') ->andWhere('cn.nodeaggregateid = :childNodeAggregateId') ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->from($this->nodeQueryBuilder->tableNames->node(), 'n') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->nodeQueryBuilder->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')') ->andWhere('h.contentstreamid = :contentStreamId') ->setParameters([ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamId' => $this->contentStreamId->value, 'childNodeAggregateId' => $childNodeAggregateId->value, 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash, ]); return $this->nodeFactory->mapNodeRowsToNodeAggregate( $this->fetchRows($queryBuilder), - $contentStreamId, + $this->workspaceName, + $this->contentStreamId, VisibilityConstraints::withoutRestrictions() ); } - /** - * @return iterable - */ - public function findChildNodeAggregates( - ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId - ): iterable { - $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); + public function findTetheredChildNodeAggregates(NodeAggregateId $parentNodeAggregateId): iterable + { + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId) + ->andWhere('cn.classification = :tetheredClassification') + ->setParameter('tetheredClassification', NodeAggregateClassification::CLASSIFICATION_TETHERED->value); + + return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } - /** - * @return iterable - */ - public function findChildNodeAggregatesByName( - ContentStreamId $contentStreamId, + public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable { - $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId) - ->andWhere('ch.name = :relationName') + ): ?NodeAggregate { + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId) + ->andWhere('cn.name = :relationName') ->setParameter('relationName', $name->value); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); - } - /** - * @return iterable - */ - public function findTetheredChildNodeAggregates( - ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId - ): iterable { - $queryBuilder = $this->buildChildNodeAggregateQuery($parentNodeAggregateId, $contentStreamId) - ->andWhere('cn.classification = :tetheredClassification') - ->setParameter('tetheredClassification', NodeAggregateClassification::CLASSIFICATION_TETHERED->value); - return $this->mapQueryBuilderToNodeAggregates($queryBuilder, $contentStreamId); + return $this->mapQueryBuilderToNodeAggregate($queryBuilder); } - /** - * @param ContentStreamId $contentStreamId - * @param NodeName $nodeName - * @param NodeAggregateId $parentNodeAggregateId - * @param OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint - * @param DimensionSpacePointSet $dimensionSpacePointsToCheck - * @return DimensionSpacePointSet - */ - public function getDimensionSpacePointsOccupiedByChildNodeName( - ContentStreamId $contentStreamId, - NodeName $nodeName, - NodeAggregateId $parentNodeAggregateId, - OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, - DimensionSpacePointSet $dimensionSpacePointsToCheck - ): DimensionSpacePointSet { + public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, DimensionSpacePointSet $dimensionSpacePointsToCheck): DimensionSpacePointSet + { $queryBuilder = $this->createQueryBuilder() ->select('dsp.dimensionspacepoint, h.dimensionspacepointhash') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') - ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.parentnodeanchor') - ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = n.relationanchorpoint') + ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h') + ->innerJoin('h', $this->nodeQueryBuilder->tableNames->node(), 'n', 'n.relationanchorpoint = h.parentnodeanchor') + ->innerJoin('h', $this->nodeQueryBuilder->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = n.relationanchorpoint') ->where('n.nodeaggregateid = :parentNodeAggregateId') ->andWhere('n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)') - ->andWhere('h.name = :nodeName') + ->andWhere('n.name = :nodeName') ->setParameters([ 'parentNodeAggregateId' => $parentNodeAggregateId->value, 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamId' => $this->contentStreamId->value, 'dimensionSpacePointHashes' => $dimensionSpacePointsToCheck->getPointHashes(), 'nodeName' => $nodeName->value ], [ @@ -338,6 +300,7 @@ public function getDimensionSpacePointsOccupiedByChildNodeName( foreach ($this->fetchRows($queryBuilder) as $hierarchyRelationData) { $dimensionSpacePoints[$hierarchyRelationData['dimensionspacepointhash']] = DimensionSpacePoint::fromJsonString($hierarchyRelationData['dimensionspacepoint']); } + return new DimensionSpacePointSet($dimensionSpacePoints); } @@ -345,7 +308,7 @@ public function countNodes(): int { $queryBuilder = $this->createQueryBuilder() ->select('COUNT(*)') - ->from($this->tableNamePrefix . '_node'); + ->from($this->nodeQueryBuilder->tableNames->node()); $result = $queryBuilder->execute(); if (!$result instanceof Result) { throw new \RuntimeException(sprintf('Failed to count nodes. Expected result to be of type %s, got: %s', Result::class, get_debug_type($result)), 1701444550); @@ -353,60 +316,43 @@ public function countNodes(): int try { return (int)$result->fetchOne(); } catch (DriverException | DBALException $e) { - throw new \RuntimeException(sprintf('Failed to fetch rows from database: %s', $e->getMessage()), 1701444590, $e); + throw new \RuntimeException(sprintf('Failed to count rows in database: %s', $e->getMessage()), 1701444590, $e); } } public function findUsedNodeTypeNames(): iterable { - $rows = $this->fetchRows($this->createQueryBuilder() - ->select('DISTINCT nodetypename') - ->from($this->tableNamePrefix . '_node')); - return array_map(static fn (array $row) => NodeTypeName::fromString($row['nodetypename']), $rows); - } - - /** - * @return ContentSubgraphWithRuntimeCaches[] - * @internal only used for {@see DoctrineDbalContentGraphProjection} - */ - public function getSubgraphs(): array - { - return $this->subgraphs; + return array_map( + static fn (array $row) => NodeTypeName::fromString($row['nodetypename']), + $this->fetchRows($this->nodeQueryBuilder->buildfindUsedNodeTypeNamesQuery()) + ); } - private function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder + private function createQueryBuilder(): QueryBuilder { - return $this->createQueryBuilder() - ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_dimensionspacepoints', 'cdsp', 'cdsp.hash = ch.dimensionspacepointhash') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->where('pn.nodeaggregateid = :parentNodeAggregateId') - ->andWhere('ph.contentstreamid = :contentStreamId') - ->andWhere('ch.contentstreamid = :contentStreamId') - ->orderBy('ch.position') - ->setParameters([ - 'parentNodeAggregateId' => $parentNodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, - ]); + return $this->client->getConnection()->createQueryBuilder(); } - private function createQueryBuilder(): QueryBuilder + private function mapQueryBuilderToNodeAggregate(QueryBuilder $queryBuilder): ?NodeAggregate { - return $this->client->getConnection()->createQueryBuilder(); + return $this->nodeFactory->mapNodeRowsToNodeAggregate( + $this->fetchRows($queryBuilder), + $this->workspaceName, + $this->contentStreamId, + VisibilityConstraints::withoutRestrictions() + ); } /** * @param QueryBuilder $queryBuilder * @return iterable */ - private function mapQueryBuilderToNodeAggregates(QueryBuilder $queryBuilder, ContentStreamId $contentStreamId): iterable + private function mapQueryBuilderToNodeAggregates(QueryBuilder $queryBuilder): iterable { return $this->nodeFactory->mapNodeRowsToNodeAggregates( $this->fetchRows($queryBuilder), - $contentStreamId, + $this->workspaceName, + $this->contentStreamId, VisibilityConstraints::withoutRestrictions() ); } @@ -426,4 +372,9 @@ private function fetchRows(QueryBuilder $queryBuilder): array throw new \RuntimeException(sprintf('Failed to fetch rows from database: %s', $e->getMessage()), 1701444358, $e); } } + + public function getContentStreamId(): ContentStreamId + { + return $this->contentStreamId; + } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index c1a278822d7..322ed95e789 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -14,12 +14,12 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository; -use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\Exception as DbalDriverException; use Doctrine\DBAL\Exception as DbalException; use Doctrine\DBAL\ForwardCompatibility\Result; -use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Query\QueryBuilder; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; +use Neos\ContentGraph\DoctrineDbalAdapter\NodeQueryBuilder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; @@ -42,28 +42,14 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSubtreeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\ExpandedNodeTypeCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Ordering\Ordering; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Ordering\OrderingDirection; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Ordering\TimestampField; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\AndCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\NegateCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\OrCriteria; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueContains; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEndsWith; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEquals; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThan; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThanOrEqual; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThan; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThanOrEqual; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueStartsWith; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\References; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; @@ -72,6 +58,7 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * The content subgraph application repository @@ -101,28 +88,40 @@ */ final class ContentSubgraph implements ContentSubgraphInterface { - private int $dynamicParameterCount = 0; + private readonly NodeQueryBuilder $nodeQueryBuilder; public function __construct( private readonly ContentRepositoryId $contentRepositoryId, + private readonly WorkspaceName $workspaceName, private readonly ContentStreamId $contentStreamId, private readonly DimensionSpacePoint $dimensionSpacePoint, private readonly VisibilityConstraints $visibilityConstraints, private readonly DbalClientInterface $client, private readonly NodeFactory $nodeFactory, private readonly NodeTypeManager $nodeTypeManager, - private readonly string $tableNamePrefix + ContentGraphTableNames $tableNames ) { + $this->nodeQueryBuilder = new NodeQueryBuilder($this->client->getConnection(), $tableNames); } - public function getIdentity(): ContentSubgraphIdentity + public function getContentRepositoryId(): ContentRepositoryId { - return ContentSubgraphIdentity::create( - $this->contentRepositoryId, - $this->contentStreamId, - $this->dimensionSpacePoint, - $this->visibilityConstraints - ); + return $this->contentRepositoryId; + } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } + + public function getDimensionSpacePoint(): DimensionSpacePoint + { + return $this->dimensionSpacePoint; + } + + public function getVisibilityConstraints(): VisibilityConstraints + { + return $this->visibilityConstraints; } public function findChildNodes(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter $filter): Nodes @@ -168,45 +167,24 @@ public function countBackReferences(NodeAggregateId $nodeAggregateId, CountBackR public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('n.nodeaggregateid = :nodeAggregateId')->setParameter('nodeAggregateId', $nodeAggregateId->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint) + ->andWhere('n.nodeaggregateid = :nodeAggregateId')->setParameter('nodeAggregateId', $nodeAggregateId->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $nodeTypeName->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('n.classification = :nodeAggregateClassification') - ->setParameter('nodeAggregateClassification', NodeAggregateClassification::CLASSIFICATION_ROOT->value); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint) + ->andWhere('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $nodeTypeName->value) + ->andWhere('n.classification = :nodeAggregateClassification')->setParameter('nodeAggregateClassification', NodeAggregateClassification::CLASSIFICATION_ROOT->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ch.name, ch.subtreetags') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.childnodeanchor = pn.relationanchorpoint') - ->where('cn.nodeaggregateid = :childNodeAggregateId')->setParameter('childNodeAggregateId', $childNodeAggregateId->value) - ->andWhere('ph.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('ch.contentstreamid = :contentStreamId') - ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash'); + $queryBuilder = $this->nodeQueryBuilder->buildBasicParentNodeQuery($childNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder, 'ph'); return $this->fetchNode($queryBuilder); } @@ -238,15 +216,8 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node */ private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node { - $queryBuilder = $this->createQueryBuilder() - ->select('cn.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') - ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('h.name = :edgeName')->setParameter('edgeName', $nodeName->value); + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint) + ->andWhere('n.name = :edgeName')->setParameter('edgeName', $nodeName->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } @@ -290,26 +261,26 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi { $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation - ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->from($this->nodeQueryBuilder->tableNames->node(), 'n') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('c.*, h.name, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') + ->select('c.*, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') ->from('tree', 'p') - ->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = p.relationanchorpoint') - ->innerJoin('p', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = h.childnodeanchor') + ->innerJoin('p', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = p.relationanchorpoint') + ->innerJoin('p', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = h.childnodeanchor') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); if ($filter->maximumLevels !== null) { $queryBuilderRecursive->andWhere('p.level < :maximumLevels')->setParameter('maximumLevels', $filter->maximumLevels); } if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderRecursive, $filter->nodeTypes, 'c'); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderRecursive, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'c'); } $this->addSubtreeTagConstraints($queryBuilderRecursive); @@ -328,7 +299,13 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi foreach (array_reverse($result) as $nodeData) { $nodeAggregateId = $nodeData['nodeaggregateid']; $parentNodeAggregateId = $nodeData['parentNodeAggregateId']; - $node = $this->nodeFactory->mapNodeRowToNode($nodeData, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints); + $node = $this->nodeFactory->mapNodeRowToNode( + $nodeData, + $this->workspaceName, + $this->contentStreamId, + $this->dimensionSpacePoint, + $this->visibilityConstraints + ); $subtree = new Subtree((int)$nodeData['level'], $node, array_key_exists($nodeAggregateId, $subtreesByParentNodeId) ? array_reverse($subtreesByParentNodeId[$nodeAggregateId]) : []); if ($subtree->level === 0) { return $subtree; @@ -357,6 +334,7 @@ public function findAncestorNodes(NodeAggregateId $entryNodeAggregateId, FindAnc return $this->nodeFactory->mapNodeRowsToNodes( $nodeRows, + $this->workspaceName, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints @@ -382,33 +360,27 @@ public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, CountA public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClosestNodeFilter $filter): ?Node { $queryBuilderInitial = $this->createQueryBuilder() - ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') - ->from($this->tableNamePrefix . '_node', 'n') + ->select('n.*, ph.subtreetags, ph.parentnodeanchor') + ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'n.relationanchorpoint = ph.childnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') + ->select('pn.*, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') - ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') + ->innerJoin('cn', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') + ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->createQueryBuilder() - ->select('*') - ->from('ancestry', 'pn') - ->setMaxResults(1) - ->setParameter('contentStreamId', $this->contentStreamId->value) - ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'pn'); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); } $nodeRows = $this->fetchCteResults( $queryBuilderInitial, @@ -418,6 +390,7 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose ); return $this->nodeFactory->mapNodeRowsToNodes( $nodeRows, + $this->workspaceName, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints @@ -435,7 +408,13 @@ public function findDescendantNodes(NodeAggregateId $entryNodeAggregateId, FindD } $queryBuilderCte->addOrderBy('level')->addOrderBy('position'); $nodeRows = $this->fetchCteResults($queryBuilderInitial, $queryBuilderRecursive, $queryBuilderCte, 'tree'); - return $this->nodeFactory->mapNodeRowsToNodes($nodeRows, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints); + return $this->nodeFactory->mapNodeRowsToNodes( + $nodeRows, + $this->workspaceName, + $this->contentStreamId, + $this->dimensionSpacePoint, + $this->visibilityConstraints + ); } public function countDescendantNodes(NodeAggregateId $entryNodeAggregateId, CountDescendantNodesFilter $filter): int @@ -446,26 +425,29 @@ public function countDescendantNodes(NodeAggregateId $entryNodeAggregateId, Coun public function countNodes(): int { - $queryBuilder = $this->createQueryBuilder() - ->select('COUNT(*)') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint, 'n', 'COUNT(*)'); try { $result = $this->executeQuery($queryBuilder)->fetchOne(); - if (!is_int($result)) { - throw new \RuntimeException(sprintf('Expected result to be of type integer but got: %s', get_debug_type($result)), 1678366902); - } - return $result; } catch (DbalDriverException | DbalException $e) { throw new \RuntimeException(sprintf('Failed to count all nodes: %s', $e->getMessage()), 1678364741, $e); } + + if (!is_int($result)) { + throw new \RuntimeException(sprintf('Expected result to be of type integer but got: %s', get_debug_type($result)), 1678366902); + } + + return $result; } - public function jsonSerialize(): ContentSubgraphIdentity + /** + * @return array + */ + public function jsonSerialize(): array { - return $this->getIdentity(); + return [ + 'workspaceName' => $this->workspaceName, + 'dimensionSpacePoint' => $this->dimensionSpacePoint, + ]; } /** ------------------------------------------- */ @@ -488,12 +470,6 @@ private function createQueryBuilder(): QueryBuilder return $this->client->getConnection()->createQueryBuilder(); } - private function createUniqueParameterName(): string - { - return 'param_' . (++$this->dynamicParameterCount); - } - - private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hierarchyRelationTableAlias = 'h'): void { $hierarchyRelationTablePrefix = $hierarchyRelationTableAlias === '' ? '' : $hierarchyRelationTableAlias . '.'; @@ -504,119 +480,17 @@ private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hi } } - private function addNodeTypeCriteria(QueryBuilder $queryBuilder, NodeTypeCriteria $nodeTypeCriteria, string $nodeTableAlias = 'n'): void - { - $nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.'; - $constraintsWithSubNodeTypes = ExpandedNodeTypeCriteria::create($nodeTypeCriteria, $this->nodeTypeManager); - $allowanceQueryPart = ''; - if (!$constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->isEmpty()) { - $allowanceQueryPart = $queryBuilder->expr()->in($nodeTablePrefix . 'nodetypename', ':explicitlyAllowedNodeTypeNames'); - $queryBuilder->setParameter('explicitlyAllowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); - } - $denyQueryPart = ''; - if (!$constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->isEmpty()) { - $denyQueryPart = $queryBuilder->expr()->notIn($nodeTablePrefix . 'nodetypename', ':explicitlyDisallowedNodeTypeNames'); - $queryBuilder->setParameter('explicitlyDisallowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); - } - if ($allowanceQueryPart && $denyQueryPart) { - if ($constraintsWithSubNodeTypes->isWildCardAllowed) { - $queryBuilder->andWhere($queryBuilder->expr()->or($allowanceQueryPart, $denyQueryPart)); - } else { - $queryBuilder->andWhere($queryBuilder->expr()->and($allowanceQueryPart, $denyQueryPart)); - } - } elseif ($allowanceQueryPart && !$constraintsWithSubNodeTypes->isWildCardAllowed) { - $queryBuilder->andWhere($allowanceQueryPart); - } elseif ($denyQueryPart) { - $queryBuilder->andWhere($denyQueryPart); - } - } - - private function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void - { - $queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); - } - - private function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): void - { - $queryBuilder->andWhere($this->propertyValueConstraints($queryBuilder, $propertyValue, $nodeTableAlias)); - } - - private function propertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias): string - { - return match ($propertyValue::class) { - AndCriteria::class => (string)$queryBuilder->expr()->and($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), - NegateCriteria::class => 'NOT (' . $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')', - OrCriteria::class => (string)$queryBuilder->expr()->or($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), - PropertyValueContains::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), - PropertyValueEndsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive), - PropertyValueEquals::class => is_string($propertyValue->value) ? $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive) : $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '=', $nodeTableAlias), - PropertyValueGreaterThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>', $nodeTableAlias), - PropertyValueGreaterThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>=', $nodeTableAlias), - PropertyValueLessThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<', $nodeTableAlias), - PropertyValueLessThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<=', $nodeTableAlias), - PropertyValueStartsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), - default => throw new \InvalidArgumentException(sprintf('Invalid/unsupported property value criteria "%s"', get_debug_type($propertyValue)), 1679561062), - }; - } - - private function comparePropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|int|float|bool $value, string $operator, string $nodeTableAlias): string - { - $paramName = $this->createUniqueParameterName(); - $paramType = match (gettype($value)) { - 'boolean' => ParameterType::BOOLEAN, - 'integer' => ParameterType::INTEGER, - default => ParameterType::STRING, - }; - $queryBuilder->setParameter($paramName, $value, $paramType); - return $this->extractPropertyValue($propertyName, $nodeTableAlias) . ' ' . $operator . ' :' . $paramName; - } - - private function extractPropertyValue(PropertyName $propertyName, string $nodeTableAlias): string - { - try { - $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); - } catch (\JsonException $e) { - throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); - } - return 'JSON_EXTRACT(' . $nodeTableAlias . '.properties, \'$.' . $escapedPropertyName . '.value\')'; - } - - private function searchPropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|bool|int|float $value, string $nodeTableAlias, bool $caseSensitive): string - { - try { - $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); - } catch (\JsonException $e) { - throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); - } - if (is_bool($value)) { - return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', \'' . ($value ? 'true' : 'false') . '\', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; - } - $paramName = $this->createUniqueParameterName(); - $queryBuilder->setParameter($paramName, $value); - if ($caseSensitive) { - return 'JSON_SEARCH(' . $nodeTableAlias . '.properties COLLATE utf8mb4_bin, \'one\', :' . $paramName . ' COLLATE utf8mb4_bin, NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; - } - return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', :' . $paramName . ', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; - } - private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter|CountChildNodesFilter $filter): QueryBuilder { - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'n', 'h.childnodeanchor = n.relationanchorpoint') - ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) - ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash); + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->searchTerm); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm); } if ($filter->propertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); } $this->addSubtreeTagConstraints($queryBuilder); return $queryBuilder; @@ -627,12 +501,12 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod $sourceTablePrefix = $backReferences ? 'd' : 's'; $destinationTablePrefix = $backReferences ? 's' : 'd'; $queryBuilder = $this->createQueryBuilder() - ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.name, {$destinationTablePrefix}h.subtreetags, r.name AS referencename, r.properties AS referenceproperties") - ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') - ->innerJoin('sh', $this->tableNamePrefix . '_referencerelation', 'r', 'r.nodeanchorpoint = sn.relationanchorpoint') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'dn', 'dn.nodeaggregateid = r.destinationnodeaggregateid') - ->innerJoin('sh', $this->tableNamePrefix . '_hierarchyrelation', 'dh', 'dh.childnodeanchor = dn.relationanchorpoint') + ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.subtreetags, r.name AS referencename, r.properties AS referenceproperties") + ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'sh') + ->innerJoin('sh', $this->nodeQueryBuilder->tableNames->node(), 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') + ->innerJoin('sh', $this->nodeQueryBuilder->tableNames->referenceRelation(), 'r', 'r.nodeanchorpoint = sn.relationanchorpoint') + ->innerJoin('sh', $this->nodeQueryBuilder->tableNames->node(), 'dn', 'dn.nodeaggregateid = r.destinationnodeaggregateid') + ->innerJoin('sh', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'dh', 'dh.childnodeanchor = dn.relationanchorpoint') ->where("{$sourceTablePrefix}n.nodeaggregateid = :nodeAggregateId")->setParameter('nodeAggregateId', $nodeAggregateId->value) ->andWhere('dh.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash') @@ -641,22 +515,19 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod $this->addSubtreeTagConstraints($queryBuilder, 'dh'); $this->addSubtreeTagConstraints($queryBuilder, 'sh'); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "{$destinationTablePrefix}n"); } if ($filter->nodeSearchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n"); } if ($filter->nodePropertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"); } if ($filter->referenceSearchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r'); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r'); } if ($filter->referencePropertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r'); - } - if ($filter->nodePropertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r'); } if ($filter->referenceName !== null) { $queryBuilder->andWhere('r.name = :referenceName')->setParameter('referenceName', $filter->referenceName->value); @@ -678,42 +549,17 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, FindPrecedingSiblingNodesFilter|FindSucceedingSiblingNodesFilter $filter): QueryBuilder { - $parentNodeAnchorSubQuery = $this->createQueryBuilder() - ->select('sh.parentnodeanchor') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') - ->where('sn.nodeaggregateid = :siblingNodeAggregateId') - ->andWhere('sh.contentstreamid = :contentStreamId') - ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); - - $siblingPositionSubQuery = $this->createQueryBuilder() - ->select('sh.position') - ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') - ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') - ->where('sn.nodeaggregateid = :siblingNodeAggregateId') - ->andWhere('sh.contentstreamid = :contentStreamId') - ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); - - $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') - ->from($this->tableNamePrefix . '_node', 'n') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('h.parentnodeanchor = (' . $parentNodeAnchorSubQuery->getSQL() . ')') - ->andWhere('n.nodeaggregateid != :siblingNodeAggregateId')->setParameter('siblingNodeAggregateId', $siblingNodeAggregateId->value) - ->andWhere('h.position ' . ($preceding ? '<' : '>') . ' (' . $siblingPositionSubQuery->getSQL() . ')') - ->orderBy('h.position', $preceding ? 'DESC' : 'ASC'); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeSiblingsQuery($preceding, $siblingNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->addSearchTermConstraints($queryBuilder, $filter->searchTerm); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm); } if ($filter->propertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); } if ($filter->pagination !== null) { $this->applyPagination($queryBuilder, $filter->pagination); @@ -727,12 +573,12 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId, FindAncestorNodesFilter|CountAncestorNodesFilter|FindClosestNodeFilter $filter): array { $queryBuilderInitial = $this->createQueryBuilder() - ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') - ->from($this->tableNamePrefix . '_node', 'n') + ->select('n.*, ph.subtreetags, ph.parentnodeanchor') + ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') - ->innerJoin('ch', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = ch.childnodeanchor') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'n.relationanchorpoint = ph.childnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') + ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = ch.childnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') ->where('ch.contentstreamid = :contentStreamId') ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('ph.contentstreamid = :contentStreamId') @@ -742,22 +588,17 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId $this->addSubtreeTagConstraints($queryBuilderInitial, 'ch'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') + ->select('pn.*, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') - ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') + ->innerJoin('cn', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') + ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->createQueryBuilder() - ->select('*') - ->from('ancestry', 'pn') - ->setParameter('contentStreamId', $this->contentStreamId->value) - ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'pn'); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); } return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte'); } @@ -769,12 +610,12 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate { $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation - ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') - ->from($this->tableNamePrefix . '_node', 'n') + ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('n', $this->tableNamePrefix . '_node', 'p', 'p.relationanchorpoint = h.parentnodeanchor') - ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = p.relationanchorpoint') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->node(), 'p', 'p.relationanchorpoint = h.parentnodeanchor') + ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = p.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('ph.contentstreamid = :contentStreamId') @@ -783,28 +624,23 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('cn.*, h.name, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') + ->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') ->from('tree', 'pn') - ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') + ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = h.childnodeanchor') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->createQueryBuilder() - ->select('*') - ->from('tree', 'n') - ->setParameter('contentStreamId', $this->contentStreamId->value) - ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint, 'tree', 'n'); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes); + $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } if ($filter->searchTerm !== null) { - $this->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm); + $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm); } if ($filter->propertyValue !== null) { - $this->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue); + $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue); } return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte'); } @@ -817,7 +653,7 @@ private function applyOrdering(QueryBuilder $queryBuilder, Ordering $ordering, s OrderingDirection::DESCENDING => 'DESC', }; if ($orderingField->field instanceof PropertyName) { - $queryBuilder->addOrderBy($this->extractPropertyValue($orderingField->field, $nodeTableAlias), $order); + $queryBuilder->addOrderBy($this->nodeQueryBuilder->extractPropertyValue($orderingField->field, $nodeTableAlias), $order); } else { $timestampColumnName = match ($orderingField->field) { TimestampField::CREATED => 'created', @@ -832,9 +668,7 @@ private function applyOrdering(QueryBuilder $queryBuilder, Ordering $ordering, s private function applyPagination(QueryBuilder $queryBuilder, Pagination $pagination): void { - $queryBuilder - ->setMaxResults($pagination->limit) - ->setFirstResult($pagination->offset); + $queryBuilder->setMaxResults($pagination->limit)->setFirstResult($pagination->offset); } /** @@ -863,6 +697,7 @@ private function fetchNode(QueryBuilder $queryBuilder): ?Node } return $this->nodeFactory->mapNodeRowToNode( $nodeRow, + $this->workspaceName, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints @@ -876,7 +711,13 @@ private function fetchNodes(QueryBuilder $queryBuilder): Nodes } catch (DbalDriverException | DbalException $e) { throw new \RuntimeException(sprintf('Failed to fetch nodes: %s', $e->getMessage()), 1678292896, $e); } - return $this->nodeFactory->mapNodeRowsToNodes($nodeRows, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints); + return $this->nodeFactory->mapNodeRowsToNodes( + $nodeRows, + $this->workspaceName, + $this->contentStreamId, + $this->dimensionSpacePoint, + $this->visibilityConstraints + ); } private function fetchCount(QueryBuilder $queryBuilder): int @@ -895,7 +736,7 @@ private function fetchReferences(QueryBuilder $queryBuilder): References } catch (DbalDriverException | DbalException $e) { throw new \RuntimeException(sprintf('Failed to fetch references: %s', $e->getMessage()), 1678364944, $e); } - return $this->nodeFactory->mapReferenceRowsToReferences($referenceRows, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints); + return $this->nodeFactory->mapReferenceRowsToReferences($referenceRows, $this->workspaceName, $this->contentStreamId, $this->dimensionSpacePoint, $this->visibilityConstraints); } /** diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php index 1a409b5abc6..6b7632f46bd 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php @@ -15,6 +15,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository; use Doctrine\DBAL\Connection; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentRepository\Core\DimensionSpace\AbstractDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; @@ -32,7 +33,7 @@ final class DimensionSpacePointsRepository public function __construct( private readonly Connection $databaseConnection, - private readonly string $tableNamePrefix + private readonly ContentGraphTableNames $tableNames ) { } @@ -81,7 +82,7 @@ public function getOriginDimensionSpacePointByHash(string $hash): OriginDimensio private function writeDimensionSpacePoint(string $hash, string $dimensionSpacePointCoordinatesJson): void { $this->databaseConnection->executeStatement( - 'INSERT IGNORE INTO ' . $this->tableNamePrefix . '_dimensionspacepoints (hash, dimensionspacepoint) VALUES (:dimensionspacepointhash, :dimensionspacepoint)', + 'INSERT IGNORE INTO ' . $this->tableNames->dimensionSpacePoints() . ' (hash, dimensionspacepoint) VALUES (:dimensionspacepointhash, :dimensionspacepoint)', [ 'dimensionspacepointhash' => $hash, 'dimensionspacepoint' => $dimensionSpacePointCoordinatesJson @@ -96,7 +97,7 @@ private function getCoordinatesByHashFromRuntimeCache(string $hash): ?string private function fillRuntimeCacheFromDatabase(): void { - $allDimensionSpacePoints = $this->databaseConnection->fetchAllAssociative('SELECT hash, dimensionspacepoint FROM ' . $this->tableNamePrefix . '_dimensionspacepoints'); + $allDimensionSpacePoints = $this->databaseConnection->fetchAllAssociative('SELECT hash, dimensionspacepoint FROM ' . $this->tableNames->dimensionSpacePoints()); foreach ($allDimensionSpacePoints as $dimensionSpacePointRow) { $this->dimensionspacePointsRuntimeCache[(string)$dimensionSpacePointRow['hash']] = (string)$dimensionSpacePointRow['dimensionspacepoint']; } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php index 88887b67d9a..3e91d0fc05a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php @@ -16,7 +16,6 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; @@ -38,11 +37,13 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * Implementation detail of ContentGraph and ContentSubgraph @@ -64,6 +65,7 @@ public function __construct( */ public function mapNodeRowToNode( array $nodeRow, + WorkspaceName $workspaceName, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints @@ -73,17 +75,13 @@ public function mapNodeRowToNode( : null; return Node::create( - ContentSubgraphIdentity::create( - $this->contentRepositoryId, - $contentStreamId, - $dimensionSpacePoint, - $visibilityConstraints - ), + $this->contentRepositoryId, + $workspaceName, + $dimensionSpacePoint, NodeAggregateId::fromString($nodeRow['nodeaggregateid']), $this->dimensionSpacePointRepository->getOriginDimensionSpacePointByHash($nodeRow['origindimensionspacepointhash']), NodeAggregateClassification::from($nodeRow['classification']), NodeTypeName::fromString($nodeRow['nodetypename']), - $nodeType, $this->createPropertyCollectionFromJsonString($nodeRow['properties']), isset($nodeRow['name']) ? NodeName::fromString($nodeRow['name']) : null, self::extractNodeTagsFromJson($nodeRow['subtreetags']), @@ -93,16 +91,30 @@ public function mapNodeRowToNode( isset($nodeRow['lastmodified']) ? self::parseDateTimeString($nodeRow['lastmodified']) : null, isset($nodeRow['originallastmodified']) ? self::parseDateTimeString($nodeRow['originallastmodified']) : null, ), + $visibilityConstraints, + $nodeType, + $contentStreamId ); } /** * @param array> $nodeRows */ - public function mapNodeRowsToNodes(array $nodeRows, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints): Nodes - { + public function mapNodeRowsToNodes( + array $nodeRows, + WorkspaceName $workspaceName, + ContentStreamId $contentStreamId, + DimensionSpacePoint $dimensionSpacePoint, + VisibilityConstraints $visibilityConstraints + ): Nodes { return Nodes::fromArray( - array_map(fn (array $nodeRow) => $this->mapNodeRowToNode($nodeRow, $contentStreamId, $dimensionSpacePoint, $visibilityConstraints), $nodeRows) + array_map(fn (array $nodeRow) => $this->mapNodeRowToNode( + $nodeRow, + $workspaceName, + $contentStreamId, + $dimensionSpacePoint, + $visibilityConstraints + ), $nodeRows) ); } @@ -119,6 +131,7 @@ public function createPropertyCollectionFromJsonString(string $jsonString): Prop */ public function mapReferenceRowsToReferences( array $nodeRows, + WorkspaceName $workspaceName, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints @@ -127,6 +140,7 @@ public function mapReferenceRowsToReferences( foreach ($nodeRows as $nodeRow) { $node = $this->mapNodeRowToNode( $nodeRow, + $workspaceName, $contentStreamId, $dimensionSpacePoint, $visibilityConstraints @@ -149,6 +163,7 @@ public function mapReferenceRowsToReferences( */ public function mapNodeRowsToNodeAggregate( array $nodeRows, + WorkspaceName $workspaceName, ContentStreamId $contentStreamId, VisibilityConstraints $visibilityConstraints ): ?NodeAggregate { @@ -175,6 +190,7 @@ public function mapNodeRowsToNodeAggregate( // ... so we handle occupation exactly once ... $nodesByOccupiedDimensionSpacePoints[$occupiedDimensionSpacePoint->hash] = $this->mapNodeRowToNode( $nodeRow, + $workspaceName, $contentStreamId, $occupiedDimensionSpacePoint->toDimensionSpacePoint(), $visibilityConstraints @@ -230,6 +246,7 @@ public function mapNodeRowsToNodeAggregate( */ public function mapNodeRowsToNodeAggregates( iterable $nodeRows, + WorkspaceName $workspaceName, ContentStreamId $contentStreamId, VisibilityConstraints $visibilityConstraints ): iterable { @@ -256,6 +273,7 @@ public function mapNodeRowsToNodeAggregates( $nodesByOccupiedDimensionSpacePointsByNodeAggregate [$rawNodeAggregateId][$occupiedDimensionSpacePoint->hash] = $this->mapNodeRowToNode( $nodeRow, + $workspaceName, $contentStreamId, $occupiedDimensionSpacePoint->toDimensionSpacePoint(), $visibilityConstraints diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index 02e6b63e5b9..2c7f005bd23 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -17,6 +17,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Driver\Exception; +use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; @@ -30,8 +31,8 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** - * The read only content graph for use by the {@see GraphProjector}. This is the class for low-level operations - * within the projector, where implementation details of the graph structure are known. + * The read only content graph for use by the {@see DoctrineDbalContentGraphProjection}. This is the class for low-level operations + * within the projection, where implementation details of the graph structure are known. * * This is NO PUBLIC API in any way. * @@ -41,7 +42,7 @@ class ProjectionContentGraph { public function __construct( private readonly DbalClientInterface $client, - private readonly string $tableNamePrefix + private readonly ContentGraphTableNames $tableNames ) { } @@ -71,11 +72,11 @@ public function findParentNode( : $originDimensionSpacePoint->hash ]; $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT p.*, ph.contentstreamid, ph.name, ph.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node p - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph ON ph.childnodeanchor = p.relationanchorpoint - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch ON ch.parentnodeanchor = p.relationanchorpoint - INNER JOIN ' . $this->tableNamePrefix . '_node c ON ch.childnodeanchor = c.relationanchorpoint - INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON p.origindimensionspacepointhash = dsp.hash + 'SELECT p.*, ph.contentstreamid, ph.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNames->node() . ' p + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' ph ON ph.childnodeanchor = p.relationanchorpoint + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' ch ON ch.parentnodeanchor = p.relationanchorpoint + INNER JOIN ' . $this->tableNames->node() . ' c ON ch.childnodeanchor = c.relationanchorpoint + INNER JOIN ' . $this->tableNames->dimensionSpacePoints() . ' dsp ON p.origindimensionspacepointhash = dsp.hash WHERE c.nodeaggregateid = :childNodeAggregateId AND c.origindimensionspacepointhash = :originDimensionSpacePointHash AND ph.contentstreamid = :contentStreamId @@ -102,9 +103,9 @@ public function findNodeInAggregate( DimensionSpacePoint $coveredDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint - INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash + 'SELECT n.*, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN ' . $this->tableNames->dimensionSpacePoints() . ' dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.nodeaggregateid = :nodeAggregateId AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash', @@ -118,35 +119,6 @@ public function findNodeInAggregate( return $nodeRow ? NodeRecord::fromDatabaseRow($nodeRow) : null; } - /** - * @param ContentStreamId $contentStreamId - * @param NodeAggregateId $nodeAggregateId - * @param OriginDimensionSpacePoint $originDimensionSpacePoint - * @return NodeRecord|null - * @throws \Exception - */ - public function findNodeByIds( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - OriginDimensionSpacePoint $originDimensionSpacePoint - ): ?NodeRecord { - $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint - INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash - WHERE n.nodeaggregateid = :nodeAggregateId - AND n.origindimensionspacepointhash = :originDimensionSpacePointHash - AND h.contentstreamid = :contentStreamId', - [ - 'contentStreamId' => $contentStreamId->value, - 'nodeAggregateId' => $nodeAggregateId->value, - 'originDimensionSpacePointHash' => $originDimensionSpacePoint->hash - ] - )->fetchAssociative(); - - return $nodeRow ? NodeRecord::fromDatabaseRow($nodeRow) : null; - } - /** * @param NodeAggregateId $nodeAggregateId * @param OriginDimensionSpacePoint $originDimensionSpacePoint @@ -160,8 +132,8 @@ public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStrea ContentStreamId $contentStreamId ): ?NodeRelationAnchorPoint { $rows = $this->getDatabaseConnection()->executeQuery( - 'SELECT DISTINCT n.relationanchorpoint FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint + 'SELECT DISTINCT n.relationanchorpoint FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId AND n.origindimensionspacepointhash = :originDimensionSpacePointHash AND h.contentstreamid = :contentStreamId', @@ -195,8 +167,8 @@ public function getAnchorPointsForNodeAggregateInContentStream( ContentStreamId $contentStreamId ): iterable { $rows = $this->getDatabaseConnection()->executeQuery( - 'SELECT DISTINCT n.relationanchorpoint FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint + 'SELECT DISTINCT n.relationanchorpoint FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId AND h.contentstreamid = :contentStreamId', [ @@ -219,8 +191,8 @@ public function getAnchorPointsForNodeAggregateInContentStream( public function getNodeByAnchorPoint(NodeRelationAnchorPoint $nodeRelationAnchorPoint): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash + 'SELECT n.*, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNames->node() . ' n + INNER JOIN ' . $this->tableNames->dimensionSpacePoints() . ' dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.relationanchorpoint = :relationAnchorPoint', [ 'relationAnchorPoint' => $nodeRelationAnchorPoint->value, @@ -255,7 +227,7 @@ public function determineHierarchyRelationPosition( if ($succeedingSiblingAnchorPoint) { /** @var array $succeedingSiblingRelation */ $succeedingSiblingRelation = $this->getDatabaseConnection()->executeQuery( - 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.childnodeanchor = :succeedingSiblingAnchorPoint AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash', @@ -270,7 +242,7 @@ public function determineHierarchyRelationPosition( $parentAnchorPoint = NodeRelationAnchorPoint::fromInteger($succeedingSiblingRelation['parentnodeanchor']); $precedingSiblingData = $this->getDatabaseConnection()->executeQuery( - 'SELECT MAX(h.position) AS position FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT MAX(h.position) AS position FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.parentnodeanchor = :anchorPoint AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash @@ -296,7 +268,7 @@ public function determineHierarchyRelationPosition( if (!$parentAnchorPoint) { /** @var array $childHierarchyRelationData */ $childHierarchyRelationData = $this->getDatabaseConnection()->executeQuery( - 'SELECT h.parentnodeanchor FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT h.parentnodeanchor FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.childnodeanchor = :childAnchorPoint AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash', @@ -311,7 +283,7 @@ public function determineHierarchyRelationPosition( ); } $rightmostSucceedingSiblingRelationData = $this->getDatabaseConnection()->executeQuery( - 'SELECT MAX(h.position) AS position FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT MAX(h.position) AS position FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.parentnodeanchor = :parentAnchorPoint AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash', @@ -348,7 +320,7 @@ public function getOutgoingHierarchyRelationsForNodeAndSubgraph( $relations = []; foreach ( $this->getDatabaseConnection()->executeQuery( - 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.parentnodeanchor = :parentAnchorPoint AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash', @@ -380,7 +352,7 @@ public function getIngoingHierarchyRelationsForNodeAndSubgraph( $relations = []; foreach ( $this->getDatabaseConnection()->executeQuery( - 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.childnodeanchor = :childAnchorPoint AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash', @@ -410,7 +382,7 @@ public function findIngoingHierarchyRelationsForNode( DimensionSpacePointSet $restrictToSet = null ): array { $relations = []; - $query = 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + $query = 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.childnodeanchor = :childAnchorPoint AND h.contentstreamid = :contentStreamId'; $parameters = [ @@ -448,7 +420,7 @@ public function findOutgoingHierarchyRelationsForNode( DimensionSpacePointSet $restrictToSet = null ): array { $relations = []; - $query = 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + $query = 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.parentnodeanchor = :parentAnchorPoint AND h.contentstreamid = :contentStreamId'; $parameters = [ @@ -488,8 +460,8 @@ public function findOutgoingHierarchyRelationsForNodeAggregate( $relations = []; foreach ( $this->getDatabaseConnection()->executeQuery( - 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h - INNER JOIN ' . $this->tableNamePrefix . '_node n ON h.parentnodeanchor = n.relationanchorpoint + 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h + INNER JOIN ' . $this->tableNames->node() . ' n ON h.parentnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes)', @@ -523,8 +495,8 @@ public function findIngoingHierarchyRelationsForNodeAggregate( ): array { $relations = []; - $query = 'SELECT h.* FROM ' . $this->tableNamePrefix . '_hierarchyrelation h - INNER JOIN ' . $this->tableNamePrefix . '_node n ON h.childnodeanchor = n.relationanchorpoint + $query = 'SELECT h.* FROM ' . $this->tableNames->hierarchyRelation() . ' h + INNER JOIN ' . $this->tableNames->node() . ' n ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId AND h.contentstreamid = :contentStreamId'; $parameters = [ @@ -561,7 +533,7 @@ public function getAllContentStreamIdsAnchorPointIsContainedIn( foreach ( $this->getDatabaseConnection()->executeQuery( 'SELECT DISTINCT h.contentstreamid - FROM ' . $this->tableNamePrefix . '_hierarchyrelation h + FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.childnodeanchor = :nodeRelationAnchorPoint', [ 'nodeRelationAnchorPoint' => $nodeRelationAnchorPoint->value, @@ -574,90 +546,16 @@ public function getAllContentStreamIdsAnchorPointIsContainedIn( return $contentStreamIds; } - /** - * Finds all descendant node aggregate ids, indexed by dimension space point hash - * - * @param ContentStreamId $contentStreamId - * @param NodeAggregateId $entryNodeAggregateId - * @param DimensionSpacePointSet $affectedDimensionSpacePoints - * @return array|NodeAggregateId[][] - * @throws DBALException - */ - public function findDescendantNodeAggregateIds( - ContentStreamId $contentStreamId, - NodeAggregateId $entryNodeAggregateId, - DimensionSpacePointSet $affectedDimensionSpacePoints - ): array { - $rows = $this->getDatabaseConnection()->executeQuery( - ' - -- ProjectionContentGraph::findDescendantNodeAggregateIds - - WITH RECURSIVE nestedNodes AS ( - -- -------------------------------- - -- INITIAL query: select the root nodes - -- -------------------------------- - SELECT - n.nodeaggregateid, - n.relationanchorpoint, - h.dimensionspacepointhash - FROM - ' . $this->tableNamePrefix . '_node n - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h - on h.childnodeanchor = n.relationanchorpoint - WHERE n.nodeaggregateid = :entryNodeAggregateId - AND h.contentstreamid = :contentStreamId - AND h.dimensionspacepointhash IN (:affectedDimensionSpacePointHashes) - - UNION - -- -------------------------------- - -- RECURSIVE query: do one "child" query step - -- -------------------------------- - SELECT - c.nodeaggregateid, - c.relationanchorpoint, - h.dimensionspacepointhash - FROM - nestedNodes p - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h - on h.parentnodeanchor = p.relationanchorpoint - INNER JOIN ' . $this->tableNamePrefix . '_node c - on h.childnodeanchor = c.relationanchorpoint - WHERE - h.contentstreamid = :contentStreamId - AND h.dimensionspacepointhash IN (:affectedDimensionSpacePointHashes) - ) - select nodeaggregateid, dimensionspacepointhash from nestedNodes - ', - [ - 'entryNodeAggregateId' => $entryNodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, - 'affectedDimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes() - ], - [ - 'affectedDimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY - ] - )->fetchAllAssociative(); - - $nodeAggregateIds = []; - foreach ($rows as $row) { - $nodeAggregateIds[$row['nodeaggregateid']][$row['dimensionspacepointhash']] - = NodeAggregateId::fromString($row['nodeaggregateid']); - } - - return $nodeAggregateIds; - } - /** * @param array $rawData */ protected function mapRawDataToHierarchyRelation(array $rawData): HierarchyRelation { - $dimensionspacepointRaw = $this->client->getConnection()->fetchOne('SELECT dimensionspacepoint FROM ' . $this->tableNamePrefix . '_dimensionspacepoints WHERE hash = :hash', ['hash' => $rawData['dimensionspacepointhash']]); + $dimensionspacepointRaw = $this->client->getConnection()->fetchOne('SELECT dimensionspacepoint FROM ' . $this->tableNames->dimensionSpacePoints() . ' WHERE hash = :hash', ['hash' => $rawData['dimensionspacepointhash']]); return new HierarchyRelation( NodeRelationAnchorPoint::fromInteger((int)$rawData['parentnodeanchor']), NodeRelationAnchorPoint::fromInteger((int)$rawData['childnodeanchor']), - $rawData['name'] ? NodeName::fromString($rawData['name']) : null, ContentStreamId::fromString($rawData['contentstreamid']), DimensionSpacePoint::fromJsonString($dimensionspacepointRaw), $rawData['dimensionspacepointhash'], diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php new file mode 100644 index 00000000000..1997347d513 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -0,0 +1,272 @@ +createQueryBuilder() + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->from($this->tableNames->node(), 'n') + ->innerJoin('n', $this->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') + ->where('h.contentstreamid = :contentStreamId'); + + return $queryBuilder; + } + + public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder + { + return $this->createQueryBuilder() + ->select('cn.*, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->from($this->tableNames->node(), 'pn') + ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('ch', $this->tableNames->dimensionSpacePoints(), 'cdsp', 'cdsp.hash = ch.dimensionspacepointhash') + ->innerJoin('ch', $this->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') + ->where('pn.nodeaggregateid = :parentNodeAggregateId') + ->andWhere('ph.contentstreamid = :contentStreamId') + ->andWhere('ch.contentstreamid = :contentStreamId') + ->orderBy('ch.position') + ->setParameters([ + 'parentNodeAggregateId' => $parentNodeAggregateId->value, + 'contentStreamId' => $contentStreamId->value, + ]); + } + + public function buildFindRootNodeAggregatesQuery(ContentStreamId $contentStreamId, FindRootNodeAggregatesFilter $filter): QueryBuilder + { + $queryBuilder = $this->buildBasicNodeAggregateQuery() + ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId') + ->setParameters([ + 'contentStreamId' => $contentStreamId->value, + 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value, + ]); + + if ($filter->nodeTypeName !== null) { + $queryBuilder->andWhere('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $filter->nodeTypeName->value); + } + + return $queryBuilder; + } + + public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.subtreetags'): QueryBuilder + { + return $this->createQueryBuilder() + ->select($select) + ->from($this->tableNames->node(), $nodeTableAlias) + ->innerJoin($nodeTableAlias, $this->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = ' . $nodeTableAlias . '.relationanchorpoint') + ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); + } + + public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + return $this->createQueryBuilder() + ->select('n.*, h.subtreetags') + ->from($this->tableNames->node(), 'pn') + ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->tableNames->node(), 'n', 'h.childnodeanchor = n.relationanchorpoint') + ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) + ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); + } + + public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + return $this->createQueryBuilder() + ->select('pn.*, ch.subtreetags') + ->from($this->tableNames->node(), 'pn') + ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->tableNames->node(), 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') + ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ch', 'ch.childnodeanchor = pn.relationanchorpoint') + ->where('cn.nodeaggregateid = :childNodeAggregateId')->setParameter('childNodeAggregateId', $childNodeAggregateId->value) + ->andWhere('ph.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->andWhere('ch.contentstreamid = :contentStreamId') + ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) + ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash'); + } + + public function buildBasicNodeSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + { + $sharedSubQuery = $this->createQueryBuilder() + ->from($this->tableNames->hierarchyRelation(), 'sh') + ->innerJoin('sh', $this->tableNames->node(), 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') + ->where('sn.nodeaggregateid = :siblingNodeAggregateId') + ->andWhere('sh.contentstreamid = :contentStreamId') + ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); + + $parentNodeAnchorSubQuery = (clone $sharedSubQuery)->select('sh.parentnodeanchor'); + $siblingPositionSubQuery = (clone $sharedSubQuery)->select('sh.position'); + + return $this->buildBasicNodeQuery($contentStreamId, $dimensionSpacePoint) + ->andWhere('h.parentnodeanchor = (' . $parentNodeAnchorSubQuery->getSQL() . ')') + ->andWhere('n.nodeaggregateid != :siblingNodeAggregateId')->setParameter('siblingNodeAggregateId', $siblingNodeAggregateId->value) + ->andWhere('h.position ' . ($preceding ? '<' : '>') . ' (' . $siblingPositionSubQuery->getSQL() . ')') + ->orderBy('h.position', $preceding ? 'DESC' : 'ASC'); + } + + public function buildBasicNodesCteQuery(NodeAggregateId $entryNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $cteName = 'ancestry', string $cteAlias = 'pn'): QueryBuilder + { + return $this->createQueryBuilder() + ->select('*') + ->from($cteName, $cteAlias) + ->setParameter('contentStreamId', $contentStreamId->value) + ->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) + ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); + } + + public function addNodeTypeCriteria(QueryBuilder $queryBuilder, ExpandedNodeTypeCriteria $constraintsWithSubNodeTypes, string $nodeTableAlias = 'n'): void + { + $nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.'; + $allowanceQueryPart = ''; + if (!$constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->isEmpty()) { + $allowanceQueryPart = $queryBuilder->expr()->in($nodeTablePrefix . 'nodetypename', ':explicitlyAllowedNodeTypeNames'); + $queryBuilder->setParameter('explicitlyAllowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); + } + $denyQueryPart = ''; + if (!$constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->isEmpty()) { + $denyQueryPart = $queryBuilder->expr()->notIn($nodeTablePrefix . 'nodetypename', ':explicitlyDisallowedNodeTypeNames'); + $queryBuilder->setParameter('explicitlyDisallowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->toStringArray(), Connection::PARAM_STR_ARRAY); + } + if ($allowanceQueryPart && $denyQueryPart) { + if ($constraintsWithSubNodeTypes->isWildCardAllowed) { + $queryBuilder->andWhere($queryBuilder->expr()->or($allowanceQueryPart, $denyQueryPart)); + } else { + $queryBuilder->andWhere($queryBuilder->expr()->and($allowanceQueryPart, $denyQueryPart)); + } + } elseif ($allowanceQueryPart && !$constraintsWithSubNodeTypes->isWildCardAllowed) { + $queryBuilder->andWhere($allowanceQueryPart); + } elseif ($denyQueryPart) { + $queryBuilder->andWhere($denyQueryPart); + } + } + + public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void + { + $queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); + } + + public function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): void + { + $queryBuilder->andWhere($this->propertyValueConstraints($queryBuilder, $propertyValue, $nodeTableAlias)); + } + + private function propertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias): string + { + return match ($propertyValue::class) { + AndCriteria::class => (string)$queryBuilder->expr()->and($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), + NegateCriteria::class => 'NOT (' . $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')', + OrCriteria::class => (string)$queryBuilder->expr()->or($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)), + PropertyValueContains::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), + PropertyValueEndsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive), + PropertyValueEquals::class => is_string($propertyValue->value) ? $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive) : $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '=', $nodeTableAlias), + PropertyValueGreaterThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>', $nodeTableAlias), + PropertyValueGreaterThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '>=', $nodeTableAlias), + PropertyValueLessThan::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<', $nodeTableAlias), + PropertyValueLessThanOrEqual::class => $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '<=', $nodeTableAlias), + PropertyValueStartsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive), + default => throw new \InvalidArgumentException(sprintf('Invalid/unsupported property value criteria "%s"', get_debug_type($propertyValue)), 1679561062), + }; + } + + private function comparePropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|int|float|bool $value, string $operator, string $nodeTableAlias): string + { + $paramName = $this->createUniqueParameterName(); + $paramType = match (gettype($value)) { + 'boolean' => ParameterType::BOOLEAN, + 'integer' => ParameterType::INTEGER, + default => ParameterType::STRING, + }; + $queryBuilder->setParameter($paramName, $value, $paramType); + + return $this->extractPropertyValue($propertyName, $nodeTableAlias) . ' ' . $operator . ' :' . $paramName; + } + + public function extractPropertyValue(PropertyName $propertyName, string $nodeTableAlias): string + { + try { + $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); + } + + return 'JSON_EXTRACT(' . $nodeTableAlias . '.properties, \'$.' . $escapedPropertyName . '.value\')'; + } + + private function searchPropertyValueStatement(QueryBuilder $queryBuilder, PropertyName $propertyName, string|bool|int|float $value, string $nodeTableAlias, bool $caseSensitive): string + { + try { + $escapedPropertyName = addslashes(json_encode($propertyName->value, JSON_THROW_ON_ERROR)); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to escape property name: %s', $e->getMessage()), 1679394579, $e); + } + if (is_bool($value)) { + return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', \'' . ($value ? 'true' : 'false') . '\', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; + } + $paramName = $this->createUniqueParameterName(); + $queryBuilder->setParameter($paramName, $value); + if ($caseSensitive) { + return 'JSON_SEARCH(' . $nodeTableAlias . '.properties COLLATE utf8mb4_bin, \'one\', :' . $paramName . ' COLLATE utf8mb4_bin, NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; + } + + return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, \'one\', :' . $paramName . ', NULL, \'$.' . $escapedPropertyName . '.value\') IS NOT NULL'; + } + + /** + * @return QueryBuilder + * @throws DBALException + */ + public function buildfindUsedNodeTypeNamesQuery(): QueryBuilder + { + return $this->createQueryBuilder() + ->select('DISTINCT nodetypename') + ->from($this->tableNames->node()); + } + + private function createQueryBuilder(): QueryBuilder + { + return $this->connection->createQueryBuilder(); + } + + private function createUniqueParameterName(): string + { + return 'param_' . str_replace('-', '', UuidFactory::create()); + } +} diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/ContentHyperGraphFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/ContentHyperGraphFactory.php new file mode 100644 index 00000000000..ebb74ab5035 --- /dev/null +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/ContentHyperGraphFactory.php @@ -0,0 +1,62 @@ +contentRepositoryId->value, + 'Workspace' + )); + + $row = $this->databaseClient->getConnection()->executeQuery( + ' + SELECT * FROM ' . $tableName . ' + WHERE workspaceName = :workspaceName + ', + [ + 'workspaceName' => $workspaceName->value, + ] + )->fetchAssociative(); + + if ($row === false) { + throw new ContentStreamDoesNotExistYet('The workspace "' . $workspaceName->value . '" does not exist.', 1714839710); + } + + return $this->buildForWorkspaceAndContentStream($workspaceName, ContentStreamId::fromString($row['currentcontentstreamid'])); + } + + public function buildForWorkspaceAndContentStream(WorkspaceName $workspaceName, ContentStreamId $contentStreamId): ContentGraphInterface + { + return new ContentHyperGraph($this->databaseClient, $this->nodeFactory, $this->contentRepositoryId, $this->nodeTypeManager, $this->tableNamePrefix, $workspaceName, $contentStreamId); + } +} diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php index 114df60ed58..794b625b1cc 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php @@ -26,9 +26,8 @@ use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeVariation; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\SubtreeTagging; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\SchemaBuilder\HypergraphSchemaBuilder; -use Neos\ContentGraph\PostgreSQLAdapter\Domain\Repository\ContentHypergraph; -use Neos\ContentGraph\PostgreSQLAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\PostgreSQLAdapter\Infrastructure\PostgresDbalClientInterface; +use Neos\ContentRepository\Core\ContentGraphFinder; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; @@ -45,18 +44,16 @@ use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\Infrastructure\DbalCheckpointStorage; use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\CheckpointStorageStatusType; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStatus; -use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventEnvelope; /** * The alternate reality-aware hypergraph projector for the PostgreSQL backend via Doctrine DBAL * - * @implements ProjectionInterface + * @implements ProjectionInterface * @internal the parent Content Graph is public */ final class HypergraphProjection implements ProjectionInterface @@ -71,20 +68,13 @@ final class HypergraphProjection implements ProjectionInterface use NodeTypeChange; use NodeVariation; - /** - * @var ContentHypergraph|null Cache for the content graph returned by {@see getState()}, - * so that always the same instance is returned - */ - private ?ContentHypergraph $contentHypergraph = null; private DbalCheckpointStorage $checkpointStorage; private ProjectionHypergraph $projectionHypergraph; public function __construct( private readonly PostgresDbalClientInterface $databaseClient, - private readonly NodeFactory $nodeFactory, - private readonly ContentRepositoryId $contentRepositoryId, - private readonly NodeTypeManager $nodeTypeManager, private readonly string $tableNamePrefix, + private readonly ContentGraphFinder $contentGraphFinder ) { $this->projectionHypergraph = new ProjectionHypergraph($this->databaseClient, $this->tableNamePrefix); $this->checkpointStorage = new DbalCheckpointStorage( @@ -159,11 +149,6 @@ public function reset(): void $this->checkpointStorage->acquireLock(); $this->checkpointStorage->updateAndReleaseLock(SequenceNumber::none()); - - //$contentGraph = $this->getState(); - //foreach ($contentGraph->getSubgraphs() as $subgraph) { - // $subgraph->inMemoryCache->enable(); - //} } private function truncateDatabaseTables(): void @@ -242,18 +227,9 @@ public function getCheckpointStorage(): DbalCheckpointStorage return $this->checkpointStorage; } - public function getState(): ContentHypergraph + public function getState(): ContentGraphFinder { - if (!$this->contentHypergraph) { - $this->contentHypergraph = new ContentHypergraph( - $this->databaseClient, - $this->nodeFactory, - $this->contentRepositoryId, - $this->nodeTypeManager, - $this->tableNamePrefix - ); - } - return $this->contentHypergraph; + return $this->contentGraphFinder; } protected function getProjectionHypergraph(): ProjectionHypergraph diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php index 49f9b7af5be..8ed8bc21557 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php @@ -35,6 +35,7 @@ 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; /** * The PostgreSQL adapter content hypergraph @@ -59,22 +60,34 @@ public function __construct( NodeFactory $nodeFactory, private readonly ContentRepositoryId $contentRepositoryId, private readonly NodeTypeManager $nodeTypeManager, - private readonly string $tableNamePrefix + private readonly string $tableNamePrefix, + public readonly WorkspaceName $workspaceName, + public readonly ContentStreamId $contentStreamId ) { $this->databaseClient = $databaseClient; $this->nodeFactory = $nodeFactory; } + public function getContentRepositoryId(): ContentRepositoryId + { + return $this->contentRepositoryId; + } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } + public function getSubgraph( - ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints ): ContentSubgraphInterface { - $index = $contentStreamId->value . '-' . $dimensionSpacePoint->hash . '-' . $visibilityConstraints->getHash(); + $index = $this->contentStreamId->value . '-' . $dimensionSpacePoint->hash . '-' . $visibilityConstraints->getHash(); if (!isset($this->subhypergraphs[$index])) { $this->subhypergraphs[$index] = new ContentSubhypergraph( $this->contentRepositoryId, - $contentStreamId, + $this->contentStreamId, + $this->workspaceName, $dimensionSpacePoint, $visibilityConstraints, $this->databaseClient, @@ -88,11 +101,9 @@ public function getSubgraph( } public function findRootNodeAggregateByType( - ContentStreamId $contentStreamId, NodeTypeName $nodeTypeName ): NodeAggregate { $rootNodeAggregates = $this->findRootNodeAggregates( - $contentStreamId, FindRootNodeAggregatesFilter::create(nodeTypeName: $nodeTypeName) ); @@ -118,7 +129,6 @@ public function findRootNodeAggregateByType( } public function findRootNodeAggregates( - ContentStreamId $contentStreamId, FindRootNodeAggregatesFilter $filter, ): NodeAggregates { throw new \BadMethodCallException('method findRootNodeAggregates is not implemented yet.', 1645782874); @@ -128,17 +138,15 @@ public function findRootNodeAggregates( * @return \Iterator */ public function findNodeAggregatesByType( - ContentStreamId $contentStreamId, NodeTypeName $nodeTypeName ): \Iterator { return new \Generator(); } public function findNodeAggregateById( - ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId ): ?NodeAggregate { - $query = HypergraphQuery::create($contentStreamId, $this->tableNamePrefix, true); + $query = HypergraphQuery::create($this->contentStreamId, $this->tableNamePrefix, true); $query = $query->withNodeAggregateId($nodeAggregateId); $nodeRows = $query->execute($this->getDatabaseConnection())->fetchAllAssociative(); @@ -150,7 +158,6 @@ public function findNodeAggregateById( } public function findParentNodeAggregateByChildOriginDimensionSpacePoint( - ContentStreamId $contentStreamId, NodeAggregateId $childNodeAggregateId, OriginDimensionSpacePoint $childOriginDimensionSpacePoint ): ?NodeAggregate { @@ -172,7 +179,7 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint( AND ch.contentstreamid = :contentStreamId )'; $parameters = [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamId' => $this->contentStreamId->value, 'childNodeAggregateId' => $childNodeAggregateId->value, 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash ]; @@ -192,10 +199,9 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint( * @return iterable */ public function findParentNodeAggregates( - ContentStreamId $contentStreamId, NodeAggregateId $childNodeAggregateId ): iterable { - $query = HypergraphParentQuery::create($contentStreamId, $this->tableNamePrefix); + $query = HypergraphParentQuery::create($this->contentStreamId, $this->tableNamePrefix); $query = $query->withChildNodeAggregateId($childNodeAggregateId); $nodeRows = $query->execute($this->getDatabaseConnection())->fetchAllAssociative(); @@ -210,11 +216,10 @@ public function findParentNodeAggregates( * @return iterable */ public function findChildNodeAggregates( - ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId ): iterable { $query = HypergraphChildQuery::create( - $contentStreamId, + $this->contentStreamId, $parentNodeAggregateId, $this->tableNamePrefix ); @@ -227,16 +232,12 @@ public function findChildNodeAggregates( ); } - /** - * @return iterable - */ - public function findChildNodeAggregatesByName( - ContentStreamId $contentStreamId, + public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable { + ): ?NodeAggregate { $query = HypergraphChildQuery::create( - $contentStreamId, + $this->contentStreamId, $parentNodeAggregateId, $this->tableNamePrefix ); @@ -244,7 +245,7 @@ public function findChildNodeAggregatesByName( $nodeRows = $query->execute($this->getDatabaseConnection())->fetchAllAssociative(); - return $this->nodeFactory->mapNodeRowsToNodeAggregates( + return $this->nodeFactory->mapNodeRowsToNodeAggregate( $nodeRows, VisibilityConstraints::withoutRestrictions() ); @@ -254,11 +255,10 @@ public function findChildNodeAggregatesByName( * @return iterable */ public function findTetheredChildNodeAggregates( - ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId ): iterable { $query = HypergraphChildQuery::create( - $contentStreamId, + $this->contentStreamId, $parentNodeAggregateId, $this->tableNamePrefix ); @@ -270,14 +270,13 @@ public function findTetheredChildNodeAggregates( } public function getDimensionSpacePointsOccupiedByChildNodeName( - ContentStreamId $contentStreamId, NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, DimensionSpacePointSet $dimensionSpacePointsToCheck ): DimensionSpacePointSet { $query = HypergraphChildQuery::create( - $contentStreamId, + $this->contentStreamId, $parentNodeAggregateId, $this->tableNamePrefix, ['ch.dimensionspacepoint, ch.dimensionspacepointhash'] @@ -318,4 +317,9 @@ private function getDatabaseConnection(): DatabaseConnection { return $this->databaseClient->getConnection(); } + + public function getContentStreamId(): ContentStreamId + { + return $this->contentStreamId; + } } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php index 95d96cbf077..b25c7452294 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentSubhypergraph.php @@ -52,6 +52,7 @@ 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; /** * The content subgraph application repository @@ -76,6 +77,7 @@ public function __construct( private ContentRepositoryId $contentRepositoryId, private ContentStreamId $contentStreamId, + private WorkspaceName $workspaceName, private DimensionSpacePoint $dimensionSpacePoint, private VisibilityConstraints $visibilityConstraints, private PostgresDbalClientInterface $databaseClient, @@ -85,14 +87,24 @@ public function __construct( ) { } - public function getIdentity(): ContentSubgraphIdentity + public function getContentRepositoryId(): ContentRepositoryId { - return ContentSubgraphIdentity::create( - $this->contentRepositoryId, - $this->contentStreamId, - $this->dimensionSpacePoint, - $this->visibilityConstraints - ); + return $this->contentRepositoryId; + } + + public function getWorkspaceName(): WorkspaceName + { + return $this->workspaceName; + } + + public function getDimensionSpacePoint(): DimensionSpacePoint + { + return $this->dimensionSpacePoint; + } + + public function getVisibilityConstraints(): VisibilityConstraints + { + return $this->visibilityConstraints; } public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node @@ -534,8 +546,14 @@ private function getDatabaseConnection(): DatabaseConnection return $this->databaseClient->getConnection(); } - public function jsonSerialize(): ContentSubgraphIdentity + /** + * @return array + */ + public function jsonSerialize(): array { - return $this->getIdentity(); + return [ + 'workspaceName' => $this->workspaceName, + 'dimensionSpacePoint' => $this->dimensionSpacePoint, + ]; } } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index da9b96cf28c..dbd29ff6642 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -37,11 +37,13 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * The node factory for mapping database rows to nodes and node aggregates @@ -77,17 +79,14 @@ public function mapNodeRowToNode( : null; return Node::create( - ContentSubgraphIdentity::create( - $this->contentRepositoryId, - $contentStreamId ?: ContentStreamId::fromString($nodeRow['contentstreamid']), - $dimensionSpacePoint ?: DimensionSpacePoint::fromJsonString($nodeRow['dimensionspacepoint']), - $visibilityConstraints - ), + $this->contentRepositoryId, + // todo use actual workspace name + WorkspaceName::fromString('missing'), + $dimensionSpacePoint ?: DimensionSpacePoint::fromJsonString($nodeRow['dimensionspacepoint']), NodeAggregateId::fromString($nodeRow['nodeaggregateid']), OriginDimensionSpacePoint::fromJsonString($nodeRow['origindimensionspacepoint']), NodeAggregateClassification::from($nodeRow['classification']), NodeTypeName::fromString($nodeRow['nodetypename']), - $nodeType, new PropertyCollection( SerializedPropertyValues::fromJsonString($nodeRow['properties']), $this->propertyConverter @@ -102,6 +101,9 @@ public function mapNodeRowToNode( isset($nodeRow['lastmodified']) ? self::parseDateTimeString($nodeRow['lastmodified']) : null, isset($nodeRow['originallastmodified']) ? self::parseDateTimeString($nodeRow['originallastmodified']) : null, ), + $visibilityConstraints, + $nodeType, + $contentStreamId ?: ContentStreamId::fromString($nodeRow['contentstreamid']), ); } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/HypergraphProjectionFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/HypergraphProjectionFactory.php index 71c076400b6..3f8189c6d01 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/HypergraphProjectionFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/HypergraphProjectionFactory.php @@ -7,6 +7,7 @@ use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\HypergraphProjection; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\PostgreSQLAdapter\Infrastructure\PostgresDbalClientInterface; +use Neos\ContentRepository\Core\ContentGraphFinder; use Neos\ContentRepository\Core\Factory\ProjectionFactoryDependencies; use Neos\ContentRepository\Core\Projection\ProjectionFactoryInterface; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; @@ -36,16 +37,16 @@ public function build( $projectionFactoryDependencies->contentRepositoryId ); - return new HypergraphProjection( - $this->dbalClient, - new NodeFactory( - $projectionFactoryDependencies->contentRepositoryId, - $projectionFactoryDependencies->nodeTypeManager, - $projectionFactoryDependencies->propertyConverter - ), + $nodeFactory = new NodeFactory( $projectionFactoryDependencies->contentRepositoryId, $projectionFactoryDependencies->nodeTypeManager, - $tableNamePrefix + $projectionFactoryDependencies->propertyConverter + ); + + return new HypergraphProjection( + $this->dbalClient, + $tableNamePrefix, + new ContentGraphFinder(new ContentHyperGraphFactory($this->dbalClient, $nodeFactory, $projectionFactoryDependencies->contentRepositoryId, $projectionFactoryDependencies->nodeTypeManager, $tableNamePrefix)) ); } } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/GherkinPyStringNodeBasedNodeTypeManagerFactory.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/GherkinPyStringNodeBasedNodeTypeManagerFactory.php index faf06ce67ae..10eff55b20c 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/GherkinPyStringNodeBasedNodeTypeManagerFactory.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/GherkinPyStringNodeBasedNodeTypeManagerFactory.php @@ -16,10 +16,7 @@ use Behat\Gherkin\Node\PyStringNode; use Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider; -use Neos\ContentRepository\Core\NodeType\NodeLabelGeneratorInterface; -use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\Factory\NodeTypeManager\NodeTypeManagerFactoryInterface; use Symfony\Component\Yaml\Yaml; diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/01-CreateRootNodeAggregateWithNode_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/01-CreateRootNodeAggregateWithNode_ConstraintChecks.feature index b1ff005357c..399af60a076 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/01-CreateRootNodeAggregateWithNode_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/01-CreateRootNodeAggregateWithNode_ConstraintChecks.feature @@ -27,7 +27,7 @@ Feature: Create a root node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -40,7 +40,7 @@ Feature: Create a root node aggregate | workspaceName | "i-do-not-exist" | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository:Root" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to create a root node aggregate in a closed content stream: When the command CloseContentStream is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/02-CreateRootNodeAggregateWithNode_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/02-CreateRootNodeAggregateWithNode_WithoutDimensions.feature index 9f86bc72a56..5573499f8a6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/02-CreateRootNodeAggregateWithNode_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/02-CreateRootNodeAggregateWithNode_WithoutDimensions.feature @@ -25,7 +25,7 @@ Feature: Create a root node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Scenario: Create the initial root node aggregate using valid payload without dimensions When the command CreateRootNodeAggregateWithNode is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/03-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/03-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithoutDimensions.feature index 163142ec3dd..28f465e078a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/03-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/03-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithoutDimensions.feature @@ -38,7 +38,7 @@ Feature: Create a root node aggregate with tethered children | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And I am user identified by "initiating-user-identifier" Scenario: Create root node with tethered children @@ -133,7 +133,7 @@ Feature: Create a root node aggregate with tethered children | Key | Value | | text | "my sub sub default" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no parent node And I expect this node to have the following child nodes: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/04-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/04-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithDimensions.feature index 2f4455862d8..8bc71cbd9e9 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/04-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/04-CreateRootNodeAggregateWithNodeAndTetheredChildren_WithDimensions.feature @@ -40,7 +40,7 @@ Feature: Create a root node aggregate with tethered children | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And I am user identified by "initiating-user-identifier" Scenario: Create root node with tethered children @@ -173,7 +173,7 @@ Feature: Create a root node aggregate with tethered children | Key | Value | | text | "my sub sub default" | - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no parent node And I expect this node to have the following child nodes: @@ -202,7 +202,7 @@ Feature: Create a root node aggregate with tethered children And I expect this node to have no references And I expect this node to not be referenced - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no parent node And I expect this node to have the following child nodes: @@ -232,7 +232,7 @@ Feature: Create a root node aggregate with tethered children And I expect this node to not be referenced - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no parent node And I expect this node to have the following child nodes: @@ -262,7 +262,7 @@ Feature: Create a root node aggregate with tethered children And I expect this node to not be referenced - When I am in the active content stream of workspace "live" and dimension space point {"language": "en_US"} + When I am in workspace "live" and dimension space point {"language": "en_US"} And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no parent node And I expect this node to have the following child nodes: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/05-CreateRootNodeAggregateWithNode_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/05-CreateRootNodeAggregateWithNode_WithDimensions.feature index af98e698f3f..e8370d00f6f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/05-CreateRootNodeAggregateWithNode_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/01-RootNodeCreation/05-CreateRootNodeAggregateWithNode_WithDimensions.feature @@ -26,7 +26,7 @@ Feature: Create a root node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Scenario: Create the initial root node aggregate using valid payload with dimensions When the command CreateRootNodeAggregateWithNode is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature index a14305214da..6e0b267686b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature @@ -38,7 +38,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -54,7 +54,7 @@ Feature: Create node aggregate with node | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "document" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to create a node aggregate in a workspace whose content stream is closed: When the command CloseContentStream is executed with payload: @@ -149,11 +149,10 @@ Feature: Create node aggregate with node | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | - | originDimensionSpacePoint | {} | | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" Scenario: Try to create a node aggregate with a property the node type does not declare When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature index 66512883875..75b53cfbcc3 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature @@ -9,8 +9,8 @@ Feature: Create node aggregate with node Background: Given using the following content dimensions: - | Identifier | Values | Generalizations | - | language | mul, de, gsw | gsw->de->mul | + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | And using the following node types: """yaml 'Neos.ContentRepository.Testing:Node': @@ -40,7 +40,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And I am in dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -54,12 +54,91 @@ Feature: Create node aggregate with node | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "lady-eleonode-rootford" | - | originDimensionSpacePoint | {"language":"gsw"} | + | originDimensionSpacePoint | {"example":"spec"} | And the graph projection is fully up to date And the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "sir-david-nodenborough" | - | originDimensionSpacePoint | {"language":"de"} | + | originDimensionSpacePoint | {"example":"source"} | Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint" + + Scenario: Try to create a node aggregate using a name that is already partially covered by one of its siblings + Given the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"example":"peer"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + And the graph projection is fully up to date + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to create a node aggregate with a root parent and a sibling already claiming the name + # root nodes are special in that they have the empty DSP as origin, wich may affect constraint checks + When the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeName | "document" | + And the graph projection is fully up to date + And the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeName | "document" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to create a node aggregate using a name of a not yet existent, tethered child of the parent + Given the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + And the graph projection is fully up to date + Given I change the node types in content repository "default" to: + """yaml + 'Neos.ContentRepository.Testing:LeafNode': {} + 'Neos.ContentRepository.Testing:Node': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:LeafNode' + properties: + postalAddress: + type: 'Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress' + 'Neos.ContentRepository.Testing:NodeWithInvalidPropertyType': + properties: + postalAddress: + type: '\I\Do\Not\Exist' + 'Neos.ContentRepository.Testing:NodeWithInvalidDefaultValue': + properties: + postalAddress: + type: 'Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress' + defaultValue: + iDoNotExist: 'whatever' + 'Neos.ContentRepository.Testing:AbstractNode': + abstract: true + """ + # We don't run structure adjustments here on purpose + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:LeafNode" | + | parentNodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeName | "tethered" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature index 4bbca7c61d7..040ab0e5576 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/03-CreateNodeAggregateWithNode_WithoutDimensions.feature @@ -30,7 +30,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And I am in dimension space point {} And I am user identified by "initiating-user-identifier" And the command CreateRootNodeAggregateWithNode is executed with payload: @@ -149,7 +149,7 @@ Feature: Create node aggregate with node | Key | Value | | defaultText | "my default" | - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect the subgraph projection to consist of exactly 4 nodes And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no parent node @@ -201,7 +201,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And I am in dimension space point {} And I am user identified by "initiating-user-identifier" And the command CreateRootNodeAggregateWithNode is executed with payload: @@ -241,7 +241,7 @@ Feature: Create node aggregate with node | nodeAggregateClassification | "regular" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "sir-nodeward-nodington-iii" and node path "esquire" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no child nodes @@ -285,7 +285,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And I am in dimension space point {} And I am user identified by "initiating-user-identifier" And the command CreateRootNodeAggregateWithNode is executed with payload: @@ -407,7 +407,7 @@ Feature: Create node aggregate with node | Key | Value | | text | "my sub sub default" | - When I am in the active content stream of workspace "live" and dimension space point [] + When I am in workspace "live" and dimension space point [] And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no parent node And I expect this node to have the following child nodes: @@ -469,7 +469,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And I am in dimension space point {} And I am user identified by "initiating-user-identifier" And the command CreateRootNodeAggregateWithNode is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/04-CreateNodeAggregateWithNode_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/04-CreateNodeAggregateWithNode_WithDimensions.feature index 96d4734ebce..ff82b174f1c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/04-CreateNodeAggregateWithNode_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/04-CreateNodeAggregateWithNode_WithDimensions.feature @@ -30,7 +30,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/05-CreateNodeAggregateWithNode_ComplexDefaultAndInitialProperties.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/05-CreateNodeAggregateWithNode_ComplexDefaultAndInitialProperties.feature index 22f043d0590..5e55c88ec77 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/05-CreateNodeAggregateWithNode_ComplexDefaultAndInitialProperties.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/05-CreateNodeAggregateWithNode_ComplexDefaultAndInitialProperties.feature @@ -55,7 +55,7 @@ Feature: Create a node aggregate with complex default values | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And I am in dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/06-CreateNodeAggregateWithNode_NodeTypeConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/06-CreateNodeAggregateWithNode_NodeTypeConstraintChecks.feature index 49a1f36fe45..bbea237f9c4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/06-CreateNodeAggregateWithNode_NodeTypeConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/06-CreateNodeAggregateWithNode_NodeTypeConstraintChecks.feature @@ -32,7 +32,6 @@ Feature: Create node aggregate with node """ And using identifier "default", I define a content repository And I am in content repository "default" - And I am in workspace "live" And I am user identified by "initiating-user-identifier" And the command CreateRootWorkspace is executed with payload: | Key | Value | @@ -41,8 +40,7 @@ Feature: Create node aggregate with node | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in content stream "cs-identifier" - And I am in dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature index d4d2be10e79..4db55956655 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/01-CreateNodeVariant_ConstraintChecks.feature @@ -26,7 +26,7 @@ Feature: Create node variant | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"market":"DE", "language":"gsw"} + And I am in workspace "live" and dimension space point {"market":"DE", "language":"gsw"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -48,7 +48,7 @@ Feature: Create node variant | nodeAggregateId | "sir-david-nodenborough" | | sourceOrigin | {"market":"CH", "language":"gsw"} | | targetOrigin | {"market":"DE", "language":"de"} | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to create a variant in a workspace that does not exist When the command CloseContentStream is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/02-CreateNodeSpecializationVariant.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/02-CreateNodeSpecializationVariant.feature index 38734f8514a..66200bdea63 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/02-CreateNodeSpecializationVariant.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/02-CreateNodeSpecializationVariant.feature @@ -30,7 +30,7 @@ Feature: Create node specialization | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + And I am in workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -49,7 +49,7 @@ Feature: Create node specialization | invariable-mc-nodeface | invariable-document | nody-mc-nodeface | | Neos.ContentRepository.Testing:LeafDocument | {} | Scenario: check the tree state before the specialization - When I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + When I am in workspace "live" and dimension space point {"example":"source"} And the subtree for node aggregate "lady-eleonode-rootford" with node types "" and 3 levels deep should be: | Level | nodeAggregateId | | 0 | lady-eleonode-rootford | @@ -106,7 +106,7 @@ Feature: Create node specialization And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"spec"},{"example":"leafSpec"}] @@ -143,7 +143,7 @@ Feature: Create node specialization And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"spec"},{"example":"leafSpec"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"spec"} + When I am in workspace "live" and dimension space point {"example":"spec"} Then I expect the subgraph projection to consist of exactly 9 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And the subtree for node aggregate "lady-eleonode-rootford" with node types "" and 3 levels deep should be: @@ -218,7 +218,7 @@ Feature: Create node specialization | cs-identifier;eldest-mc-nodeface;{"example":"source"} | And I expect this node to have no succeeding siblings - When I am in the active content stream of workspace "live" and dimension space point {"example":"leafSpec"} + When I am in workspace "live" and dimension space point {"example":"leafSpec"} Then I expect the subgraph projection to consist of exactly 9 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -335,7 +335,7 @@ Feature: Create node specialization And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"spec"},{"example":"leafSpec"}] @@ -372,7 +372,7 @@ Feature: Create node specialization And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"spec"},{"example":"leafSpec"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"spec"} + When I am in workspace "live" and dimension space point {"example":"spec"} Then I expect the subgraph projection to consist of exactly 9 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -436,7 +436,7 @@ Feature: Create node specialization | cs-identifier;eldest-mc-nodeface;{"example":"source"} | And I expect this node to have no succeeding siblings - When I am in the active content stream of workspace "live" and dimension space point {"example":"leafSpec"} + When I am in workspace "live" and dimension space point {"example":"leafSpec"} Then I expect the subgraph projection to consist of exactly 9 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/03-CreateNodeGeneralizationVariant.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/03-CreateNodeGeneralizationVariant.feature index 55bddb932f0..7fcf542fdf0 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/03-CreateNodeGeneralizationVariant.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/03-CreateNodeGeneralizationVariant.feature @@ -30,7 +30,7 @@ Feature: Create node generalization | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + And I am in workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -123,7 +123,7 @@ Feature: Create node generalization And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"general"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"rootGeneral"},{"example":"general"},{"example":"source"},{"example":"specB"}] @@ -160,7 +160,7 @@ Feature: Create node generalization And I expect this node aggregate to occupy dimension space points [{"example":"source"},{"example":"general"}] And I expect this node aggregate to cover dimension space points [{"example":"general"},{"example":"source"},{"example":"specB"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + When I am in workspace "live" and dimension space point {"example":"source"} Then I expect the subgraph projection to consist of exactly 9 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -221,7 +221,7 @@ Feature: Create node generalization | cs-identifier;eldest-mc-nodeface;{"example":"general"} | And I expect this node to have no succeeding siblings - When I am in the active content stream of workspace "live" and dimension space point {"example":"general"} + When I am in workspace "live" and dimension space point {"example":"general"} Then I expect the subgraph projection to consist of exactly 8 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -286,7 +286,7 @@ Feature: Create node generalization And I expect this node to have no succeeding siblings And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example":"rootGeneral"} + When I am in workspace "live" and dimension space point {"example":"rootGeneral"} Then I expect the subgraph projection to consist of exactly 1 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no child nodes @@ -299,7 +299,7 @@ Feature: Create node generalization And I expect node aggregate identifier "youngest-mc-nodeface" and node path "youngest-document" to lead to no node And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example":"specB"} + When I am in workspace "live" and dimension space point {"example":"specB"} Then I expect the subgraph projection to consist of exactly 8 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -439,7 +439,7 @@ Feature: Create node generalization And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"rootGeneral"},{"example":"general"},{"example":"source"},{"example":"specB"}] @@ -472,7 +472,7 @@ Feature: Create node generalization And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"general"} + When I am in workspace "live" and dimension space point {"example":"general"} Then I expect the subgraph projection to consist of exactly 6 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -511,7 +511,7 @@ Feature: Create node generalization And I expect node aggregate identifier "youngest-mc-nodeface" and node path "youngest-document" to lead to no node And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "specB"} + When I am in workspace "live" and dimension space point {"example": "specB"} Then I expect the subgraph projection to consist of exactly 6 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -625,7 +625,7 @@ Feature: Create node generalization And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"rootGeneral"},{"example":"general"},{"example":"source"},{"example":"specB"}] @@ -658,7 +658,7 @@ Feature: Create node generalization And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"general"} + When I am in workspace "live" and dimension space point {"example":"general"} Then I expect the subgraph projection to consist of exactly 6 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -697,7 +697,7 @@ Feature: Create node generalization And I expect node aggregate identifier "youngest-mc-nodeface" and node path "youngest-document" to lead to no node And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "specB"} + When I am in workspace "live" and dimension space point {"example": "specB"} Then I expect the subgraph projection to consist of exactly 6 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -811,7 +811,7 @@ Feature: Create node generalization And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"rootGeneral"},{"example":"general"},{"example":"source"},{"example":"specB"}] @@ -844,7 +844,7 @@ Feature: Create node generalization And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"rootGeneral"} + When I am in workspace "live" and dimension space point {"example":"rootGeneral"} Then I expect the subgraph projection to consist of exactly 4 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -867,7 +867,7 @@ Feature: Create node generalization And I expect node aggregate identifier "youngest-mc-nodeface" and node path "youngest-document" to lead to no node And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "general"} + When I am in workspace "live" and dimension space point {"example": "general"} Then I expect the subgraph projection to consist of exactly 6 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -906,7 +906,7 @@ Feature: Create node generalization And I expect node aggregate identifier "youngest-mc-nodeface" and node path "youngest-document" to lead to no node And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "specB"} + When I am in workspace "live" and dimension space point {"example": "specB"} Then I expect the subgraph projection to consist of exactly 6 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/04-CreateNodePeerVariant.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/04-CreateNodePeerVariant.feature index 2a3ef7c7f8f..5b60071e11a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/04-CreateNodePeerVariant.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/03-NodeVariation/04-CreateNodePeerVariant.feature @@ -30,7 +30,7 @@ Feature: Create node peer variant | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + And I am in workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -132,7 +132,7 @@ Feature: Create node peer variant And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"peer"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"peer"},{"example":"peerSpec"}] @@ -169,7 +169,7 @@ Feature: Create node peer variant And I expect this node aggregate to occupy dimension space points [{"example":"source"},{"example":"peer"}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"peer"},{"example":"peerSpec"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + When I am in workspace "live" and dimension space point {"example":"source"} Then I expect the subgraph projection to consist of exactly 7 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -206,7 +206,7 @@ Feature: Create node peer variant | cs-identifier;elder-mc-nodeface;{"example":"source"} | And I expect this node to have no succeeding siblings - When I am in the active content stream of workspace "live" and dimension space point {"example":"peer"} + When I am in workspace "live" and dimension space point {"example":"peer"} Then I expect the subgraph projection to consist of exactly 8 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -271,7 +271,7 @@ Feature: Create node peer variant And I expect this node to have no succeeding siblings And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example":"peerSpec"} + When I am in workspace "live" and dimension space point {"example":"peerSpec"} Then I expect the subgraph projection to consist of exactly 8 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -410,7 +410,7 @@ Feature: Create node peer variant And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"peer"},{"example":"peerSpec"}] @@ -443,7 +443,7 @@ Feature: Create node peer variant And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"peer"} + When I am in workspace "live" and dimension space point {"example":"peer"} Then I expect the subgraph projection to consist of exactly 5 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -473,7 +473,7 @@ Feature: Create node peer variant And I expect node aggregate identifier "youngest-mc-nodeface" and node path "youngest-document" to lead to no node And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "peerSpec"} + When I am in workspace "live" and dimension space point {"example": "peerSpec"} Then I expect the subgraph projection to consist of exactly 5 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -577,7 +577,7 @@ Feature: Create node peer variant And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"peer"},{"example":"peerSpec"}] @@ -610,7 +610,7 @@ Feature: Create node peer variant And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"peer"} + When I am in workspace "live" and dimension space point {"example":"peer"} Then I expect the subgraph projection to consist of exactly 5 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -640,7 +640,7 @@ Feature: Create node peer variant And I expect node aggregate identifier "youngest-mc-nodeface" and node path "youngest-document" to lead to no node And I expect node aggregate identifier "invariable-mc-nodeface" and node path "document/invariable-document" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"example": "peerSpec"} + When I am in workspace "live" and dimension space point {"example": "peerSpec"} Then I expect the subgraph projection to consist of exactly 5 nodes Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -721,7 +721,7 @@ Feature: Create node peer variant And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;invariable-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"peer"},{"example":"peerSpec"}] @@ -754,7 +754,7 @@ Feature: Create node peer variant And I expect this node aggregate to occupy dimension space points [{"example":"source"}] And I expect this node aggregate to cover dimension space points [{"example":"source"}] - When I am in the active content stream of workspace "live" and dimension space point {"example":"peer"} + When I am in workspace "live" and dimension space point {"example":"peer"} Then I expect the subgraph projection to consist of exactly 4 nodes And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -772,7 +772,7 @@ Feature: Create node peer variant | elder-child-document | cs-identifier;elder-child-mc-nodeface;{"example":"peer"} | And I expect node aggregate identifier "elder-child-mc-nodeface" and node path "elder-document/elder-child-document" to lead to node cs-identifier;elder-child-mc-nodeface;{"example":"peer"} - When I am in the active content stream of workspace "live" and dimension space point {"example": "peerSpec"} + When I am in workspace "live" and dimension space point {"example": "peerSpec"} Then I expect the subgraph projection to consist of exactly 4 nodes And I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/01-SetNodeProperties_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/01-SetNodeProperties_ConstraintChecks.feature index 2ac617f4d44..9220aa46393 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/01-SetNodeProperties_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/01-SetNodeProperties_ConstraintChecks.feature @@ -26,7 +26,7 @@ Feature: Set node properties: Constraint checks | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -45,7 +45,7 @@ Feature: Set node properties: Constraint checks | nodeAggregateId | "nody-mc-nodeface" | | originDimensionSpacePoint | {"language":"de"} | | propertyValues | {"text":"New text"} | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to set properties in a workspace whose content stream is closed When the command CloseContentStream is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/02-SetNodeProperties.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/02-SetNodeProperties.feature index a48915306a0..6f9cbbbfae8 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/02-SetNodeProperties.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/02-SetNodeProperties.feature @@ -64,7 +64,7 @@ Feature: Set properties | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"mul"} + And I am in workspace "live" and dimension space point {"language":"mul"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/03-SetNodeProperties_PropertyScopes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/03-SetNodeProperties_PropertyScopes.feature index 40fe2c65f85..e1efc50ef85 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/03-SetNodeProperties_PropertyScopes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/04-NodeModification/03-SetNodeProperties_PropertyScopes.feature @@ -37,7 +37,7 @@ Feature: Set node properties with different scopes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"mul"} + And I am in workspace "live" and dimension space point {"language":"mul"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature index c374e45a337..eb15cb9cc55 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/01-SetNodeReferences_ConstraintChecks.feature @@ -46,7 +46,7 @@ Feature: Constraint checks on SetNodeReferences | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -77,7 +77,7 @@ Feature: Constraint checks on SetNodeReferences | sourceNodeAggregateId | "source-nodandaise" | | referenceName | "referenceProperty" | | references | [{"target":"anthony-destinode"}] | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" with code 1710407870 + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" with code 1513924741 # checks for sourceNodeAggregateId Scenario: Try to reference nodes in a non-existent node aggregate diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature index 287ab61f562..89e816585d1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/02-SetNodeReferences_WithoutDimensions.feature @@ -51,7 +51,7 @@ Feature: Node References without Dimensions | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/03-SetNodeReferences_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/03-SetNodeReferences_WithDimensions.feature index 466f0e67602..9e938dd1427 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/03-SetNodeReferences_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/03-SetNodeReferences_WithDimensions.feature @@ -30,7 +30,7 @@ Feature: Node References with Dimensions | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -49,7 +49,7 @@ Feature: Node References with Dimensions | references | [{"target": "anthony-destinode"}] | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -59,7 +59,7 @@ Feature: Node References with Dimensions | Name | Node | Properties | | referenceProperty | cs-identifier;source-nodandaise;{"language": "de"} | null | - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/04-SetNodeReferences_PropertyScopes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/04-SetNodeReferences_PropertyScopes.feature index 0bc4edb6565..2fea3b4a00f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/04-SetNodeReferences_PropertyScopes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/04-SetNodeReferences_PropertyScopes.feature @@ -44,7 +44,7 @@ Feature: Set node properties with different scopes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"mul"} + And I am in workspace "live" and dimension space point {"language":"mul"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -120,7 +120,7 @@ Feature: Set node properties with different scopes | references | [{"target": "anthony-destinode"}] | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} + When I am in workspace "live" and dimension space point {"language": "mul"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "mul"} And I expect this node to have the following references: | Name | Node | Properties | @@ -133,7 +133,7 @@ Feature: Set node properties with different scopes | nodeAggregateScopedReference | cs-identifier;source-nodandaise;{"language": "mul"} | null | | nodeAggregateScopedReferences | cs-identifier;source-nodandaise;{"language": "mul"} | null | - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -157,7 +157,7 @@ Feature: Set node properties with different scopes | unscopedReference | cs-identifier;source-nodandaise;{"language": "de"} | null | | unscopedReferences | cs-identifier;source-nodandaise;{"language": "de"} | null | - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "gsw"} And I expect this node to have the following references: | Name | Node | Properties | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/05-NodeVariation_After_NodeReferencing.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/05-NodeVariation_After_NodeReferencing.feature index 35bfb620617..b09549fa2d9 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/05-NodeVariation_After_NodeReferencing.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/05-NodeReferencing/05-NodeVariation_After_NodeReferencing.feature @@ -30,7 +30,7 @@ Feature: Node References with Dimensions | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -57,7 +57,7 @@ Feature: Node References with Dimensions And the graph projection is fully up to date # after specialization, the reference must still exist on the specialized node - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "ch"} And I expect this node to have the following references: | Name | Node | Properties | @@ -68,7 +68,7 @@ Feature: Node References with Dimensions | referenceProperty | cs-identifier;source-nodandaise;{"language": "ch"} | null | # the reference must also exist on the non-touched nodes - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -89,7 +89,7 @@ Feature: Node References with Dimensions And the graph projection is fully up to date # reference to self (modified 2 lines above) - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "ch"} And I expect this node to have the following references: | Name | Node | Properties | @@ -99,7 +99,7 @@ Feature: Node References with Dimensions | referenceProperty | cs-identifier;source-nodandaise;{"language": "ch"} | null | # unmodified on the untouched nodes - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -127,7 +127,7 @@ Feature: Node References with Dimensions # on the specialization, the reference exists. - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "ch"} And I expect this node to have the following references: | Name | Node | Properties | @@ -138,7 +138,7 @@ Feature: Node References with Dimensions | referenceProperty | cs-identifier;source-nodandaise;{"language": "ch"} | null | # on the other nodes, the reference does not exist. - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have no references @@ -170,7 +170,7 @@ Feature: Node References with Dimensions And the graph projection is fully up to date # after creating a peer, the reference must still exist on the peer node - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "en"} And I expect this node to have the following references: | Name | Node | Properties | @@ -181,7 +181,7 @@ Feature: Node References with Dimensions | referenceProperty | cs-identifier;source-nodandaise;{"language": "en"} | null | # the reference must also exist on the non-touched nodes - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -191,7 +191,7 @@ Feature: Node References with Dimensions | Name | Node | Properties | | referenceProperty | cs-identifier;source-nodandaise;{"language": "de"} | null | - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -212,7 +212,7 @@ Feature: Node References with Dimensions And the graph projection is fully up to date # reference to self (modified 2 lines above) - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "en"} And I expect this node to have the following references: | Name | Node | Properties | @@ -222,7 +222,7 @@ Feature: Node References with Dimensions | referenceProperty | cs-identifier;source-nodandaise;{"language": "en"} | null | # unmodified on the untouched nodes - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -232,7 +232,7 @@ Feature: Node References with Dimensions | Name | Node | Properties | | referenceProperty | cs-identifier;source-nodandaise;{"language": "de"} | null | - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "source-nodandaise" to lead to node cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties | @@ -269,7 +269,7 @@ Feature: Node References with Dimensions And the graph projection is fully up to date # after generalizing, the reference must still exist on the generalized node - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "ch-only" to lead to node cs-identifier;ch-only;{"language": "de"} Then I expect this node to have the following references: | Name | Node | Properties | @@ -280,7 +280,7 @@ Feature: Node References with Dimensions | referenceProperty | cs-identifier;ch-only;{"language": "de"} | null | # the reference must also exist on the non-touched node - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "ch-only" to lead to node cs-identifier;ch-only;{"language": "ch"} Then I expect this node to have the following references: | Name | Node | Properties | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature index 82db6e57770..573a37a422b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature @@ -23,7 +23,7 @@ Feature: Constraint checks on node aggregate disabling | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -39,7 +39,7 @@ Feature: Constraint checks on node aggregate disabling | workspaceName | "i-do-not-exist" | | nodeAggregateId | "sir-david-nodenborough" | | nodeVariantSelectionStrategy | "allVariants" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to disable a node aggregate in a workspace whose content stream is closed When the command CloseContentStream is executed with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature index ed23f5e23db..06a01480aaa 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature @@ -24,7 +24,7 @@ Feature: Disable a node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -58,7 +58,7 @@ Feature: Disable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 5 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;preceding-nodenborough;{} to exist in the content graph @@ -69,7 +69,7 @@ Feature: Disable a node aggregate And I expect the node aggregate "sir-david-nodenborough" to exist And I expect this node aggregate to disable dimension space points [{}] - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And VisibilityConstraints are set to "withoutRestrictions" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature index b4df608c120..6beb1bf8013 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature @@ -26,7 +26,7 @@ Feature: Disable a node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"mul"} + And I am in workspace "live" and dimension space point {"language":"mul"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -68,7 +68,7 @@ Feature: Disable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 6 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;preceding-nodenborough;{"language":"mul"} to exist in the content graph @@ -320,7 +320,7 @@ Feature: Disable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 6 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;preceding-nodenborough;{"language":"mul"} to exist in the content graph diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature index 44b5734afd4..deacbe55ea5 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/04-EnableNodeAggregate_ConstraintChecks.feature @@ -23,7 +23,7 @@ Feature: Enable a node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -39,7 +39,7 @@ Feature: Enable a node aggregate | workspaceName | "i-do-not-exist" | | nodeAggregateId | "sir-david-nodenborough" | | nodeVariantSelectionStrategy | "allVariants" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to enable a non-existing node aggregate When the command EnableNodeAggregate is executed with payload and exceptions are caught: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature index b44170f3870..e684962577c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature @@ -24,7 +24,7 @@ Feature: Enable a node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -64,7 +64,7 @@ Feature: Enable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 5 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;preceding-nodenborough;{} to exist in the content graph @@ -75,7 +75,7 @@ Feature: Enable a node aggregate And I expect the node aggregate "sir-david-nodenborough" to exist And I expect this node aggregate to disable dimension space points [] - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And VisibilityConstraints are set to "frontend" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -146,14 +146,14 @@ Feature: Enable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the node aggregate "sir-david-nodenborough" to exist And I expect this node aggregate to disable dimension space points [] And I expect the node aggregate "nody-mc-nodeface" to exist And I expect this node aggregate to disable dimension space points [{}] - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And VisibilityConstraints are set to "frontend" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: @@ -224,14 +224,14 @@ Feature: Enable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the node aggregate "sir-david-nodenborough" to exist And I expect this node aggregate to disable dimension space points [{}] And I expect the node aggregate "nody-mc-nodeface" to exist And I expect this node aggregate to disable dimension space points [] - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And VisibilityConstraints are set to "frontend" Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have the following child nodes: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature index 97f56b109d3..6ea2bb37452 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature @@ -26,7 +26,7 @@ Feature: Enable a node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"mul"} + And I am in workspace "live" and dimension space point {"language":"mul"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -82,7 +82,7 @@ Feature: Enable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 7 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;preceding-nodenborough;{"language":"mul"} to exist in the content graph @@ -378,7 +378,7 @@ Feature: Enable a node aggregate | tag | "disabled" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 7 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;preceding-nodenborough;{"language":"mul"} to exist in the content graph @@ -706,7 +706,7 @@ Feature: Enable a node aggregate | nodeVariantSelectionStrategy | "allVariants" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the node aggregate "the-great-nodini" to exist And I expect this node aggregate to disable dimension space points [] diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/07-CreateNodeAggregateWithNodeWithDisabledAncestor_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/07-CreateNodeAggregateWithNodeWithDisabledAncestor_WithoutDimensions.feature index 1d236a45a38..a2cd0880fb1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/07-CreateNodeAggregateWithNodeWithDisabledAncestor_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/07-CreateNodeAggregateWithNodeWithDisabledAncestor_WithoutDimensions.feature @@ -22,7 +22,7 @@ Feature: Creation of nodes underneath disabled nodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature index 48ca1fa841d..f566fe2fb78 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/08-CreateNodeAggregateWithNodeWithDisabledAncestor_WithDimensions.feature @@ -24,7 +24,7 @@ Feature: Creation of nodes underneath disabled nodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"mul"} + And I am in workspace "live" and dimension space point {"language":"mul"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature index d7acb6a6c92..02b888a19df 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature @@ -22,7 +22,7 @@ Feature: Variation of hidden nodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"mul"} + And I am in workspace "live" and dimension space point {"language":"mul"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/01-RemoveNodeAggregate_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/01-RemoveNodeAggregate_ConstraintChecks.feature index bc0798286f2..d40ecf604c5 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/01-RemoveNodeAggregate_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/01-RemoveNodeAggregate_ConstraintChecks.feature @@ -27,7 +27,7 @@ Feature: Remove NodeAggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -44,7 +44,7 @@ Feature: Remove NodeAggregate | nodeAggregateId | "sir-david-nodenborough" | | coveredDimensionSpacePoint | {"language":"de"} | | nodeVariantSelectionStrategy | "allVariants" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to remove a node aggregate in a workspace whose content stream is closed When the command CloseContentStream is executed with payload: 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 96d5fdbf148..a1f9d63b75a 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 @@ -24,7 +24,7 @@ Feature: Remove NodeAggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/03-RemoveNodeAggregate_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/03-RemoveNodeAggregate_WithDimensions.feature index 7039cbfc02b..38f12dc934a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/03-RemoveNodeAggregate_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/03-RemoveNodeAggregate_WithDimensions.feature @@ -26,7 +26,7 @@ Feature: Remove NodeAggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"en"} + And I am in workspace "live" and dimension space point {"language":"en"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/04-VariantRecreation.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/04-VariantRecreation.feature index 81027a506e7..d128cf5a5b6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/04-VariantRecreation.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/04-VariantRecreation.feature @@ -32,7 +32,7 @@ Feature: Recreate a node variant | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"en"} + And I am in workspace "live" and dimension space point {"language":"en"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -94,7 +94,7 @@ Feature: Recreate a node variant | targetOrigin | {"language":"de"} | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-ws" and dimension space point {"language": "de"} + When I am in workspace "user-ws" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" and node path "document" to lead to node new-user-cs-id;sir-david-nodenborough;{"language": "de"} Then I expect node aggregate identifier "nodimus-prime" and node path "document/tethered-document" to lead to node new-user-cs-id;nodimus-prime;{"language": "de"} Then I expect node aggregate identifier "nodimus-mediocre" and node path "document/tethered-document/tethered" to lead to node new-user-cs-id;nodimus-mediocre;{"language": "de"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/05-CreateNodeAfterDeletion.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/05-CreateNodeAfterDeletion.feature index e8402d7025e..8a7c075a96e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/05-CreateNodeAfterDeletion.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/05-CreateNodeAfterDeletion.feature @@ -31,7 +31,7 @@ Feature: Create node specialization | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + And I am in workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -69,7 +69,7 @@ Feature: Create node specialization And I expect a node identified by cs-identifier;younger-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"spec"},{"example":"leafSpec"}] diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/06-CreateNodeSpecializationVariantAfterDeletion.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/06-CreateNodeSpecializationVariantAfterDeletion.feature index a0c1ae951a9..ffb3aceb16c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/06-CreateNodeSpecializationVariantAfterDeletion.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/07-NodeRemoval/06-CreateNodeSpecializationVariantAfterDeletion.feature @@ -32,7 +32,7 @@ Feature: Create node specialization | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"example":"source"} + And I am in workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -79,7 +79,7 @@ Feature: Create node specialization And I expect a node identified by cs-identifier;younger-mc-nodeface;{"example":"source"} to exist in the content graph And I expect a node identified by cs-identifier;youngest-mc-nodeface;{"example":"source"} to exist in the content graph - When I am in the active content stream of workspace "live" + When I am in workspace "live" Then I expect the node aggregate "lady-eleonode-rootford" to exist And I expect this node aggregate to occupy dimension space points [{}] And I expect this node aggregate to cover dimension space points [{"example":"source"},{"example":"spec"},{"example":"leafSpec"}] diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature new file mode 100644 index 00000000000..cfb744ffc71 --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature @@ -0,0 +1,359 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Move node to a new parent / within the current parent before a sibling / to the end of the sibling list + + As a user of the CR I want to move a node to a new parent / within the current parent before a sibling / to the end of the sibling list, + without affecting other nodes in the node aggregate. + + These are the base test cases for the NodeAggregateCommandHandler to block invalid commands + + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + 'Neos.ContentRepository.Testing:Content': + constraints: + nodeTypes: + '*': true + 'Neos.ContentRepository.Testing:Document': false + 'Neos.ContentRepository.Testing:DocumentWithTetheredChildNode': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Content' + constraints: + nodeTypes: + '*': true + 'Neos.ContentRepository.Testing:Content': false + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {"example": "source"} + And the graph projection is fully up to date + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | {"example": "source"} | Neos.ContentRepository.Testing:DocumentWithTetheredChildNode | lady-eleonode-rootford | document | {"tethered": "nodewyn-tetherton"} | + | sir-nodeward-nodington-iii | {"example": "source"} | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | esquire | {} | + | anthony-destinode | {"example": "spec"} | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | target-document | {} | + | lady-abigail-nodenborough | {"example": "spec"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | child-target-document | {} | + | nodimus-prime | {"example": "source"} | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | child-document | {} | + | nodimus-mediocre | {"example": "source"} | Neos.ContentRepository.Testing:Document | nodimus-prime | grandchild-document | {} | + | general-nodesworth | {"example": "general"} | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | general-document | {} | + + Scenario: Try to move a node in a non-existing workspace: + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | workspaceName | "non-existing" | + | nodeAggregateId | "sir-david-nodenborough" | + | dimensionSpacePoint | {"example": "source"} | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" + + Scenario: Try to move a node in a workspace whose content stream is closed: + When the command CloseContentStream is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | dimensionSpacePoint | {"example": "source"} | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "ContentStreamIsClosed" + + Scenario: Try to move a non-existing node aggregate: + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "i-do-not-exist" | + | dimensionSpacePoint | {"example": "source"} | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + + Scenario: Try to move a root node aggregate: + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | dimensionSpacePoint | {"example": "source"} | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsRoot" + + Scenario: Try to move a node of a tethered node aggregate: + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nodewyn-tetherton" | + | dimensionSpacePoint | {"example": "source"} | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsTethered" + + Scenario: Try to move a node in a non-existing dimension space point: + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | dimensionSpacePoint | {"example": "nope"} | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "DimensionSpacePointNotFound" + + Scenario: Try to move a node in a dimension space point the aggregate does not cover + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | dimensionSpacePoint | {"example": "general"} | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint" + + Scenario: Try to move a node to a non-existing parent + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "non-existing-parent-identifier" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + + Scenario: Using the scatter strategy, try to move a node to a new, existing parent in a dimension space point the new parent does not cover + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "anthony-destinode" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePointSet" + + Scenario: Using the gatherSpecializations strategy, try to move a node to a new, existing parent in a dimension space point with a specialization the new parent does not cover + # reduce coverage of the target + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePointSet" + + Scenario: Using the gatherAll strategy, try to move a node to a new, existing parent in a dimension space point with a generalization the new parent does not cover + # increase coverage of the source + Given the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"example": "source"} | + | targetOrigin | {"example": "general"} | + And the graph projection is fully up to date + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherAll" | + Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePointSet" + + Scenario: Using the scatter strategy, try to move a node to a parent that already has a child node of the same name + Given the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | sourceOrigin | {"example": "source"} | + | targetOrigin | {"example": "peer"} | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | nody-mc-nodeface | {"example": "peer"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | document | + + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "peer"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Using the scatter (or really any) strategy, try to move a node to a parent that reserves the name for a tethered child + Given I change the node types in content repository "default" to: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + 'Neos.ContentRepository.Testing:Content': + constraints: + nodeTypes: + '*': true + 'Neos.ContentRepository.Testing:Document': false + 'Neos.ContentRepository.Testing:DocumentWithTetheredChildNode': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Content' + constraints: + nodeTypes: + '*': true + 'Neos.ContentRepository.Testing:Content': false + another-tethered: + type: 'Neos.ContentRepository.Testing:Content' + """ + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | another-tethered | + + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Using the gatherSpecializations strategy, try to move a node to a parent that already has a child node of the same name in a specialization + Given the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | target-document | + + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | relationDistributionStrategy | "gatherSpecializations" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Using the gatherAll strategy, try to move a node to a parent that already has a child node of the same name in a generalization + Given the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | rival-destinode | {"example": "general"} | Neos.ContentRepository.Testing:Document | general-nodesworth | target-document | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | nodimus-prime | target-document | + # Remove the node with the conflicting name in all variants except the generalization + And the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "rival-destinode" | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + And the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | coveredDimensionSpacePoint | {"example": "peer"} | + | nodeAggregateId | "rival-destinode" | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "general-nodesworth" | + | relationDistributionStrategy | "gatherAll" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to move a node to a parent whose node type does not allow child nodes of the node's type + Given the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | other-document | + + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "nodewyn-tetherton" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + Scenario: Using the scatter strategy, try to move a node to a parent whose parent's node type does not allow grand child nodes of the node's type + Given the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Content | lady-eleonode-rootford | content | + + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "nodewyn-tetherton" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeConstraintException" + + # Hint: One might come to the conclusion that we also have to check the other strategies. + # The cases would be that the new parent is partially moved to a new grandparent which then imposes different constraints. + # Yet these grandparent constraints are only imposed on the children of their tethered children, but the latter by definition cannot be moved. + # Thus the preconditions for these test cases cannot be established because the needed commands would fail. + + Scenario: Try to move existing node to a non-existing preceding sibling + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "i-do-not-exist" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + + Scenario: Try to move existing node after a node which is not a sibling + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "lady-abigail-nodenborough" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsNoSibling" + + Scenario: Try to move existing node after a node which is not a child of the new parent + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "spec"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "anthony-destinode" | + | newPrecedingSiblingNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsNoChild" + + Scenario: Try to move existing node to a non-existing succeeding sibling + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "i-do-not-exist" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + + Scenario: Try to move existing node before a node which is not a sibling + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "lady-abigail-nodenborough" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsNoSibling" + + Scenario: Try to move existing node before a node which is not a child of the new parent + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "spec"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "anthony-destinode" | + | newSucceedingSiblingNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsNoChild" + + Scenario: Try to move a node to one of its children + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "nodewyn-tetherton" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsDescendant" + + Scenario: Try to move a node to one of its grandchildren + When the command MoveNodeAggregate is executed with payload and exceptions are caught: + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "nodimus-mediocre" | + | relationDistributionStrategy | "scatter" | + Then the last command should have thrown an exception of type "NodeAggregateIsDescendant" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/02-MoveNodeAggregate_NoNewParent_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/02-MoveNodeAggregate_NoNewParent_Dimensions.feature new file mode 100644 index 00000000000..3be7125e4bf --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/02-MoveNodeAggregate_NoNewParent_Dimensions.feature @@ -0,0 +1,2188 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Move a node with content dimensions + + As a user of the CR I want to move a node + - before the first of its siblings + - between two of its siblings + - after the last of its siblings + + These are the test cases for moving nodes with content dimensions being involved, which is a lot more fun! + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, peer, spec | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example": "general"} + 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 | nodeTypeName | parentNodeAggregateId | nodeName | + | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | document | + | eldest-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | child-document-a | + | elder-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | child-document-b | + | nody-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | child-document-n | + | younger-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | child-document-c | + | youngest-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | child-document-d | + + # Test cases for the gatherAll strategy + + Scenario: Move a complete node aggregate before the first of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate before the first of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate before a siblings which is partially the first + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate before one of its siblings, which is not the first + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate before one of its siblings, which is not the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "younger-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate before one of its siblings, which is the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "youngest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + # The given succeeding sibling cannot be resolved and since younger-mc-nodeface isn't given as a preceding sibling, nothing is done + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate after the last of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + Scenario: Move a complete node aggregate after the last of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + Scenario: Move a complete node aggregate after one of its siblings, which is partially the last + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate after one of its siblings, which is not the last + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate after one of its siblings, which is not the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "younger-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate after one of its siblings, which is the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + # The given preceding sibling cannot be resolved and since elder-mc-nodeface isn't given as a succeeding sibling, nothing is done + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + # Test cases for the gatherSpecializations strategy + + Scenario: Move a node and its specialization variants before the first of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants before the first of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants before a siblings which is partially the first + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants before one of its siblings, which is not the first + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants before one of its siblings, which is not the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "younger-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants before one of its siblings, which is the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "youngest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + # The given succeeding sibling cannot be resolved and since elder-mc-nodeface isn't given as a preceding sibling, nothing is done + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants after the last of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants after the last of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants after one of its siblings, which is partially the last + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants after one of its siblings, which is not the last + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants after one of its siblings, which is not the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "younger-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specialization variants after one of its siblings, which is the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + # The given preceding sibling cannot be resolved and since elder-mc-nodeface isn't given as a succeeding sibling, nothing is done + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + # Test cases for the scatter strategy + + Scenario: Move a single node before the first of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + # Scenario: Move a single node before the first of its siblings, which does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + Scenario: Move a single node before a siblings which is partially the first + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node before one of its siblings, which is not the first + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + # Scenario: Move a single node before one of its siblings, which is not the first and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + # Scenario: Move a single node before one of its siblings, which is the last and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + Scenario: Move a single node after the last of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node after the last of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node after one of its siblings, which is partially the last + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier" + And event at index 9 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node after one of its siblings, which is not the last + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | null | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier" + And event at index 8 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + # Scenario: Move a single node after one of its siblings, which is not the last and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + # Scenario: Move a single node after one of its siblings, which is the first and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/03-MoveNodeAggregate_NewParent_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/03-MoveNodeAggregate_NewParent_Dimensions.feature new file mode 100644 index 00000000000..216e0251cfb --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/03-MoveNodeAggregate_NewParent_Dimensions.feature @@ -0,0 +1,2132 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Move a node with content dimensions + + As a user of the CR I want to move a node to a new parent + - before the first of its new siblings + - between two of its new siblings + - after the last of its new siblings + + These are the test cases for moving nodes with content dimensions being involved, which is a lot more fun! + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example": "general"} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | + | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | parent-document | + | eldest-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | eldest-document | + | elder-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | elder-document | + | younger-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | younger-document | + | youngest-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | youngest-document | + | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | esquire | + | source-elder-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | elder-document | + | nody-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | document | + | source-younger-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | younger-document | + | bustling-mc-nodeface | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | bustling-document | + + # Test cases for the gatherAll strategy + + Scenario: Move a complete node aggregate to a new parent before the first of its new siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent before the first of its new siblings - which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent before a siblings which is partially the first + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent before one of its new siblings, which is not the first + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "general"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent before one of its siblings, which is not the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "younger-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent before one of its siblings, which is the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "youngest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": "youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent after the last of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + Scenario: Move a complete node aggregate to a new parent after the last of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId": null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + Scenario: Move a complete node aggregate to a new parent after one of its siblings, which is partially the last + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId":"youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent after one of its siblings, which is not the last + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId":"youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent after one of its siblings, which is not the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "younger-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId":"youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + Scenario: Move a complete node aggregate to a new parent after one of its siblings, which is the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherAll" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId":"elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null},{"dimensionSpacePoint":{"example":"peer"},"nodeAggregateId":"elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + # The given preceding sibling cannot be resolved and since elder-mc-nodeface isn't given as a succeeding sibling, the node is moved at the end + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + And I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + # Test cases for the gatherSpecializations strategy + + Scenario: Move a node and its specializations to a new parent before the first of its new siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent before the first of its new siblings - which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "eldest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent before a siblings which is partially the first + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent before one of its new siblings, which is not the first + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent before one of its siblings, which is not the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId": "elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId": "younger-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent before one of its siblings, which is the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "youngest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent after the last of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent after the last of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent after one of its siblings, which is partially the last + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent after one of its siblings, which is not the last + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent after one of its siblings, which is not the last and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "younger-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"youngest-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a node and its specializations to a new parent after one of its siblings, which is the first and does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"elder-mc-nodeface"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + # The given preceding sibling cannot be resolved and since elder-mc-nodeface isn't given as a succeeding sibling, the node is moved to the end + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + # Test cases for the scatter strategy + + Scenario: Move a single node to a new parent before the first of its new siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "eldest-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"eldest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + # Scenario: Move a single node to a new parent before the first of its new siblings - which does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + Scenario: Move a single node to a new parent before a siblings which is partially the first + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "eldest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node to a new parent before one of its new siblings, which is not the first + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | "elder-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"elder-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + # Scenario: Move a single node to a new parent before one of its siblings, which is not the first and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + # Scenario: Move a single node to a new parent before one of its siblings, which is the last and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + Scenario: Move a single node to a new parent after the last of its siblings + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node to a new parent after the last of its siblings, which does not exist in all variants + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newSucceedingSiblingNodeAggregateId | null | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node to a new parent after one of its siblings, which is partially the last + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + Scenario: Move a single node to a new parent after one of its siblings, which is not the last + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | newPrecedingSiblingNodeAggregateId | "younger-mc-nodeface" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + Then I expect exactly 13 events to be published on stream "ContentStream:cs-identifier" + And event at index 12 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-david-nodenborough" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"youngest-mc-nodeface"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + | cs-identifier;eldest-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;youngest-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;source-elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;source-younger-mc-nodeface;{"example": "general"} | + + # Scenario: Move a single node to a new parent after one of its siblings, which is not the last and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + # Scenario: Move a single node to a new parent after one of its siblings, which is the first and does not exist in all variants + # This scenario is invalid because the given succeeding sibling does not exist in the selected DSP. + # This constraint check is enforced by the command handler. + + # Other test cases + + Scenario: Move a node that has no name + Given the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface-ii" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | parentNodeAggregateId | "sir-david-nodenborough" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface-ii" | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | relationDistributionStrategy | "scatter" | + + Then I expect exactly 14 events to be published on stream "ContentStream:cs-identifier" + And event at index 13 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface-ii" | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"general"},"nodeAggregateId":null}] | + + And the graph projection is fully up to date + When I am in workspace "live" and dimension space point {"example": "general"} + And I expect node aggregate identifier "nody-mc-nodeface-ii" to lead to node cs-identifier;nody-mc-nodeface-ii;{"example": "general"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/04-MoveNodeAggregate_ScatteredChildren.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/04-MoveNodeAggregate_ScatteredChildren.feature new file mode 100644 index 00000000000..da8bd43063f --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/04-MoveNodeAggregate_ScatteredChildren.feature @@ -0,0 +1,318 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Move a node with content dimensions + + As a user of the CR, when I move a node to a new parent, where that parent or requested siblings have been scattered, + I expect scattered parents to be considered and scattered siblings to be ignored. + The other way around, if a scattered node is moved, the variants are to be gathered again as specified in the command. + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, peer, spec | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example": "general"} + 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 | nodeTypeName | parentNodeAggregateId | nodeName | + | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | parent-document | + | elder-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | elder-document | + | nody-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | document | + | younger-mc-nodeface | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | younger-document | + | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | esquire | + | nodimus-mediocre | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | esquire-child | + | elder-destinode | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | elder-target-document | + | bustling-destinode | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | bustling-target-document | + | younger-destinode | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | younger-target-document | + | elder-child-destinode | Neos.ContentRepository.Testing:Document | nodimus-mediocre | elder-child-target-document | + | younger-child-destinode | Neos.ContentRepository.Testing:Document | nodimus-mediocre | younger-child-target-document | + + Scenario: Scatter a node aggregate by moving a specialization variant to a different parent. Then move another node to the node's parent as a new sibling + Given the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-destinode" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "nodimus-mediocre" | + | newSucceedingSiblingNodeAggregateId | "younger-child-destinode" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | newSucceedingSiblingNodeAggregateId | "bustling-destinode" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 15 events to be published on stream "ContentStream:cs-identifier" + And event at index 14 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"bustling-destinode"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"younger-destinode"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-destinode;{"example": "general"} | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;bustling-destinode;{"example": "general"} | + | cs-identifier;younger-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-destinode;{"example": "general"} | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + Scenario: Scatter a node aggregate by moving a specialization variant to a different parent. Then move another node to the scattered node as a new child + Given the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-destinode" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "nodimus-mediocre" | + | newSucceedingSiblingNodeAggregateId | "younger-child-destinode" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "bustling-destinode" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 15 events to be published on stream "ContentStream:cs-identifier" + And event at index 14 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "bustling-destinode" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":null},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":null}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/bustling-target-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;bustling-destinode;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/bustling-target-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;bustling-destinode;{"example": "general"} + And I expect this node to have no preceding siblings + And I expect this node to have no succeeding siblings + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-mc-nodeface;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-mc-nodeface;{"example": "general"} | + + Scenario: Scatter a node aggregate by moving a specialization variant to a different parent. Then move both variants to a new succeeding sibling only present as sibling in the general one + Given the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-destinode" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "nodimus-mediocre" | + | newSucceedingSiblingNodeAggregateId | "younger-child-destinode" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-destinode" | + | dimensionSpacePoint | {"example": "source"} | + | newSucceedingSiblingNodeAggregateId | "elder-destinode" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 15 events to be published on stream "ContentStream:cs-identifier" + And event at index 14 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "bustling-destinode" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"elder-destinode"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "bustling-destinode" and node path "esquire/bustling-target-document" to lead to node cs-identifier;bustling-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-destinode;{"example": "general"} | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "bustling-destinode" and node path "esquire/bustling-target-document" to lead to node cs-identifier;bustling-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;elder-destinode;{"example": "general"} | + | cs-identifier;younger-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "bustling-destinode" and node path "esquire/esquire-child/bustling-target-document" to lead to node cs-identifier;bustling-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;nodimus-mediocre;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-child-destinode;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-child-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "bustling-destinode" and node path "esquire/bustling-target-document" to lead to node cs-identifier;bustling-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-destinode;{"example": "general"} | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;younger-destinode;{"example": "general"} | + + Scenario: Scatter a node aggregate by moving a specialization variant to a different parent. Then let a sibling variant follow suit and move the sibling before the node in both variants. + # We expect to be the node to be the sibling's succeeding sibling in both variants across parents + Given the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-destinode" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "nodimus-mediocre" | + | newSucceedingSiblingNodeAggregateId | "younger-child-destinode" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-destinode" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "nodimus-mediocre" | + | newPrecedingSiblingNodeAggregateId | "bustling-destinode" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-destinode" | + | dimensionSpacePoint | {"example": "source"} | + | newSucceedingSiblingNodeAggregateId | "bustling-destinode" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + Then I expect exactly 16 events to be published on stream "ContentStream:cs-identifier" + And event at index 15 is of type "NodeAggregateWasMoved" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "elder-destinode" | + | newParentNodeAggregateId | null | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"example":"source"},"nodeAggregateId":"bustling-destinode"},{"dimensionSpacePoint":{"example":"spec"},"nodeAggregateId":"bustling-destinode"}] | + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "elder-destinode" and node path "esquire/elder-target-document" to lead to node cs-identifier;elder-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;bustling-destinode;{"example": "general"} | + | cs-identifier;younger-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "elder-destinode" and node path "esquire/elder-target-document" to lead to node cs-identifier;elder-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;bustling-destinode;{"example": "general"} | + | cs-identifier;younger-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "elder-destinode" and node path "esquire/esquire-child/elder-target-document" to lead to node cs-identifier;elder-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;nodimus-mediocre;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;elder-child-destinode;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;bustling-destinode;{"example": "general"} | + | cs-identifier;younger-child-destinode;{"example": "general"} | + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "elder-destinode" and node path "esquire/elder-target-document" to lead to node cs-identifier;elder-destinode;{"example": "general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example": "general"} + And I expect this node to have the following preceding siblings: + | NodeDiscriminator | + | cs-identifier;nodimus-mediocre;{"example": "general"} | + And I expect this node to have the following succeeding siblings: + | NodeDiscriminator | + | cs-identifier;bustling-destinode;{"example": "general"} | + | cs-identifier;younger-destinode;{"example": "general"} | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/05-MoveNodeAggregate_SubtreeTags.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/05-MoveNodeAggregate_SubtreeTags.feature new file mode 100644 index 00000000000..06d9bc48849 --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/05-MoveNodeAggregate_SubtreeTags.feature @@ -0,0 +1,2790 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Move a node aggregate into and out of a tagged parent + + As a user of the CR I want to move a node that + - is untagged + - tags itself (partially or completely) + - is tagged by one of its ancestors (partially or completely) + + to a new parent that + - is untagged (except when the source is untagged too, which is covered by the other move test cases) + - tags itself, same or differently than the source (partially or completely) + - is tagged by one of its ancestors, same or differently than the source (partially or completely) + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, peer, spec | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example": "general"} + 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 | parentNodeAggregateId | nodeTypeName | nodeName | + | sir-david-nodenborough | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | parent-document | + | nody-mc-nodeface | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | document | + | nodimus-mediocre | nody-mc-nodeface | Neos.ContentRepository.Testing:Document | child-document | + | sir-nodeward-nodington-iii | lady-eleonode-rootford | Neos.ContentRepository.Testing:Document | esquire | + | nodimus-prime | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | esquire-child | + + # move untagged to self-tagging + + Scenario: Move an untagged node to a new parent that tags itself + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move an untagged node to a new parent that tags itself partially + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + # move untagged to tagged + + Scenario: Move an untagged node to a new parent that is tagged by its ancestors + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move an untagged node to a new parent that is partially tagged by its ancestors + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + # move tagging to untagged + + Scenario: Move a node that tags itself to a new, untagged parent + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new, untagged parent + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + # move tagging to tagging + + Scenario: Move a node that tags itself to a new parent that tags the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself to a new parent that tags the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that tags the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that tags the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself to a new parent that tags differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself to a new parent that tags differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that tags differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that tags differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + # move tagging to tagged + + Scenario: Move a node that tags itself to a new parent that is tagged the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself to a new parent that is tagged the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that is tagged the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that is tagged the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself to a new parent that is tagged differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself to a new parent that is tagged differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that tags differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a node that tags itself partially to a new parent that is tagged differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "tag1" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1,tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + # move tagged to untagged + + Scenario: Move a tagged node to a new, untagged parent + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new, untagged parent + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + # move tagged to tagging + + Scenario: Move a tagged node to a new parent that tags the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a tagged node to a new parent that tags the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that tags the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that tags the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a tagged node to a new parent that tags differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a tagged node to a new parent that tags differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that tags differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that tags differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "nodimus-prime" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + # move tagged to tagged + + Scenario: Move a tagged node to a new parent that is tagged the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a tagged node to a new parent that is tagged the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that is tagged the same + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that is tagged the same, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag1" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a tagged node to a new parent that is tagged differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a tagged node to a new parent that is tagged differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that is tagged differently + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "source"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + Scenario: Move a partially tagged node to a new parent that is tagged differently, partially + + Given the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | coveredDimensionSpacePoint | {"example": "spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + And the graph projection is fully up to date + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "source"} | + | newParentNodeAggregateId | "nodimus-prime" | + | relationDistributionStrategy | "gatherSpecializations" | + And the graph projection is fully up to date + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nodimus-prime;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "esquire/esquire-child/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "tag2" + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" + + And I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/document/child-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"general"} + And I expect this node to be a child of node cs-identifier;nody-mc-nodeface;{"example":"general"} + And I expect this node to be exactly explicitly tagged "" + And I expect this node to exactly inherit the tags "" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature new file mode 100644 index 00000000000..e3ac3759a33 --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature @@ -0,0 +1,79 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Additional constraint checks after move node capabilities are introduced + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, peer, spec | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': [] + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example": "general"} + 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 | nodeTypeName | parentNodeAggregateId | nodeName | + | sir-david-nodenborough | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | parent-document | + | lady-abigail-nodenborough | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | other-parent-document | + | sir-nodeward-nodington-iii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | esquire | + | general-nodesworth | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | general-document | + | bustling-mc-nodeface | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | document | + + Scenario: Scatter a named node aggregate among different parents, then try to create a new node with the same name under one of the new parents + Given the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-mc-nodeface" | + | dimensionSpacePoint | {"example": "spec"} | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-mc-nodeface" | + | dimensionSpacePoint | {"example": "peer"} | + | newParentNodeAggregateId | "lady-abigail-nodenborough" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + And the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "bustling-mc-nodeface" | + | dimensionSpacePoint | {"example": "general"} | + | newParentNodeAggregateId | "general-nodesworth" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | parentNodeAggregateId | "sir-nodeward-nodington-iii" | + | nodeName | "document" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | parentNodeAggregateId | "lady-abigail-nodenborough" | + | nodeName | "document" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | parentNodeAggregateId | "general-nodesworth" | + | nodeName | "document" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/07-MoveNodeAggregateWithoutDimensions.feature similarity index 85% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateWithoutDimensions.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/07-MoveNodeAggregateWithoutDimensions.feature index 048a6738ceb..fbde6f92b7a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/07-MoveNodeAggregateWithoutDimensions.feature @@ -24,11 +24,11 @@ Feature: Move a node without content dimensions | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | And the event NodeAggregateWithNodeWasCreated was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | @@ -78,7 +78,7 @@ Feature: Move a node without content dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-nodeward-nodington-iii" and node path "esquire" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings @@ -113,7 +113,7 @@ Feature: Move a node without content dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-nodeward-nodington-iii" and node path "esquire" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings @@ -151,10 +151,11 @@ Feature: Move a node without content dimensions Then I expect exactly 7 events to be published on stream "ContentStream:cs-identifier" And event at index 6 is of type "NodeAggregateWasMoved" with payload: - | Key | Expected | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeMoveMappings | [{"movedNodeOrigin":[],"newLocations":[{"coveredDimensionSpacePoint": [], "newParent": {"nodeAggregateId":"sir-nodeward-nodington-iii","originDimensionSpacePoint":[]}}]}] | + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint": [],"nodeAggregateId":null}] | When the graph projection is fully up to date Then I expect the graph projection to consist of exactly 5 nodes @@ -166,7 +167,7 @@ Feature: Move a node without content dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "sir-nodeward-nodington-iii" and node path "esquire" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings @@ -197,10 +198,11 @@ Feature: Move a node without content dimensions | newSucceedingSiblingNodeAggregateId | "sir-nodeward-nodington-iii" | Then I expect exactly 6 events to be published on stream "ContentStream:cs-identifier" And event at index 5 is of type "NodeAggregateWasMoved" with payload: - | Key | Expected | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeMoveMappings | [{"movedNodeOrigin":[],"newLocations":[{"coveredDimensionSpacePoint": [],"newSucceedingSibling":{"nodeAggregateId":"sir-nodeward-nodington-iii","originDimensionSpacePoint":[], "parentNodeAggregateId": "lady-eleonode-rootford", "parentOriginDimensionSpacePoint": []}}]}] | + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint": [],"nodeAggregateId":"sir-nodeward-nodington-iii"}] | When the graph projection is fully up to date Then I expect the graph projection to consist of exactly 4 nodes @@ -211,7 +213,7 @@ Feature: Move a node without content dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "sir-david-nodenborough" and node path "document" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate.feature deleted file mode 100644 index 5dd2d441fa8..00000000000 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate.feature +++ /dev/null @@ -1,262 +0,0 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Move node to a new parent / within the current parent before a sibling / to the end of the sibling list - - As a user of the CR I want to move a node to a new parent / within the current parent before a sibling / to the end of the sibling list, - without affecting other nodes in the node aggregate. - - These are the base test cases for the NodeAggregateCommandHandler to block invalid commands - - Content Structure: - - lady-eleonode-rootford (Neos.ContentRepository:Root) - - sir-david-nodenborough (Neos.ContentRepository.Testing:DocumentWithTetheredChildNode) - - "tethered" nodewyn-tetherton (Neos.ContentRepository.Testing:Content) - - sir-nodeward-nodington-iii (Neos.ContentRepository.Testing:Document) - - Background: - Given using the following content dimensions: - | Identifier | Values | Generalizations | - | market | DE, CH | CH->DE | - | language | de, gsw, fr | gsw->de | - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Document': [] - 'Neos.ContentRepository.Testing:Content': - constraints: - nodeTypes: - '*': true - 'Neos.ContentRepository.Testing:Document': false - 'Neos.ContentRepository.Testing:DocumentWithTetheredChildNode': - childNodes: - tethered: - type: 'Neos.ContentRepository.Testing:Content' - constraints: - nodeTypes: - '*': true - 'Neos.ContentRepository.Testing:Content': false - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" - And the graph projection is fully up to date - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:DocumentWithTetheredChildNode" | - | originDimensionSpacePoint | {"market":"DE", "language":"de"} | - | coveredDimensionSpacePoints | [{"market":"DE", "language":"de"},{"market":"DE", "language":"gsw"},{"market":"CH", "language":"de"},{"market":"CH", "language":"gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nodewyn-tetherton" | - | nodeTypeName | "Neos.ContentRepository.Testing:Content" | - | originDimensionSpacePoint | {"market":"DE", "language":"de"} | - | coveredDimensionSpacePoints | [{"market":"DE", "language":"de"},{"market":"DE", "language":"gsw"},{"market":"CH", "language":"de"},{"market":"CH", "language":"gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "tethered" | - | nodeAggregateClassification | "tethered" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"market":"DE", "language":"de"} | - | coveredDimensionSpacePoints | [{"market":"DE", "language":"de"},{"market":"DE", "language":"gsw"},{"market":"CH", "language":"de"},{"market":"CH", "language":"gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "esquire" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - - Scenario: Try to move a node in a non-existing workspace: - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | workspaceName | "non-existing" | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" - - Scenario: Try to move a node in a workspace whose content stream is closed: - When the command CloseContentStream is executed with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "ContentStreamIsClosed" - - Scenario: Try to move a node of a non-existing node aggregate: - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | nodeAggregateId | "i-do-not-exist" | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" - - Scenario: Try to move a node of a root node aggregate: - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | dimensionSpacePoint | {"market":"DE", "language":"de"} | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateIsRoot" - - Scenario: Try to move a node of a tethered node aggregate: - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | nodeAggregateId | "nodewyn-tetherton" | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateIsTethered" - - Scenario: Try to move a node in a non-existing dimension space point: - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {"market": "nope", "language": "neither"} | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "DimensionSpacePointNotFound" - - Scenario: Try to move a node in a dimension space point the aggregate does not cover - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {"market": "DE", "language": "fr"} | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint" - - Scenario: Try to move existing node to a non-existing parent - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "sir-david-nodenborough" | - | newParentNodeAggregateId | "non-existing-parent-identifier" | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" - - Scenario: Try to move a node to a parent that already has a child node of the same name - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"market": "DE", "language": "de"} | - | coveredDimensionSpacePoints | [{"market": "DE", "language": "de"}, {"market": "DE", "language": "gsw"}, {"market": "CH", "language": "de"}, {"market": "CH", "language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "nody-mc-nodeface" | - | newParentNodeAggregateId | "lady-eleonode-rootford" | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" - - Scenario: Move a node that has no name - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"market": "DE", "language": "de"} | - | coveredDimensionSpacePoints | [{"market": "DE", "language": "de"}, {"market": "DE", "language": "gsw"}, {"market": "CH", "language": "de"}, {"market": "CH", "language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "nody-mc-nodeface" | - | newParentNodeAggregateId | "lady-eleonode-rootford" | - | relationDistributionStrategy | "scatter" | - And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {"market": "DE", "language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{"market":"DE","language":"de"} - - - Scenario: Try to move a node to a parent whose node type does not allow child nodes of the node's type - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"market": "DE", "language": "de"} | - | coveredDimensionSpacePoints | [{"market": "DE", "language": "de"}, {"market": "DE", "language": "gsw"}, {"market": "CH", "language": "de"}, {"market": "CH", "language": "gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "other-document" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "nody-mc-nodeface" | - | newParentNodeAggregateId | "nodewyn-tetherton" | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeConstraintException" - - Scenario: Try to move a node to a parent whose parent's node type does not allow grand child nodes of the node's type - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Content" | - | originDimensionSpacePoint | {"market": "DE", "language": "de"} | - | coveredDimensionSpacePoints | [{"market": "DE", "language": "de"}, {"market": "DE", "language": "gsw"}, {"market": "CH", "language": "de"}, {"market": "CH", "language": "gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "content" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "nody-mc-nodeface" | - | newParentNodeAggregateId | "nodewyn-tetherton" | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeConstraintException" - - Scenario: Try to move existing node to a non-existing succeeding sibling - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | "i-do-not-exist" | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" - - Scenario: Try to move existing node to a non-existing preceding sibling - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "sir-david-nodenborough" | - | newPrecedingSiblingNodeAggregateId | "i-do-not-exist" | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" - - Scenario: Try to move a node to one of its children - When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"market": "DE", "language": "de"} | - | nodeAggregateId | "sir-david-nodenborough" | - | newParentNodeAggregateId | "nodewyn-tetherton" | - | relationDistributionStrategy | "scatter" | - Then the last command should have thrown an exception of type "NodeAggregateIsDescendant" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateConsideringDisableStateWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateConsideringDisableStateWithoutDimensions.feature deleted file mode 100644 index e8f664d3ec1..00000000000 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateConsideringDisableStateWithoutDimensions.feature +++ /dev/null @@ -1,343 +0,0 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Move a node aggregate considering disable state but without content dimensions - - As a user of the CR I want to move a node that - - is disabled by one of its ancestors - - disables itself - - disables any of its descendants - - is enabled - - to a new parent that - - is enabled - - disables itself - - is disabled by one of its ancestors - - These are the test cases without content dimensions being involved - - Background: - Given using no content dimensions - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Document': [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "esquire" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nodimus-prime" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeName | "esquire-child" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - - Scenario: Move a node disabled by one of its ancestors to a new parent that is enabled - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - - # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - And I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/child-document" to lead to node cs-identifier;nody-mc-nodeface;{} - And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{} - - Scenario: Move a node disabled by itself to a new parent that is enabled - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/child-document" to lead to no node - - Scenario: Move a node that disables one of its descendants to a new parent that is enabled - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/document" to lead to no node - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document/child-document" to lead to no node - - Scenario: Move a node that is disabled by one of its ancestors to a new parent that disables itself - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/child-document" to lead to no node - - Scenario: Move a node that is disabled by itself to a new parent that disables itself - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/document" to lead to no node - - Scenario: Move a node that is enabled to a new parent that disables itself - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/document" to lead to no node - - Scenario: Move a node that disables any of its descendants to a new parent that disables itself - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "sir-nodeward-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/document" to lead to no node - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document/child-document" to lead to no node - - Scenario: Move a node that is disabled by one of its ancestors to a new parent that is disabled by one of its ancestors - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "nodimus-prime" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/child-document" to lead to no node - - Scenario: Move a node that is disabled by itself to a new parent that is disabled by one of its ancestors - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "nodimus-prime" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/esquire-child/document" to lead to no node - - Scenario: Move a node that disables any of its descendants to a new parent that is disabled by one of its ancestors - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - And the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "nodimus-prime" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/esquire-child/document" to lead to no node - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document/child-document" to lead to no node - - Scenario: Move a node that is enabled to a new parent that is disabled by one of its ancestors - Given the event SubtreeWasTagged was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | affectedDimensionSpacePoints | [{}] | - | tag | "disabled" | - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "sir-david-nodenborough" | - | dimensionSpacePoint | {} | - | newParentNodeAggregateId | "nodimus-prime" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - And VisibilityConstraints are set to "frontend" - - Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/esquire-child/document" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate_NewParent_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate_NewParent_Dimensions.feature deleted file mode 100644 index e2791f32b28..00000000000 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate_NewParent_Dimensions.feature +++ /dev/null @@ -1,432 +0,0 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Move a node with content dimensions - - As a user of the CR I want to move a node to a new parent - - before the first of its new siblings - - between two of its new siblings - - after the last of its new siblings - - These are the test cases for moving nodes with content dimensions being involved, which is a lot more fun! - - Background: - Given using the following content dimensions: - | Identifier | Values | Generalizations | - | language | mul, de, en, gsw | gsw->de->mul, en->mul | - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Document': [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "anthony-destinode" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document-a" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "berta-destinode" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document-b" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "carl-destinode" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document-c" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "esquire" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeName | "child-document-n" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "lady-abigail-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document2" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - - Scenario: Move a complete node aggregate to a new parent before the first of its new siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | "anthony-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate to a new parent before the first of its new siblings - which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "anthony-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | "anthony-destinode" | - | relationDistributionStrategy | "gatherAll" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate to a new parent before one of its new siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate to a new parent before one of its siblings - which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "berta-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate to a new parent after another of its new siblings - which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "carl-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newPrecedingSiblingNodeAggregateId | "berta-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - Scenario: Move a complete node aggregate to a new parent after the last of its new siblings - with a predecessor which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "carl-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - Scenario: Move a single node in a node aggregate to a new parent after the last of its new siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "de"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | null | - | relationDistributionStrategy | "scatter" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have no succeeding siblings - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - Scenario: Move a node and its specializations in a node aggregate to a new parent after the last of its new siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "de"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newSucceedingSiblingNodeAggregateId | null | - | relationDistributionStrategy | "gatherSpecializations" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have no succeeding siblings - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - Scenario: Move a complete node aggregate to a new parent between siblings with different parents in other variants - Given the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "berta-destinode" | - | dimensionSpacePoint | {"language": "gsw"} | - | newParentNodeAggregateId | "lady-abigail-nodenborough" | - | newSucceedingSiblingNodeAggregateId | null | - | relationDistributionStrategy | "scatter" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | "sir-david-nodenborough" | - | newPrecedingSiblingNodeAggregateId | "anthony-destinode" | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - | relationDistributionStrategy | "gatherAll" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - # An explicitly given parent node aggregate identifier should overrule given sibling identifiers - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate between siblings with different parents in other variants (without explicit new parent) - Given the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "berta-destinode" | - | dimensionSpacePoint | {"language": "gsw"} | - | newParentNodeAggregateId | "lady-abigail-nodenborough" | - | newSucceedingSiblingNodeAggregateId | null | - | relationDistributionStrategy | "scatter" | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newPrecedingSiblingNodeAggregateId | "anthony-destinode" | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - | relationDistributionStrategy | "gatherAll" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - Then I expect node aggregate identifier "nody-mc-nodeface" and node path "document2/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;lady-abigail-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate_NoNewParent_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate_NoNewParent_Dimensions.feature deleted file mode 100644 index fa4d0a3467f..00000000000 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregate_NoNewParent_Dimensions.feature +++ /dev/null @@ -1,573 +0,0 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Move a node with content dimensions - - As a user of the CR I want to move a node - - before the first of its siblings - - between two of its siblings - - after the last of its siblings - - These are the test cases for moving nodes with content dimensions being involved, which is a lot more fun! - - Background: - Given using the following content dimensions: - | Identifier | Values | Generalizations | - | language | mul, de, en, gsw | gsw->de->mul, en->mul | - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Document': [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "anthony-destinode" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document-a" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "berta-destinode" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document-b" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document-n" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "carl-destinode" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document-c" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-nodeward-nodington-iii" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language": "mul"} | - | coveredDimensionSpacePoints | [{"language": "mul"}, {"language": "de"}, {"language": "en"}, {"language": "gsw"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "esquire" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - - Scenario: Move a complete node aggregate before the first of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | null | - | newSucceedingSiblingNodeAggregateId | "anthony-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate before the first of its siblings - which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "anthony-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | null | - | newSucceedingSiblingNodeAggregateId | "anthony-destinode" | - | relationDistributionStrategy | "gatherAll" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate before another of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | null | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate before another of its siblings - which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "berta-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | null | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a complete node aggregate after another of its siblings - which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "carl-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | null | - | newPrecedingSiblingNodeAggregateId | "berta-destinode" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - Scenario: Move a complete node aggregate after the last of its siblings - with a predecessor which does not exist in all variants - Given the event NodeAggregateWasRemoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "carl-destinode" | - | affectedOccupiedDimensionSpacePoints | [] | - | affectedCoveredDimensionSpacePoints | [{"language": "gsw"}] | - And the graph projection is fully up to date - - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newParentNodeAggregateId | null | - | newSucceedingSiblingNodeAggregateId | null | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - Scenario: Move a single node before the first of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newSucceedingSiblingNodeAggregateId | "anthony-destinode" | - | relationDistributionStrategy | "scatter" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a single node between two of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - | relationDistributionStrategy | "scatter" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a single node to the end of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "mul"} | - | relationDistributionStrategy | "scatter" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a node and its specializations before the first of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "de"} | - | newSucceedingSiblingNodeAggregateId | "anthony-destinode" | - | relationDistributionStrategy | "gatherSpecializations" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have no preceding siblings - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a node and its specializations between two of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "de"} | - | newSucceedingSiblingNodeAggregateId | "berta-destinode" | - | relationDistributionStrategy | "gatherSpecializations" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;carl-destinode;{"language": "mul"} | - - Scenario: Move a node and its specializations to the end of its siblings - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "nody-mc-nodeface" | - | dimensionSpacePoint | {"language": "de"} | - | relationDistributionStrategy | "gatherSpecializations" | - And the graph projection is fully up to date - - When I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have the following succeeding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} - And I expect node aggregate identifier "nody-mc-nodeface" and node path "document/child-document-n" to lead to node cs-identifier;nody-mc-nodeface;{"language": "mul"} - And I expect this node to be a child of node cs-identifier;sir-david-nodenborough;{"language": "mul"} - And I expect this node to have the following preceding siblings: - | NodeDiscriminator | - | cs-identifier;carl-destinode;{"language": "mul"} | - | cs-identifier;berta-destinode;{"language": "mul"} | - | cs-identifier;anthony-destinode;{"language": "mul"} | - And I expect this node to have no succeeding siblings - - Scenario: Trigger position update in DBAL graph - Given I am in the active content stream of workspace "live" and dimension space point {"language": "mul"} - # distance i to x: 128 - Given the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | - | lady-nodette-nodington-i | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-i | - | lady-nodette-nodington-x | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-x | - | lady-nodette-nodington-ix | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-ix | - | lady-nodette-nodington-viii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-viii | - | lady-nodette-nodington-vii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-vii | - | lady-nodette-nodington-vi | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-vi | - | lady-nodette-nodington-v | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-v | - | lady-nodette-nodington-iv | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-iv | - | lady-nodette-nodington-iii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-iii | - | lady-nodette-nodington-ii | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | nodington-ii | - # distance ii to x: 64 - When the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-ii" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - # distance iii to x: 32 - And the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-iii" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - # distance iv to x: 16 - And the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-iv" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - # distance v to x: 8 - And the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-v" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - # distance vi to x: 4 - And the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-vi" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - # distance vii to x: 2 - And the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-vii" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - # distance viii to x: 1 -> reorder -> 128 - And the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-viii" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - # distance ix to x: 64 after reorder - And the command MoveNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-nodette-nodington-ix" | - | newSucceedingSiblingNodeAggregateId | "lady-nodette-nodington-x" | - And the graph projection is fully up to date - - Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} - And I expect this node to have the following child nodes: - | Name | NodeDiscriminator | - | document | cs-identifier;sir-david-nodenborough;{"language": "mul"} | - | esquire | cs-identifier;sir-nodeward-nodington-iii;{"language": "mul"} | - | nodington-i | cs-identifier;lady-nodette-nodington-i;{"language": "mul"} | - | nodington-ii | cs-identifier;lady-nodette-nodington-ii;{"language": "mul"} | - | nodington-iii | cs-identifier;lady-nodette-nodington-iii;{"language": "mul"} | - | nodington-iv | cs-identifier;lady-nodette-nodington-iv;{"language": "mul"} | - | nodington-v | cs-identifier;lady-nodette-nodington-v;{"language": "mul"} | - | nodington-vi | cs-identifier;lady-nodette-nodington-vi;{"language": "mul"} | - | nodington-vii | cs-identifier;lady-nodette-nodington-vii;{"language": "mul"} | - | nodington-viii | cs-identifier;lady-nodette-nodington-viii;{"language": "mul"} | - | nodington-ix | cs-identifier;lady-nodette-nodington-ix;{"language": "mul"} | - | nodington-x | cs-identifier;lady-nodette-nodington-x;{"language": "mul"} | - - diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01-ChangeNodeAggregateName_ConstraintChecks.feature similarity index 50% rename from Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature rename to Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01-ChangeNodeAggregateName_ConstraintChecks.feature index 542dbc3fb26..444ed0a481d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01-ChangeNodeAggregateName_ConstraintChecks.feature @@ -6,7 +6,9 @@ Feature: Change node name These are the base test cases for the NodeAggregateCommandHandler to block invalid commands. Background: - Given using no content dimensions + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | And using the following node types: """yaml 'Neos.ContentRepository.Testing:Content': [] @@ -24,7 +26,7 @@ Feature: Change node name | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {"example":"source"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -32,9 +34,9 @@ Feature: Change node name | nodeTypeName | "Neos.ContentRepository:Root" | And the graph projection is fully up to date And the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | initialPropertyValues | tetheredDescendantNodeAggregateIds | - | sir-david-nodenborough | null | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | {} | {"tethered": "nodewyn-tetherton"} | - | nody-mc-nodeface | occupied | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | {} | {} | + | nodeAggregateId | nodeName | nodeTypeName | parentNodeAggregateId | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | null | Neos.ContentRepository.Testing:Document | lady-eleonode-rootford | {"tethered": "nodewyn-tetherton"} | + | nody-mc-nodeface | occupied | Neos.ContentRepository.Testing:Document | sir-david-nodenborough | {} | Scenario: Try to rename a node aggregate in a non-existing workspace When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: @@ -42,7 +44,17 @@ Feature: Change node name | workspaceName | "i-do-not-exist" | | nodeAggregateId | "sir-david-nodenborough" | | newNodeName | "new-name" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" + + Scenario: Try to rename a node aggregate in a workspace whose content stream is closed: + When the command CloseContentStream is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | newNodeName | "new-name" | + Then the last command should have thrown an exception of type "ContentStreamIsClosed" Scenario: Try to rename a non-existing node aggregate When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: @@ -65,9 +77,49 @@ Feature: Change node name | newNodeName | "new-name" | Then the last command should have thrown an exception of type "NodeAggregateIsTethered" - Scenario: Try to rename a node aggregate using an already occupied name + Scenario: Try to rename a node aggregate using an already covered name When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | newNodeName | "tethered" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to rename a node aggregate using a partially covered name + # Could happen via creation or move with the same effect + Given the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | sourceOrigin | {"example": "source"} | + | targetOrigin | {"example": "peer"} | + And the graph projection is fully up to date + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | originDimensionSpacePoint | {"example": "peer"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | parentNodeAggregateId | "sir-david-nodenborough" | + | nodeName | "esquire" | + And the graph projection is fully up to date + When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "esquire" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to rename a node aggregate using a name of a not yet existent, tethered child + Given I change the node types in content repository "default" to: + """yaml + 'Neos.ContentRepository.Testing:Content': [] + 'Neos.ContentRepository.Testing:Document': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Content' + another-tethered: + type: 'Neos.ContentRepository.Testing:Content' + """ + # We don't run structure adjustments here on purpose + When the command ChangeNodeAggregateName is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "another-tethered" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature new file mode 100644 index 00000000000..457b44cb3bf --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/02-ChangeNodeAggregateName.feature @@ -0,0 +1,135 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Change node aggregate name + + As a user of the CR I want to change the name of a node aggregate + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | general, source, spec, peer | spec->source->general, peer->general | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Node': {} + 'Neos.ContentRepository.Testing:NodeWithTetheredChildren': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:Node' + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example":"source"} + + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | originDimensionSpacePoint | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds | + | sir-david-nodenborough | Neos.ContentRepository.Testing:Node | {"example":"general"} | lady-eleonode-rootford | parent-document | {} | + | nody-mc-nodeface | Neos.ContentRepository.Testing:NodeWithTetheredChildren | {"example":"source"} | sir-david-nodenborough | document | {"tethered": "nodimus-prime"} | + | nodimus-mediocre | Neos.ContentRepository.Testing:Node | {"example":"source"} | nodimus-prime | grandchild-document | {} | + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"general"} | + And the graph projection is fully up to date + # leave spec as a virtual variant + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"peer"} | + And the graph projection is fully up to date + + Scenario: Rename a child node aggregate with descendants + When the command ChangeNodeAggregateName is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + Then I expect exactly 11 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 10 is of type "NodeAggregateNameWasChanged" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + And I expect the node aggregate "nody-mc-nodeface" to exist + And I expect this node aggregate to be named "renamed-document" + + And I expect the graph projection to consist of exactly 9 nodes + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"general"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node + + Scenario: Rename a scattered node aggregate + Given the command MoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | dimensionSpacePoint | {"example": "peer"} | + | newParentNodeAggregateId | "lady-eleonode-rootford" | + | relationDistributionStrategy | "scatter" | + And the graph projection is fully up to date + + When the command ChangeNodeAggregateName is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + Then I expect exactly 12 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 11 is of type "NodeAggregateNameWasChanged" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | newNodeName | "renamed-document" | + + And I expect the node aggregate "nody-mc-nodeface" to exist + And I expect this node aggregate to be named "renamed-document" + + And I expect the graph projection to consist of exactly 9 nodes + + When I am in workspace "live" and dimension space point {"example": "general"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"general"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"general"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to no node + + When I am in workspace "live" and dimension space point {"example": "source"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in workspace "live" and dimension space point {"example": "spec"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "parent-document/renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"source"} + Then I expect node aggregate identifier "nodimus-prime" and node path "parent-document/renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"source"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "parent-document/renamed-document/tethered/grandchild-document" to lead to node cs-identifier;nodimus-mediocre;{"example":"source"} + + When I am in workspace "live" and dimension space point {"example": "peer"} + Then I expect node aggregate identifier "nody-mc-nodeface" and node path "renamed-document" to lead to node cs-identifier;nody-mc-nodeface;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-prime" and node path "renamed-document/tethered" to lead to node cs-identifier;nodimus-prime;{"example":"peer"} + Then I expect node aggregate identifier "nodimus-mediocre" and node path "renamed-document/tethered/grandchild-document" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature index 8d25d71e03a..ff0f58bdd9a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature @@ -24,7 +24,7 @@ Feature: ForkContentStream Without Dimensions | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature index dcc5fad2036..c4a13c11db4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithDisabledNodesWithoutDimensions.feature @@ -23,7 +23,7 @@ Feature: On forking a content stream, hidden nodes should be correctly copied as | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithoutDimensions.feature index e7b8197080c..357c6406deb 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/ForkContentStreamWithoutDimensions.feature @@ -24,7 +24,7 @@ Feature: ForkContentStream Without Dimensions | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature index e8b6e164bc4..75decf3a791 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature @@ -29,7 +29,7 @@ Feature: On forking a content stream, node references should be copied as well. | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature index b8e3d750c59..ef2f1eaf151 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddDimensionShineThrough.feature @@ -72,7 +72,7 @@ Feature: Add Dimension Specialization to: { language: 'ch' } """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" + When I am in workspace "live" And I am in dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} And I expect this node to be of type "Neos.ContentRepository.Testing:Document" @@ -124,12 +124,12 @@ Feature: Add Dimension Specialization | text | "changed" | # the original content stream was untouched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} And I expect this node to have the following properties: | Key | Value | | text | "hello" | - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When I run integrity violation detection @@ -145,7 +145,7 @@ Feature: Add Dimension Specialization And the graph projection is fully up to date # ensure the node is disabled - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature index e99b20f0e24..9d47c6e453a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/AddNewProperty_NoDimensions.feature @@ -23,7 +23,7 @@ Feature: Add New Property | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -75,7 +75,7 @@ Feature: Add New Property type: 'DateTime' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_Dimensions.feature index bd52c8d1d2b..e219bf8c2c8 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_Dimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_Dimensions.feature @@ -29,7 +29,7 @@ Feature: Change Property Value across dimensions; and test DimensionSpacePoints | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -79,19 +79,19 @@ Feature: Change Property Value across dimensions; and test DimensionSpacePoints # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} And I expect this node to have the following properties: | Key | Value | | text | "Original text" | - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} And I expect this node to have the following properties: | Key | Value | | text | "Original text" | - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "en"} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_NoDimensions.feature index 2c3ae2d9969..abfb634b4ec 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/ChangePropertyValue_NoDimensions.feature @@ -24,7 +24,7 @@ Feature: Change Property | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -59,7 +59,7 @@ Feature: Change Property newSerializedValue: 'fixed value' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_NodeName_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_NodeName_NoDimensions.feature index 0958d6cf1f4..0c3a24f7559 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_NodeName_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_NodeName_NoDimensions.feature @@ -23,7 +23,7 @@ Feature: Filter - Node Name | workspaceTitle | "Live" | | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the graph projection is fully up to date And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -81,7 +81,7 @@ Feature: Filter - Node Name newSerializedValue: 'fixed value' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "na-name1" to lead to node cs-identifier;na-name1;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyNotEmpty_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyNotEmpty_NoDimensions.feature index 103e8290d27..87c80dbd804 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyNotEmpty_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyNotEmpty_NoDimensions.feature @@ -24,7 +24,7 @@ Feature: Filter - Property not empty | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -91,7 +91,7 @@ Feature: Filter - Property not empty newSerializedValue: 'fixed value' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "na-name1" to lead to node cs-identifier;na-name1;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyValue_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyValue_NoDimensions.feature index b3e314fab70..a3469927d45 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyValue_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/Filter_PropertyValue_NoDimensions.feature @@ -23,7 +23,7 @@ Feature: Filter - Property Value | workspaceTitle | "Live" | | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the graph projection is fully up to date And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -92,7 +92,7 @@ Feature: Filter - Property Value newSerializedValue: 'fixed value' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "na-name1" to lead to node cs-identifier;na-name1;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature index c3a56b5605b..cba219a40f8 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/MoveDimensionSpacePoint.feature @@ -35,7 +35,7 @@ Feature: Move dimension space point | workspaceTitle | "Live" | | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the graph projection is fully up to date And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -96,7 +96,7 @@ Feature: Move dimension space point And the graph projection is fully up to date # ensure the node is disabled - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} @@ -120,7 +120,7 @@ Feature: Move dimension space point """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node When VisibilityConstraints are set to "withoutRestrictions" Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_Dimensions.feature index b4394b7679f..dd77fc64c91 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_Dimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_Dimensions.feature @@ -30,7 +30,7 @@ Feature: Adjust node types with a node migration | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -75,11 +75,11 @@ Feature: Adjust node types with a node migration newType: 'Neos.ContentRepository.Testing:OtherDocument' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} And I expect this node to be of type "Neos.ContentRepository.Testing:Document" # ... also in the fallback dimension - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} And I expect this node to be of type "Neos.ContentRepository.Testing:Document" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_NoDimensions.feature index afd94e15f65..bd8bc50105e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/NodeTypeAdjustment_NoDimensions.feature @@ -28,7 +28,7 @@ Feature: Adjust node types with a node migration | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -73,7 +73,7 @@ Feature: Adjust node types with a node migration newType: 'Neos.ContentRepository.Testing:OtherDocument' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to be of type "Neos.ContentRepository.Testing:Document" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveNodes_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveNodes_Dimensions.feature index 9498e574b49..2c03aa7d782 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveNodes_Dimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveNodes_Dimensions.feature @@ -27,7 +27,7 @@ Feature: Remove Nodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -72,13 +72,13 @@ Feature: Remove Nodes type: 'RemoveNode' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "en"} # the node was removed inside the new content stream, but only in de and gsw (virtual specialization) @@ -118,13 +118,13 @@ Feature: Remove Nodes """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "en"} # the node was removed inside the new content stream, but only in de and gsw, since it is a specialization @@ -188,13 +188,13 @@ Feature: Remove Nodes """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "en"} # the node was removed inside the new content stream, but only in gsw @@ -229,13 +229,13 @@ Feature: Remove Nodes """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "en"} # the node was removed inside the new content stream, but only in gsw @@ -275,13 +275,13 @@ Feature: Remove Nodes """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "en"} # the node was removed inside the new content stream, but only in gsw @@ -313,13 +313,13 @@ Feature: Remove Nodes """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "gsw"} + When I am in workspace "live" and dimension space point {"language": "gsw"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "de"} - When I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + When I am in workspace "live" and dimension space point {"language": "en"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language": "en"} # the node was removed inside the new content stream, but only in gsw diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveProperty_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveProperty_NoDimensions.feature index 399bcf84a1d..ef9a9cbe68f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveProperty_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RemoveProperty_NoDimensions.feature @@ -24,7 +24,7 @@ Feature: Remove Property | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -58,7 +58,7 @@ Feature: Remove Property property: 'text' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameNodeAggregate_Dimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameNodeAggregate_Dimensions.feature index 2842fc39a2d..b222db6b1f1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameNodeAggregate_Dimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameNodeAggregate_Dimensions.feature @@ -27,7 +27,7 @@ Feature: Rename Node Aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -72,10 +72,10 @@ Feature: Rename Node Aggregate # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + When I am in workspace "live" and dimension space point {"language": "de"} Then I expect the node "sir-david-nodenborough" to have the name "foo" - When I am in the active content stream of workspace "live" and dimension space point {"language": "ch"} + When I am in workspace "live" and dimension space point {"language": "ch"} Then I expect the node "sir-david-nodenborough" to have the name "foo" # the node was changed inside the new content stream, across all dimensions diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameProperty_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameProperty_NoDimensions.feature index b63569bd20f..1856aacb601 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameProperty_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/RenameProperty_NoDimensions.feature @@ -24,7 +24,7 @@ Feature: Rename Property | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -72,7 +72,7 @@ Feature: Rename Property to: 'newText' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/StripTagsOnProperty_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/StripTagsOnProperty_NoDimensions.feature index 987cbc19287..7ebff06074d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/StripTagsOnProperty_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/EventSourced/Migration/StripTagsOnProperty_NoDimensions.feature @@ -24,7 +24,7 @@ Feature: Strip Tags on Property | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -58,7 +58,7 @@ Feature: Strip Tags on Property property: 'text' """ # the original content stream has not been touched - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature index 28634b325b5..32177b21410 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeCopying/CopyNode_NoDimensions.feature @@ -16,7 +16,7 @@ Feature: Copy nodes (without dimensions) | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -54,7 +54,7 @@ Feature: Copy nodes (without dimensions) And the graph projection is fully up to date Scenario: Copy - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} # node to copy (currentNode): "sir-nodeward-nodington-iii" Then I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} When the command CopyNodesRecursively is executed, copying the current node aggregate with payload: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePropertyConversion/NodePropertyConversion.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePropertyConversion/NodePropertyConversion.feature index 77aa2b318ad..cdab8e20bfd 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePropertyConversion/NodePropertyConversion.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodePropertyConversion/NodePropertyConversion.feature @@ -17,7 +17,7 @@ Feature: Node Property Conversion | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -35,7 +35,7 @@ Feature: Node Property Conversion And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | @@ -58,7 +58,7 @@ Feature: Node Property Conversion | propertyValues | {"dateProperty": "Date:1997-07-19T19:20:30+05:00"} | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature index f6355585dc0..7ce7deadf6a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateAfterDisabling.feature @@ -24,7 +24,7 @@ Feature: Disable a node aggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -65,7 +65,7 @@ Feature: Disable a node aggregate | nodeName | "child-document" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 5 nodes And I expect a node identified by cs-identifier;lady-eleonode-rootford;{} to exist in the content graph And I expect a node identified by cs-identifier;preceding-nodenborough;{} to exist in the content graph @@ -76,7 +76,7 @@ Feature: Disable a node aggregate And I expect the node aggregate "sir-david-nodenborough" to exist And I expect this node aggregate to disable dimension space points [] - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And VisibilityConstraints are set to "frontend" Then the subtree for node aggregate "lady-eleonode-rootford" with node types "" and 2 levels deep should be: | Level | nodeAggregateId | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature index 9f44a74c9c0..ae829e65373 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRemoval/RemoveNodeAggregateWithDimensions.feature @@ -20,7 +20,7 @@ Feature: Remove NodeAggregate | workspaceDescription | "The live workspace" | | newContentStreamId | "live-cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And I am in workspace "live" and dimension space point {"language":"de"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-nodesworth" | @@ -65,11 +65,11 @@ Feature: Remove NodeAggregate Then I expect the graph projection to consist of exactly 1 node And I expect a node identified by live-cs-identifier;lady-eleonode-nodesworth;{} to exist in the content graph - When I am in content stream "live-cs-identifier" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect the subgraph projection to consist of exactly 1 nodes And I expect node aggregate identifier "lady-eleonode-nodesworth" to lead to node live-cs-identifier;lady-eleonode-nodesworth;{} - When I am in content stream "live-cs-identifier" and dimension space point {"language":"gsw"} + When I am in workspace "live" and dimension space point {"language":"gsw"} Then I expect the subgraph projection to consist of exactly 1 nodes And I expect node aggregate identifier "lady-eleonode-nodesworth" to lead to node live-cs-identifier;lady-eleonode-nodesworth;{} @@ -82,7 +82,7 @@ Feature: Remove NodeAggregate | baseWorkspaceName | "live" | | newContentStreamId | "user-cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "user-test" + And I am in workspace "user-test" When the command RemoveNodeAggregate is executed with payload: | Key | Value | @@ -97,22 +97,22 @@ Feature: Remove NodeAggregate And I expect a node identified by live-cs-identifier;nody-mc-nodeface;{"language":"gsw"} to exist in the content graph And I expect a node identified by live-cs-identifier;nodimus-prime;{"language":"de"} to exist in the content graph - When I am in content stream "user-cs-identifier" and dimension space point {"language":"de"} + When I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the subgraph projection to consist of exactly 1 nodes And I expect node aggregate identifier "lady-eleonode-nodesworth" to lead to node user-cs-identifier;lady-eleonode-nodesworth;{} - When I am in content stream "user-cs-identifier" and dimension space point {"language":"gsw"} + When I am in workspace "user-test" and dimension space point {"language":"gsw"} Then I expect the subgraph projection to consist of exactly 1 nodes And I expect node aggregate identifier "lady-eleonode-nodesworth" to lead to node user-cs-identifier;lady-eleonode-nodesworth;{} # ensure LIVE ContentStream is untouched - When I am in content stream "live-cs-identifier" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect the subgraph projection to consist of exactly 3 nodes And I expect node aggregate identifier "lady-eleonode-nodesworth" to lead to node live-cs-identifier;lady-eleonode-nodesworth;{} And I expect node aggregate identifier "nody-mc-nodeface" and node path "document" to lead to node live-cs-identifier;nody-mc-nodeface;{"language":"de"} And I expect node aggregate identifier "nodimus-prime" and node path "document/child-document" to lead to node live-cs-identifier;nodimus-prime;{"language":"de"} - When I am in content stream "live-cs-identifier" and dimension space point {"language":"gsw"} + When I am in workspace "live" and dimension space point {"language":"gsw"} Then I expect the subgraph projection to consist of exactly 3 nodes And I expect node aggregate identifier "lady-eleonode-nodesworth" to lead to node live-cs-identifier;lady-eleonode-nodesworth;{} And I expect node aggregate identifier "nody-mc-nodeface" and node path "document" to lead to node live-cs-identifier;nody-mc-nodeface;{"language":"gsw"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature index e1760070c4e..e69de29bb2d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature @@ -1,84 +0,0 @@ -@contentrepository @adapters=DoctrineDBAL -Feature: Change node name - - As a user of the CR I want to change the name of a hierarchical relation between two nodes (e.g. in taxonomies) - - Background: - Given using no content dimensions - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Content': [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} - - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - - Scenario: Change node name of content node - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Content" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "dog" | - | nodeAggregateClassification | "regular" | - - And the graph projection is fully up to date - When the command "ChangeNodeAggregateName" is executed with payload: - | Key | Value | - | workspaceName | "live" | - | nodeAggregateId | "nody-mc-nodeface" | - | newNodeName | "cat" | - - Then I expect exactly 4 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 3 is of type "NodeAggregateNameWasChanged" with payload: - | Key | Expected | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | newNodeName | "cat" | - - Scenario: Change node name actually updates projection - Given the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Content" | - | originDimensionSpacePoint | {} | - | coveredDimensionSpacePoints | [{}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "dog" | - | nodeAggregateClassification | "regular" | - And the graph projection is fully up to date - # we read the node initially, to ensure it is filled in the cache (to check whether cache clearing actually works) - When I am in the active content stream of workspace "live" and dimension space point {} - Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} - Then I expect this node to have the following child nodes: - | Name | NodeDiscriminator | - | dog | cs-identifier;nody-mc-nodeface;{} | - - When the command "ChangeNodeAggregateName" is executed with payload: - | Key | Value | - | workspaceName | "live" | - | nodeAggregateId | "nody-mc-nodeface" | - | newNodeName | "cat" | - And the graph projection is fully up to date - - Then I expect node aggregate identifier "lady-eleonode-rootford" to lead to node cs-identifier;lady-eleonode-rootford;{} - Then I expect this node to have the following child nodes: - | Name | NodeDiscriminator | - | cat | cs-identifier;nody-mc-nodeface;{} | - diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature index 597ab0036b9..244892ae790 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature @@ -50,7 +50,7 @@ Feature: Change node aggregate type - basic error cases | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -74,7 +74,7 @@ Feature: Change node aggregate type - basic error cases | nodeAggregateId | "sir-david-nodenborough" | | newNodeTypeName | "Neos.ContentRepository.Testing:ChildOfNodeTypeA" | | strategy | "happypath" | - Then the last command should have thrown an exception of type "ContentStreamDoesNotExistYet" + Then the last command should have thrown an exception of type "WorkspaceDoesNotExist" Scenario: Try to change the type on a non-existing node aggregate When the command ChangeNodeAggregateType was published with payload and exceptions are caught: @@ -135,7 +135,7 @@ Feature: Change node aggregate type - basic error cases | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeConstraintException" - Scenario: Try to change the node type of an auto created child node to anything other than defined: + Scenario: Try to change the node type of an tethered child node: When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: | Key | Value | | nodeAggregateId | "parent2-na" | @@ -152,4 +152,4 @@ Feature: Change node aggregate type - basic error cases | nodeAggregateId | "nody-mc-nodeface" | | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | | strategy | "happypath" | - Then the last command should have thrown an exception of type "NodeConstraintException" + Then the last command should have thrown an exception of type "NodeAggregateIsTethered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_DeleteStrategy.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_DeleteStrategy.feature index c2bfccea819..26f82f07947 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_DeleteStrategy.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_DeleteStrategy.feature @@ -66,7 +66,7 @@ Feature: Change node aggregate type - behavior of DELETE strategy | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -101,13 +101,13 @@ Feature: Change node aggregate type - behavior of DELETE strategy And the graph projection is fully up to date # the type has changed - When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{"language":"de"} And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeB" # the child nodes have been removed Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"language":"gsw"} + 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 @@ -138,19 +138,19 @@ Feature: Change node aggregate type - behavior of DELETE strategy And the graph projection is fully up to date # the type has changed - When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "parent2-na" to lead to node cs-identifier;parent2-na;{"language":"de"} And I expect this node to be of type "Neos.ContentRepository.Testing:ParentNodeTypeB" # the child nodes still exist - When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "autocreated-child" to lead to node cs-identifier;autocreated-child;{"language":"de"} - When I am in the active content stream of workspace "live" and dimension space point {"language":"gsw"} + 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"} # the grandchild nodes have been removed Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node - When I am in the active content stream of workspace "live" and dimension space point {"language":"gsw"} + 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 @@ -181,11 +181,11 @@ Feature: Change node aggregate type - behavior of DELETE strategy And the graph projection is fully up to date # the type has changed - When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "nodea-identifier-de" to lead to node cs-identifier;nodea-identifier-de;{"language":"de"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" - When I am in the active content stream of workspace "live" and dimension space point {"language":"gsw"} + 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"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" And I expect this node to have the following child nodes: @@ -220,7 +220,7 @@ Feature: Change node aggregate type - behavior of DELETE strategy And the graph projection is fully up to date # the type has changed - When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "nodea-identifier-de" to lead to node cs-identifier;nodea-identifier-de;{"language":"de"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_HappyPathStrategy.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_HappyPathStrategy.feature index cf130d3c988..07a3e086e20 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_HappyPathStrategy.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_HappyPathStrategy.feature @@ -59,7 +59,7 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | @@ -147,11 +147,11 @@ Feature: Change node aggregate type - behavior of HAPPYPATH strategy And the graph projection is fully up to date # the type has changed - When I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + When I am in workspace "live" and dimension space point {"language":"de"} Then I expect node aggregate identifier "nodea-identifier-de" to lead to node cs-identifier;nodea-identifier-de;{"language":"de"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" - When I am in the active content stream of workspace "live" and dimension space point {"language":"gsw"} + 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"} And I expect this node to be of type "Neos.ContentRepository.Testing:NodeTypeB" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature index db876fac357..2af6ab93c19 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/AncestorNodes.feature @@ -51,7 +51,7 @@ Feature: Find and count nodes using the findAncestorNodes and countAncestorNodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature index 565e2556cc6..c842280f9be 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature @@ -65,7 +65,7 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ClosestNode.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ClosestNode.feature index c5816b0d300..22b1ef2eb29 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ClosestNode.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ClosestNode.feature @@ -51,7 +51,7 @@ Feature: Find nodes using the findClosestNode query | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/CountNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/CountNodes.feature index 33d104675a4..4f2530014ce 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/CountNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/CountNodes.feature @@ -53,7 +53,7 @@ Feature: Find nodes using the countNodes query | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature index 34847488825..d3dd853dcb1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/DescendantNodes.feature @@ -65,7 +65,7 @@ Feature: Find and count nodes using the findDescendantNodes and countDescendantN | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature index b61dffbc52f..3891287536c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeById.feature @@ -64,7 +64,7 @@ Feature: Find nodes using the findNodeById query | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature index c895ac33025..0b5fd8d9ab6 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPath.feature @@ -67,7 +67,7 @@ Feature: Find nodes using the findNodeByPath query | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature index bb98926abc2..93e2a7d9d6b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindNodeByPathAsNodeName.feature @@ -64,7 +64,7 @@ Feature: Find nodes using the findNodeByPath query with node name as path argume | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature index 68bc44ef005..c830b3ceaef 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindParentNode.feature @@ -64,7 +64,7 @@ Feature: Find nodes using the findParentNodes query | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindRootNodeByType.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindRootNodeByType.feature index c56e42a84a0..e74a0ba467a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindRootNodeByType.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindRootNodeByType.feature @@ -24,7 +24,7 @@ Feature: Find root nodes by type | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindSubtree.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindSubtree.feature index f65d2abd4ce..1df9aaf8820 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindSubtree.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/FindSubtree.feature @@ -53,7 +53,7 @@ Feature: Find nodes using the findSubtree query | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature index 13a8e586444..488bb7f4bc4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/References.feature @@ -74,7 +74,7 @@ Feature: Find and count references and their target nodes using the findReferenc | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/RetrieveNodePath.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/RetrieveNodePath.feature index d4bf8631090..e90515a436b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/RetrieveNodePath.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/RetrieveNodePath.feature @@ -54,7 +54,7 @@ Feature: Find nodes using the retrieveNodePath query | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature index a94e992bd83..bf88395dede 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/SiblingNodes.feature @@ -64,7 +64,7 @@ Feature: Find sibling nodes using the findPrecedingSiblingNodes and findSucceedi | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index ed4672f8e85..4487f84325f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -75,7 +75,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | newContentStreamId | "cs-user" | | workspaceOwner | "some-user" | And the graph projection is fully up to date - And I am in the active content stream of workspace "user-test" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -103,12 +103,12 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | nodeAggregateId | "a" | | propertyValues | {"text": "Changed"} | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | @@ -117,31 +117,79 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command "ChangeNodeAggregateName" is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | newNodeName | "a-renamed" | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + Scenario: NodeAggregateNameWasChanged events update last modified timestamps only in the user workspace + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + And the graph projection is fully up to date + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review" | + And the graph projection is fully up to date + And the current date and time is "2023-03-16T14:00:00+01:00" + And the command "ChangeNodeAggregateName" is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | nodeAggregateId | "a" | + | newNodeName | "a-renamed" | + And the graph projection is fully up to date + + And I am in workspace "user-test" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | + + And I am in workspace "user-test" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | 2023-03-16 14:00:00 | 2023-03-16 14:00:00 | + + When I am in workspace "review" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | | | + + When I am in workspace "review" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | | | + + When I am in workspace "live" and dimension space point {"language":"de"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:00:00 | | | + + When I am in workspace "live" and dimension space point {"language":"ch"} + Then I expect the node "a" to have the following timestamps: + | created | originalCreated | lastModified | originalLastModified | + | 2023-03-16 13:00:00 | 2023-03-16 12:30:00 | | | + Scenario: NodeReferencesWereSet events update last modified timestamps When the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeReferences is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | sourceOriginDimensionSpacePoint | {"language": "ch"} | | sourceNodeAggregateId | "a" | | referenceName | "ref" | | references | [{"target": "b"}] | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | @@ -149,7 +197,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | @@ -161,17 +209,17 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command ChangeNodeAggregateType was published with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | newNodeTypeName | "Neos.ContentRepository.Testing:SpecialPage" | | strategy | "happypath" | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | @@ -184,12 +232,12 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | sourceOrigin | {"language":"de"} | | targetOrigin | {"language":"en"} | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "home" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"en"} + When I am in workspace "user-test" and dimension space point {"language":"en"} Then I expect the node "home" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | | | @@ -202,12 +250,12 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | sourceOrigin | {"language":"de"} | | targetOrigin | {"language":"mul"} | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "home" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"mul"} + When I am in workspace "user-test" and dimension space point {"language":"mul"} Then I expect the node "home" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | | | @@ -217,18 +265,18 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command MoveNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | dimensionSpacePoint | {"language": "ch"} | | relationDistributionStrategy | "gatherSpecializations" | | nodeAggregateId | "a" | | newParentNodeAggregateId | "b" | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | @@ -239,12 +287,12 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | @@ -253,18 +301,18 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command DisableNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | coveredDimensionSpacePoint | {"language": "ch"} | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} And VisibilityConstraints are set to "withoutRestrictions" Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | @@ -272,17 +320,17 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T14:00:00+01:00" And the command EnableNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | coveredDimensionSpacePoint | {"language": "ch"} | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | And the graph projection is fully up to date - And I am in content stream "cs-user" and dimension space point {"language":"de"} + And I am in workspace "user-test" and dimension space point {"language":"de"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | - When I am in content stream "cs-user" and dimension space point {"language":"ch"} + When I am in workspace "user-test" and dimension space point {"language":"ch"} Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | | | @@ -292,7 +340,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command SetNodeProperties is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | propertyValues | {"text": "Changed"} | And I execute the findNodeById query for node aggregate id "non-existing" I expect no node to be returned @@ -303,15 +351,15 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | workspaceName | "user-test" | And the graph projection is fully up to date - And I am in content stream "cs-user" + And I am in workspace "user-test" Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 13:00:00 | And I expect the node "b" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | - | 2023-03-16 12:00:00 | 2023-03-16 12:00:00 | | | + | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | | | - And I am in content stream "cs-review" + And I am in workspace "review" Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 14:00:00 | 2023-03-16 12:00:00 | 2023-03-16 14:00:00 | 2023-03-16 13:00:00 | @@ -324,7 +372,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | Key | Value | | workspaceName | "review" | And the graph projection is fully up to date - And I am in content stream "cs-live" + And I am in workspace "live" Then I expect the node "a" to have the following timestamps: | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 15:00:00 | 2023-03-16 12:00:00 | 2023-03-16 15:00:00 | 2023-03-16 13:00:00 | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature index 9dd34c4676b..d9c1c1604fc 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature @@ -52,10 +52,11 @@ Feature: Run projection integrity violation detection regarding root connection | nodeAggregateClassification | "regular" | And the graph projection is fully up to date And the event NodeAggregateWasMoved was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeMoveMappings | [{"movedNodeOrigin":{"language":"de"},"newLocations":[{"coveredDimensionSpacePoint": {"language":"de"},"newParent":{"nodeAggregateId":"nody-mc-nodeface","originDimensionSpacePoint":{"language":"de"}}}, {"coveredDimensionSpacePoint": {"language":"gsw"},"newParent":{"nodeAggregateId":"nody-mc-nodeface","originDimensionSpacePoint":{"language":"de"}}}]}] | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "sir-david-nodenborough" | + | newParentNodeAggregateId | "nody-mc-nodeface" | + | succeedingSiblingsForCoverage | [{"dimensionSpacePoint":{"language":"de"},"nodeAggregateId": null},{"dimensionSpacePoint":{"language":"gsw"},"nodeAggregateId": null}] | And the graph projection is fully up to date And I run integrity violation detection # one error per subgraph diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesCoverTheirOrigin.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesCoverTheirOrigin.feature index f7c96024ca4..adea8b499c5 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesCoverTheirOrigin.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/AllNodesCoverTheirOrigin.feature @@ -20,7 +20,7 @@ Feature: Run projection integrity violation detection to find nodes that do not | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregateIdentifiersAreUniquePerSubgraph.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregateIdentifiersAreUniquePerSubgraph.feature index cb0fa7092c4..1d25c75ec68 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregateIdentifiersAreUniquePerSubgraph.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregateIdentifiersAreUniquePerSubgraph.feature @@ -20,7 +20,7 @@ Feature: Create two nodes with the same node aggregate identifier in the same su | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyClassifiedPerContentStream.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyClassifiedPerContentStream.feature index 79752e4371e..607bcde6626 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyClassifiedPerContentStream.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyClassifiedPerContentStream.feature @@ -20,7 +20,7 @@ Feature: Run projection integrity violation detection regarding node aggregate c | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyTypedPerContentStream.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyTypedPerContentStream.feature index 5ddfd4d3e0a..af45832048d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyTypedPerContentStream.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/NodeAggregatesAreConsistentlyTypedPerContentStream.feature @@ -22,7 +22,7 @@ Feature: Run projection integrity violation detection regarding node aggregate t | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature index f8101ef42c1..56093467cf7 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ProjectionIntegrityViolationDetection/ReferenceIntegrityIsProvided.feature @@ -20,7 +20,7 @@ Feature: Run integrity violation detection regarding reference relations | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} + And 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" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/RootNodeAggregateDimensionUpdates/UpdateRootNodeAggregateDimensions_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/RootNodeAggregateDimensionUpdates/UpdateRootNodeAggregateDimensions_WithDimensions.feature index 3382f6122a5..b18e736a678 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/RootNodeAggregateDimensionUpdates/UpdateRootNodeAggregateDimensions_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/RootNodeAggregateDimensionUpdates/UpdateRootNodeAggregateDimensions_WithDimensions.feature @@ -20,7 +20,7 @@ Feature: Update Root Node aggregate dimensions | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature index b059a1cf922..21d602c5891 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DimensionMismatch.feature @@ -25,7 +25,7 @@ Feature: Dimension mismatch | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language": "en"} + And I am in workspace "live" and dimension space point {"language": "en"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNode.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNode.feature index d662515984e..8272d781e12 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNode.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNode.feature @@ -31,7 +31,7 @@ Feature: Remove disallowed Child Nodes and grandchild nodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -85,7 +85,7 @@ Feature: Remove disallowed Child Nodes and grandchild nodes When I adjust the node structure for node type "Neos.ContentRepository.Testing:Document" Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:Document" - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "sir-david-nodenborough" to lead to no node @@ -121,7 +121,7 @@ Feature: Remove disallowed Child Nodes and grandchild nodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -184,6 +184,6 @@ Feature: Remove disallowed Child Nodes and grandchild nodes When I adjust the node structure for node type "Neos.ContentRepository.Testing:SubDocument" Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:SubDocument" - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "subdoc" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNodesAndTetheredNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNodesAndTetheredNodes.feature index 9b819a633ee..8fcc7e9a361 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNodesAndTetheredNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/DisallowedChildNodesAndTetheredNodes.feature @@ -31,7 +31,7 @@ Feature: Remove disallowed Child Nodes and grandchild nodes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature index ce752a421b1..24952a84ece 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/Properties.feature @@ -25,7 +25,7 @@ Feature: Properties | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -57,7 +57,7 @@ Feature: Properties When I adjust the node structure for node type "Neos.ContentRepository.Testing:Document" Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:Document" - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have no properties @@ -79,7 +79,7 @@ Feature: Properties When I adjust the node structure for node type "Neos.ContentRepository.Testing:Document" Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:Document" - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -131,6 +131,6 @@ Feature: Properties When I adjust the node structure for node type "Neos.ContentRepository.Testing:Document" Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:Document" - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have no properties diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodes.feature index e5ef4bb624b..de6d9e0673c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodes.feature @@ -37,7 +37,7 @@ Feature: Tethered Nodes integrity violations | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -144,7 +144,7 @@ Feature: Tethered Nodes integrity violations When I adjust the node structure for node type "Neos.ContentRepository:Root" Then I expect no needed structure adjustments for type "Neos.ContentRepository:Root" - When I am in the active content stream of workspace "live" and dimension space point {"market":"CH", "language":"gsw"} + When I am in workspace "live" and dimension space point {"market":"CH", "language":"gsw"} And I get the node at path "document/some-new-child" And I expect this node to have the following properties: | Key | Value | @@ -203,7 +203,7 @@ Feature: Tethered Nodes integrity violations Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:Document" When I adjust the node structure for node type "Neos.ContentRepository:Root" Then I expect no needed structure adjustments for type "Neos.ContentRepository:Root" - When I am in the active content stream of workspace "live" and dimension space point {"market":"CH", "language":"gsw"} + When I am in workspace "live" and dimension space point {"market":"CH", "language":"gsw"} Then I expect node aggregate identifier "nodewyn-tetherton" to lead to no node Then I expect node aggregate identifier "nodimer-tetherton" to lead to no node And I expect path "tethered-node" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature index 72bc102776d..2e6b41fe1ce 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/TetheredNodesReordering.feature @@ -26,7 +26,7 @@ Feature: Tethered Nodes Reordering Structure changes | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -71,7 +71,7 @@ Feature: Tethered Nodes Reordering Structure changes When I adjust the node structure for node type "Neos.ContentRepository.Testing:Document" Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:Document" - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And I get the node at path "document/tethered-node" And I expect this node to have the following preceding siblings: | NodeDiscriminator | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/UnknownNodeType.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/UnknownNodeType.feature index 6bcb3ca90ff..5006a854579 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/UnknownNodeType.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/StructureAdjustment/UnknownNodeType.feature @@ -18,7 +18,7 @@ Feature: Unknown node types | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -47,6 +47,6 @@ Feature: Unknown node types When I adjust the node structure for node type "Neos.ContentRepository.Testing:Document" Then I expect no needed structure adjustments for type "Neos.ContentRepository.Testing:Document" - When I am in the active content stream of workspace "live" and dimension space point {"market":"CH", "language":"gsw"} + When I am in workspace "live" and dimension space point {"market":"CH", "language":"gsw"} And I expect node aggregate identifier "sir-david-nodenborough" to lead to no node diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithDimensions.feature index 6da6d7e9020..4b80d2f1960 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithDimensions.feature @@ -23,7 +23,7 @@ Feature: Tag subtree with dimensions | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "root" | @@ -180,7 +180,7 @@ Feature: Tag subtree with dimensions | sourceOrigin | {"language":"de"} | | targetOrigin | {"language":"gsw"} | And the graph projection is fully up to date - And I am in the active content stream of workspace "user-ws" and dimension space point {"language":"gsw"} + And I am in workspace "user-ws" and dimension space point {"language":"gsw"} And I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: """ a (tag1*) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature index c88e62f53b3..5273addf313 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature @@ -21,7 +21,7 @@ Feature: Tag subtree without dimensions | workspaceDescription | "The live workspace" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "root" | @@ -108,10 +108,10 @@ Feature: Tag subtree without dimensions | tag | "tag1" | When the graph projection is fully up to date - And I am in content stream "cs-identifier" + And I am in workspace "live" Then I expect the graph projection to consist of exactly 12 nodes - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect the node with aggregate identifier "a1" to be explicitly tagged "tag1" Then I expect the node with aggregate identifier "a1a" to inherit the tag "tag1" Then I expect the node with aggregate identifier "a1a1" to inherit the tag "tag1" @@ -173,11 +173,11 @@ Feature: Tag subtree without dimensions """ b (tag2*) b1 (tag3*,tag2) - a1a (tag4*,tag3,tag2) - a1a1 (tag4*,tag1*,tag3,tag2) - a1a1a (tag4*,tag3,tag2) - a1a1b (tag4*,tag3,tag2) - a1a2 (tag4*,tag3,tag2) + a1a (tag4*,tag2,tag3) + a1a1 (tag1*,tag2,tag3,tag4) + a1a1a (tag1,tag2,tag3,tag4) + a1a1b (tag1,tag2,tag3,tag4) + a1a2 (tag2,tag3,tag4) """ When the command CreateNodeAggregateWithNode is executed with payload: @@ -189,12 +189,12 @@ Feature: Tag subtree without dimensions """ b (tag2*) b1 (tag3*,tag2) - a1a (tag4*,tag3,tag2) - a1a1 (tag4*,tag1*,tag3,tag2) - a1a1a (tag4*,tag3,tag2) - a1a1b (tag4*,tag3,tag2) - a1a2 (tag4*,tag3,tag2) - a1a3 (tag4,tag3,tag2) + a1a (tag4*,tag2,tag3) + a1a1 (tag1*,tag2,tag3,tag4) + a1a1a (tag1,tag2,tag3,tag4) + a1a1b (tag1,tag2,tag3,tag4) + a1a2 (tag2,tag3,tag4) + a1a3 (tag2,tag3,tag4) """ When the command UntagSubtree is executed with payload: @@ -206,10 +206,10 @@ Feature: Tag subtree without dimensions """ b (tag2*) b1 (tag3*,tag2) - a1a (tag3,tag2) - a1a1 (tag4*,tag1*,tag3,tag2) - a1a1a (tag4*,tag3,tag2) - a1a1b (tag4*,tag3,tag2) - a1a2 (tag4*,tag3,tag2) - a1a3 (tag3,tag2) + a1a (tag2,tag3) + a1a1 (tag1*,tag2,tag3) + a1a1a (tag1,tag2,tag3) + a1a1b (tag1,tag2,tag3) + a1a2 (tag2,tag3) + a1a3 (tag2,tag3) """ diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature index 271e9440ced..3614ab4af0f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/01-ConstraintChecks.feature @@ -30,7 +30,7 @@ Feature: Workspace discarding - complex chained functionality | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + And 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" | @@ -84,5 +84,5 @@ Feature: Workspace discarding - complex chained functionality | workspaceName | "user-ws" | | newContentStreamId | "user-cs-id-yet-again-rebased" | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-ws" and dimension space point {"language": "de"} + When I am in workspace "user-ws" and dimension space point {"language": "de"} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-id-yet-again-rebased;nody-mc-nodeface;{"language": "de"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/02-BasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/02-BasicFeatures.feature index 7698343042a..b79d8cc1789 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/02-BasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W10-IndividualNodeDiscarding/02-BasicFeatures.feature @@ -30,7 +30,7 @@ Feature: Discard individual nodes (basics) | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -108,7 +108,7 @@ Feature: Discard individual nodes (basics) And the graph projection is fully up to date - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-new;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -130,7 +130,7 @@ Feature: Discard individual nodes (basics) | newContentStreamId | "user-cs-identifier-new" | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-new;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -152,7 +152,7 @@ Feature: Discard individual nodes (basics) | newContentStreamId | "user-cs-identifier-new" | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-new;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -175,7 +175,7 @@ Feature: Discard individual nodes (basics) And the graph projection is fully up to date # live WS does not change because of a discard - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature index cd071f56e12..3460bc39efd 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/02-RebasingWithAutoCreatedNodes.feature @@ -33,7 +33,7 @@ Feature: Rebasing auto-created nodes works | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the graph projection is fully up to date And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | @@ -59,7 +59,7 @@ Feature: Rebasing auto-created nodes works | originDimensionSpacePoint | {} | | parentNodeAggregateId | "lady-eleonode-rootford" | And the graph projection is fully up to date - And I am in content stream "user-cs-identifier" and dimension space point {} + And I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-identifier;nody-mc-nodeface;{} When I get the node at path "mcnodeface/foo" And I expect this node to be a child of node user-cs-identifier;nody-mc-nodeface;{} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature index f47658abbf1..6d656e26dcd 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/03-RebasingWithConflictingChanges.feature @@ -23,7 +23,7 @@ Feature: Workspace rebasing - conflicting changes | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W7-WorkspacePublication/02-PublishWorkspace.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W7-WorkspacePublication/02-PublishWorkspace.feature index ce52bbcb253..eeeec62bf8c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W7-WorkspacePublication/02-PublishWorkspace.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W7-WorkspacePublication/02-PublishWorkspace.feature @@ -23,7 +23,7 @@ Feature: Workspace based content publishing | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -91,13 +91,13 @@ Feature: Workspace based content publishing | propertyValues | {"text": "Modified"} | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | | text | "Original" | - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | @@ -109,7 +109,7 @@ Feature: Workspace based content publishing | workspaceName | "user-test" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | @@ -151,7 +151,7 @@ Feature: Workspace based content publishing | workspaceName | "user-test" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: @@ -174,7 +174,7 @@ Feature: Workspace based content publishing | Key | Value | | workspaceName | "user-test" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} When the command SetNodeProperties is executed with payload: | Key | Value | @@ -191,7 +191,7 @@ Feature: Workspace based content publishing | workspaceName | "user-test" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature index 90504e16e75..327caf1fa7e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/01-ConstraintChecks.feature @@ -30,7 +30,7 @@ Feature: Workspace publication - complex chained functionality | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language": "de"} + And 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" | @@ -76,5 +76,5 @@ Feature: Workspace publication - complex chained functionality | workspaceName | "user-ws" | | newContentStreamId | "user-cs-id-yet-again-rebased" | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-ws" and dimension space point {"language": "de"} + When I am in workspace "user-ws" and dimension space point {"language": "de"} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-id-yet-again-rebased;nody-mc-nodeface;{"language": "de"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature index d7605cc24b9..68984305699 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature @@ -44,7 +44,7 @@ Feature: Individual node publication Scenario: It is possible to publish a single node; and only this one is live. # create nodes in user WS Given I am in workspace "user-test" - And I am in the active content stream of workspace "user-test" + And I am in workspace "user-test" And I am in dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds | @@ -60,7 +60,7 @@ Feature: Individual node publication | contentStreamIdForMatchingPart | "user-cs-identifier-matching" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" Then I expect a node identified by cs-identifier;sir-david-nodenborough;{} to exist in the content graph diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature index cd22380f14c..3b9b33847cd 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/03-MoreBasicFeatures.feature @@ -30,7 +30,7 @@ Feature: Publishing individual nodes (basics) | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -108,7 +108,7 @@ Feature: Publishing individual nodes (basics) | contentStreamIdForMatchingPart | "user-cs-identifier-matching" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -122,7 +122,7 @@ Feature: Publishing individual nodes (basics) | Key | Value | | image | "Modified image" | - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-remaining;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -144,7 +144,7 @@ Feature: Publishing individual nodes (basics) | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -158,7 +158,7 @@ Feature: Publishing individual nodes (basics) | Key | Value | | image | "Initial image" | - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-remaining;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -180,7 +180,7 @@ Feature: Publishing individual nodes (basics) | contentStreamIdForRemainingPart | "user-cs-identifier-remaining" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | @@ -194,7 +194,7 @@ Feature: Publishing individual nodes (basics) | Key | Value | | image | "Modified image" | - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-remaining;sir-david-nodenborough;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature index dfdb72c5b5f..514ad393268 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/04-AllFeaturePublication.feature @@ -39,7 +39,7 @@ Feature: Publishing hide/show scenario of nodes | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -83,7 +83,7 @@ Feature: Publishing hide/show scenario of nodes | baseWorkspaceName | "live" | | newContentStreamId | "user-cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "user-test" + And I am in workspace "user-test" # SETUP: hide two nodes in USER workspace Given the command DisableNodeAggregate is executed with payload: @@ -106,7 +106,7 @@ Feature: Publishing hide/show scenario of nodes | contentStreamIdForMatchingPart | "matching-cs-id" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node And I expect node aggregate identifier "nody-mc-nodeface" to lead to no node And I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} @@ -160,12 +160,12 @@ Feature: Publishing hide/show scenario of nodes | contentStreamIdForRemainingPart | "user-cs-identifier-modified" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to no node - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-modified;sir-david-nodenborough;{} And I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-identifier-modified;nody-mc-nodeface;{} And I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to node user-cs-identifier-modified;sir-nodeward-nodington-iii;{} @@ -200,13 +200,13 @@ Feature: Publishing hide/show scenario of nodes # | nodesToPublish | [{"nodeAggregateId": "sir-david-nodenborough", "contentStreamId": "user-cs-identifier", "dimensionSpacePoint": {}}] | #And the graph projection is fully up to date - # When I am in the active content stream of workspace "live" and dimension space point {} + # When I am in workspace "live" and dimension space point {} ## Then I expect the node aggregate "lady-eleonode-rootford" to have the following child nodes: # | Name | nodeAggregateId | # | text1mod | sir-david-nodenborough | # | image | sir-nodeward-nodington-iii | - # When I am in the active content stream of workspace "user-test" and dimension space point {} + # When I am in workspace "user-test" and dimension space point {} # Then I expect the node aggregate "lady-eleonode-rootford" to have the following child nodes: # | Name | nodeAggregateId | # | text1mod | sir-david-nodenborough | @@ -244,12 +244,12 @@ Feature: Publishing hide/show scenario of nodes | contentStreamIdForRemainingPart | "user-cs-identifier-modified" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node Then I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node Then I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to no node @@ -284,12 +284,12 @@ Feature: Publishing hide/show scenario of nodes | nodesToPublish | [{"nodeAggregateId": "sir-david-nodenborough", "workspaceName": "user-test", "dimensionSpacePoint": {}}] | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node Then I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to no node Then I expect node aggregate identifier "nody-mc-nodeface" to lead to no node Then I expect node aggregate identifier "sir-nodeward-nodington-iii" to lead to no node @@ -327,7 +327,7 @@ Feature: Publishing hide/show scenario of nodes | contentStreamIdForRemainingPart | "user-cs-identifier-modified" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to have the following references: | Name | Node | Properties | @@ -340,7 +340,7 @@ Feature: Publishing hide/show scenario of nodes | Name | Node | Properties | | referenceProperty | cs-identifier;sir-david-nodenborough;{} | null | - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier-modified;sir-david-nodenborough;{} And I expect this node to have the following references: | Name | Node | Properties | @@ -389,11 +389,11 @@ Feature: Publishing hide/show scenario of nodes | contentStreamIdForRemainingPart | "user-cs-identifier-modified" | And the graph projection is fully up to date - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "new1-agg" to lead to node cs-identifier;new1-agg;{} Then I expect node aggregate identifier "new2-agg" to lead to no node - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "new1-agg" to lead to node user-cs-identifier-modified;new1-agg;{} Then I expect node aggregate identifier "new2-agg" to lead to node user-cs-identifier-modified;new2-agg;{} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/05-PublishMovedNodesWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/05-PublishMovedNodesWithoutDimensions.feature index 9b2018d0cb1..9da2034d93c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/05-PublishMovedNodesWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/05-PublishMovedNodesWithoutDimensions.feature @@ -23,7 +23,7 @@ Feature: Publishing moved nodes without dimensions | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" + And I am in workspace "live" And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -89,7 +89,7 @@ Feature: Publishing moved nodes without dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-nodeward-nodington-iii" and node path "esquire" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings @@ -129,7 +129,7 @@ Feature: Publishing moved nodes without dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "sir-nodeward-nodington-iii" and node path "esquire" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings @@ -180,7 +180,7 @@ Feature: Publishing moved nodes without dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "sir-nodeward-nodington-iii" and node path "esquire" to lead to node cs-identifier;sir-nodeward-nodington-iii;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings @@ -224,7 +224,7 @@ Feature: Publishing moved nodes without dimensions # node aggregate occupation and coverage is not relevant without dimensions and thus not tested - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} And I expect node aggregate identifier "sir-david-nodenborough" and node path "document" to lead to node cs-identifier;sir-david-nodenborough;{} And I expect this node to be a child of node cs-identifier;lady-eleonode-rootford;{} And I expect this node to have no preceding siblings diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature index 4f19a7d6741..1aac8c9bde1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W9-WorkspaceDiscarding/02-DiscardWorkspace.feature @@ -23,7 +23,7 @@ Feature: Workspace discarding - basic functionality | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -58,7 +58,7 @@ Feature: Workspace discarding - basic functionality | propertyValues | {"text": "Modified"} | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | @@ -71,7 +71,7 @@ Feature: Workspace discarding - basic functionality | newContentStreamId | "user-cs-identifier-modified" | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-identifier-modified;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | @@ -101,7 +101,7 @@ Feature: Workspace discarding - basic functionality | newContentStreamId | "user-cs-identifier-modified" | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-identifier-modified;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/NodeOperationsOnMultipleWorkspaces.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/NodeOperationsOnMultipleWorkspaces.feature index 7e26a903218..7d178996e7a 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/NodeOperationsOnMultipleWorkspaces.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/NodeOperationsOnMultipleWorkspaces.feature @@ -17,7 +17,7 @@ Feature: Single Node operations on multiple workspaces/content streams; e.g. cop | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -73,26 +73,26 @@ Feature: Single Node operations on multiple workspaces/content streams; e.g. cop | propertyValues.text.value | "Changed" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | | text | "Original" | - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node user-cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | | text | "Changed" | - When I am in the active content stream of workspace "live" and dimension space point {} + When I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nodingers-cat" and node path "child/pet" to lead to node cs-identifier;nodingers-cat;{} When I go to the parent node of node aggregate "nodingers-cat" Then I expect this node to have the following properties: | Key | Value | | text | "Original" | - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "nodingers-cat" and node path "child/pet" to lead to node user-cs-identifier;nodingers-cat;{} When I go to the parent node of node aggregate "nodingers-cat" Then I expect this node to have the following properties: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/PruneContentStreams.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/PruneContentStreams.feature index 92dc9d8e919..34c8e21d299 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/PruneContentStreams.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/PruneContentStreams.feature @@ -14,7 +14,7 @@ Feature: If content streams are not in use anymore by the workspace, they can be | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "root-node" | @@ -47,7 +47,7 @@ Feature: If content streams are not in use anymore by the workspace, they can be | workspaceName | "user-test" | And the graph projection is fully up to date - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then the current content stream has state "IN_USE_BY_WORKSPACE" And the content stream "user-cs-identifier" has state "NO_LONGER_IN_USE" @@ -72,7 +72,7 @@ Feature: If content streams are not in use anymore by the workspace, they can be When I am in content stream "user-cs-identifier" and dimension space point {} Then I expect node aggregate identifier "root-node" to lead to no node - When I am in the active content stream of workspace "user-test" and dimension space point {} + When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "root-node" to lead to node user-cs-identifier-rebased;root-node;{} Scenario: NO_LONGER_IN_USE content streams can be cleaned up completely (simple case) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/SingleNodeOperationsOnLiveWorkspace.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/SingleNodeOperationsOnLiveWorkspace.feature index 1d68654f937..cee29b67b01 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/SingleNodeOperationsOnLiveWorkspace.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/Workspaces/SingleNodeOperationsOnLiveWorkspace.feature @@ -19,7 +19,7 @@ Feature: Single Node operations on live workspace | workspaceName | "live" | | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -51,7 +51,7 @@ Feature: Single Node operations on live workspace | propertyValues.text.value | "Hello" | When the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{} And I expect this node to have the following properties: | Key | Value | diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index 1ce93c44e86..b77ce854cf6 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -4,6 +4,7 @@ namespace Neos\ContentRepository\Core\CommandHandler; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventsToPublish; @@ -25,12 +26,12 @@ public function __construct(CommandHandlerInterface ...$handlers) $this->handlers = $handlers; } - public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { // TODO fail if multiple handlers can handle the same command foreach ($this->handlers as $handler) { if ($handler->canHandle($command)) { - return $handler->handle($command, $contentRepository); + return $handler->handle($command, $commandHandlingDependencies); } } throw new \RuntimeException(sprintf('No handler found for Command "%s"', get_debug_type($command)), 1649582778); diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index 4a8ca5a5b5f..e190337c370 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -4,6 +4,7 @@ namespace Neos\ContentRepository\Core\CommandHandler; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventsToPublish; @@ -18,5 +19,5 @@ interface CommandHandlerInterface { public function canHandle(CommandInterface $command): bool; - public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish; + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish; } diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandResult.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandResult.php index a3b1ab3701f..515309a4974 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandResult.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandResult.php @@ -5,75 +5,23 @@ namespace Neos\ContentRepository\Core\CommandHandler; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Projection\ProjectionInterface; -use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\EventStore\Model\Event\SequenceNumber; -use Neos\EventStore\Model\Event\Version; -use Neos\EventStore\Model\EventStore\CommitResult; /** - * Result of the {@see ContentRepository::handle()} method to be able to block until the projections were updated. + * Was the result of the {@see ContentRepository::handle()} method. + * Previously one would need this to be able to block until the projections were updated. * - * {@see PendingProjections} for a detailed explanation how the blocking works. + * This will no longer be required in the future see https://github.com/neos/neos-development-collection/pull/4988 * + * @deprecated this b/c layer will be removed with the next beta or before Neos 9 final release * @api */ final readonly class CommandResult { - public function __construct( - private PendingProjections $pendingProjections, - public CommitResult $commitResult, - ) { - } - - /** - * an empty command result which should not result in projection updates - * @return self - */ - public static function empty(): self - { - return new self( - PendingProjections::empty(), - new CommitResult( - Version::first(), - SequenceNumber::none() - ) - ); - } - /** - * Wait until all projections are up to date; i.e. have processed the events. - * - * @return void - * @api + * We block by default thus you must not call this method or use this legacy stub + * @deprecated this b/c layer will be removed with the next beta or before Neos 9 final release */ public function block(): void { - foreach ($this->pendingProjections->projections as $pendingProjection) { - $expectedSequenceNumber = $this->pendingProjections->getExpectedSequenceNumber($pendingProjection); - $this->blockProjection($pendingProjection, $expectedSequenceNumber); - } - } - - /** - * @param ProjectionInterface $projection - */ - private function blockProjection(ProjectionInterface $projection, SequenceNumber $expectedSequenceNumber): void - { - $attempts = 0; - while ($projection->getCheckpointStorage()->getHighestAppliedSequenceNumber()->value < $expectedSequenceNumber->value) { - usleep(50000); // 50000μs = 50ms - if (++$attempts > 100) { // 5 seconds - throw new \RuntimeException( - sprintf( - 'TIMEOUT while waiting for projection "%s" to catch up to sequence number %d ' . - '- check the error logs for details.', - $projection::class, - $expectedSequenceNumber->value - ), - 1550232279 - ); - } - } } } diff --git a/Neos.ContentRepository.Core/Classes/CommandHandlingDependencies.php b/Neos.ContentRepository.Core/Classes/CommandHandlingDependencies.php new file mode 100644 index 00000000000..c0b5270cc37 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/CommandHandlingDependencies.php @@ -0,0 +1,95 @@ +value to ContentGraphInterface + * @var array + */ + private array $overridenContentGraphInstances = []; + + public function __construct(private readonly ContentRepository $contentRepository) + { + } + + public function handle(CommandInterface $command): CommandResult + { + return $this->contentRepository->handle($command); + } + + public function getWorkspaceFinder(): WorkspaceFinder + { + return $this->contentRepository->getWorkspaceFinder(); + } + + public function getContentStreamFinder(): ContentStreamFinder + { + return $this->contentRepository->getContentStreamFinder(); + } + + /** + * @throws WorkspaceDoesNotExist if the workspace does not exist + */ + public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInterface + { + if (isset($this->overridenContentGraphInstances[$workspaceName->value])) { + return $this->overridenContentGraphInstances[$workspaceName->value]; + } + + return $this->contentRepository->getContentGraph($workspaceName); + } + + /** + * Stateful (dirty) override of the chosen ContentStreamId for a given workspace, it applies within the given closure. + * Implementations must ensure that requesting the contentStreamId for this workspace will resolve to the given + * override ContentStreamId and vice versa resolving the WorkspaceName from this ContentStreamId should result in the + * given WorkspaceName within the closure. + * + * @internal Used in write operations applying commands to a contentstream that will have WorkspaceName in the future + * but doesn't have one yet. + */ + public function overrideContentStreamId(WorkspaceName $workspaceName, ContentStreamId $contentStreamId, \Closure $fn): void + { + if (isset($this->overridenContentGraphInstances[$workspaceName->value])) { + throw new \RuntimeException('Contentstream override for this workspace already in effect, nesting not allowed.', 1715170938); + } + + $contentGraph = $this->contentRepository->projectionState(ContentGraphFinder::class)->getByWorkspaceNameAndContentStreamId($workspaceName, $contentStreamId); + $this->overridenContentGraphInstances[$workspaceName->value] = $contentGraph; + + try { + $fn(); + } finally { + unset($this->overridenContentGraphInstances[$workspaceName->value]); + } + } +} diff --git a/Neos.ContentRepository.Core/Classes/ContentGraphFactoryInterface.php b/Neos.ContentRepository.Core/Classes/ContentGraphFactoryInterface.php new file mode 100644 index 00000000000..2a766579751 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/ContentGraphFactoryInterface.php @@ -0,0 +1,35 @@ + + */ + private array $contentGraphInstances = []; + + public function __construct( + private readonly ContentGraphFactoryInterface $contentGraphFactory + ) { + } + + /** + * The default way to get a content graph to operate on. + * The currently assigned ContentStreamId for the given Workspace is resolved internally. + * + * @throws WorkspaceDoesNotExist if the provided workspace does not resolve to an existing content stream + * @see ContentRepository::getContentGraph() + */ + public function getByWorkspaceName(WorkspaceName $workspaceName): ContentGraphInterface + { + if (isset($this->contentGraphInstances[$workspaceName->value])) { + return $this->contentGraphInstances[$workspaceName->value]; + } + + $this->contentGraphInstances[$workspaceName->value] = $this->contentGraphFactory->buildForWorkspace($workspaceName); + return $this->contentGraphInstances[$workspaceName->value]; + } + + /** + * To release all held instances, in case a workspace/content stream relation needs to be reset + * + * @internal Should only be needed after write operations (which should take care on their own) + */ + public function forgetInstances(): void + { + $this->contentGraphInstances = []; + } + + /** + * For testing we allow getting an instance set by both parameters, effectively overriding the relationship at will + * + * @param WorkspaceName $workspaceName + * @param ContentStreamId $contentStreamId + * @internal Only for testing + */ + public function getByWorkspaceNameAndContentStreamId(WorkspaceName $workspaceName, ContentStreamId $contentStreamId): ContentGraphInterface + { + return $this->contentGraphFactory->buildForWorkspaceAndContentStream($workspaceName, $contentStreamId); + } +} diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 180e9fd0066..f6fc32cbc0e 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -38,7 +38,9 @@ use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryStatus; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\EventEnvelope; @@ -63,6 +65,8 @@ final class ContentRepository */ private array $projectionStateCache; + private CommandHandlingDependencies $commandHandlingDependencies; + /** * @internal use the {@see ContentRepositoryFactory::getOrBuild()} to instantiate @@ -80,6 +84,7 @@ public function __construct( private readonly UserIdProviderInterface $userIdProvider, private readonly ClockInterface $clock, ) { + $this->commandHandlingDependencies = new CommandHandlingDependencies($this); } /** @@ -96,7 +101,7 @@ public function handle(CommandInterface $command): CommandResult { // the commands only calculate which events they want to have published, but do not do the // publishing themselves - $eventsToPublish = $this->commandBus->handle($command, $this); + $eventsToPublish = $this->commandBus->handle($command, $this->commandHandlingDependencies); // TODO meaningful exception message $initiatingUserId = $this->userIdProvider->getUserId(); @@ -234,9 +239,12 @@ public function getNodeTypeManager(): NodeTypeManager return $this->nodeTypeManager; } - public function getContentGraph(): ContentGraphInterface + /** + * @throws WorkspaceDoesNotExist if the workspace does not exist + */ + public function getContentGraph(WorkspaceName $workspaceName): ContentGraphInterface { - return $this->projectionState(ContentGraphInterface::class); + return $this->projectionState(ContentGraphFinder::class)->getByWorkspaceName($workspaceName); } public function getWorkspaceFinder(): WorkspaceFinder diff --git a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php index 9a31aef29ad..31532f5e148 100644 --- a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php +++ b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php @@ -19,8 +19,6 @@ /** * A point in the dimension space with coordinates DimensionName => DimensionValue. * E.g.: ["language" => "es", "country" => "ar"] - * - * Implements CacheAwareInterface because of Fusion Runtime caching and Routing * @api */ final class DimensionSpacePoint extends AbstractDimensionSpacePoint diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php index bd25c64b498..f0bf2926419 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php @@ -43,6 +43,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased; use Neos\EventStore\Model\Event; use Neos\EventStore\Model\Event\EventData; +use Neos\EventStore\Model\Event\EventId; use Neos\EventStore\Model\Event\EventType; /** @@ -117,32 +118,6 @@ public function __construct() } } - public function getEventData(EventInterface $event): EventData - { - try { - $eventDataAsJson = json_encode($event, JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new \InvalidArgumentException( - sprintf( - 'Failed to normalize event of type "%s": %s', - get_debug_type($event), - $exception->getMessage() - ), - 1651838981 - ); - } - return EventData::fromString($eventDataAsJson); - } - - public function getEventType(EventInterface $event): EventType - { - $className = get_class($event); - - return $this->fullClassNameToShortEventType[$className] ?? throw new \RuntimeException( - 'Event type ' . get_class($event) . ' not registered' - ); - } - /** * @return class-string */ @@ -154,6 +129,23 @@ public function getEventClassName(Event $event): string ); } + public function normalize(EventInterface|DecoratedEvent $event): Event + { + $eventId = $event instanceof DecoratedEvent && $event->eventId !== null ? $event->eventId : EventId::create(); + $eventMetadata = $event instanceof DecoratedEvent ? $event->eventMetadata : null; + $causationId = $event instanceof DecoratedEvent ? $event->causationId : null; + $correlationId = $event instanceof DecoratedEvent ? $event->correlationId : null; + $event = $event instanceof DecoratedEvent ? $event->innerEvent : $event; + return new Event( + $eventId, + $this->getEventType($event), + $this->getEventData($event), + $eventMetadata, + $causationId, + $correlationId, + ); + } + public function denormalize(Event $event): EventInterface { $eventClassName = $this->getEventClassName($event); @@ -177,4 +169,30 @@ public function denormalize(Event $event): EventInterface default => $eventInstance, }; } + + private function getEventData(EventInterface $event): EventData + { + try { + $eventDataAsJson = json_encode($event, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new \InvalidArgumentException( + sprintf( + 'Failed to normalize event of type "%s": %s', + get_debug_type($event), + $exception->getMessage() + ), + 1651838981 + ); + } + return EventData::fromString($eventDataAsJson); + } + + private function getEventType(EventInterface $event): EventType + { + $className = get_class($event); + + return $this->fullClassNameToShortEventType[$className] ?? throw new \RuntimeException( + 'Event type ' . get_class($event) . ' not registered' + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 80b4aa07c94..aa0e2abed44 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -11,8 +11,6 @@ use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\ConcurrencyException; -use Neos\EventStore\Model\Event; -use Neos\EventStore\Model\Event\EventId; use Neos\EventStore\Model\Events; /** @@ -21,13 +19,13 @@ * * @internal */ -final class EventPersister +final readonly class EventPersister { public function __construct( - private readonly EventStoreInterface $eventStore, - private readonly ProjectionCatchUpTriggerInterface $projectionCatchUpTrigger, - private readonly EventNormalizer $eventNormalizer, - private readonly Projections $projections, + private EventStoreInterface $eventStore, + private ProjectionCatchUpTriggerInterface $projectionCatchUpTrigger, + private EventNormalizer $eventNormalizer, + private Projections $projections, ) { } @@ -39,12 +37,12 @@ public function __construct( public function publishEvents(EventsToPublish $eventsToPublish): CommandResult { if ($eventsToPublish->events->isEmpty()) { - return CommandResult::empty(); + return new CommandResult(); } // the following logic could also be done in an AppEventStore::commit method (being called // directly from the individual Command Handlers). $normalizedEvents = Events::fromArray( - $eventsToPublish->events->map(fn(EventInterface|DecoratedEvent $event) => $this->normalizeEvent($event)) + $eventsToPublish->events->map($this->eventNormalizer->normalize(...)) ); $commitResult = $this->eventStore->commit( $eventsToPublish->streamName, @@ -66,25 +64,6 @@ public function publishEvents(EventsToPublish $eventsToPublish): CommandResult } } $this->projectionCatchUpTrigger->triggerCatchUp($pendingProjections->projections); - - // The CommandResult can be used to block until projections are up to date. - return new CommandResult($pendingProjections, $commitResult); - } - - private function normalizeEvent(EventInterface|DecoratedEvent $event): Event - { - $eventId = $event instanceof DecoratedEvent && $event->eventId !== null ? $event->eventId : EventId::create(); - $eventMetadata = $event instanceof DecoratedEvent ? $event->eventMetadata : null; - $causationId = $event instanceof DecoratedEvent ? $event->causationId : null; - $correlationId = $event instanceof DecoratedEvent ? $event->correlationId : null; - $event = $event instanceof DecoratedEvent ? $event->innerEvent : $event; - return new Event( - $eventId, - $this->eventNormalizer->getEventType($event), - $this->eventNormalizer->getEventData($event), - $eventMetadata, - $causationId, - $correlationId, - ); + return new CommandResult(); } } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index b23f23851c0..bba7d784579 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -120,6 +120,7 @@ public function getOrBuild(): ContentRepository public function buildService( ContentRepositoryServiceFactoryInterface $serviceFactory ): ContentRepositoryServiceInterface { + $serviceFactoryDependencies = ContentRepositoryServiceFactoryDependencies::create( $this->projectionFactoryDependencies, $this->getOrBuild(), @@ -144,16 +145,16 @@ private function buildCommandBus(): CommandBus $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->contentDimensionZookeeper, $this->projectionFactoryDependencies->interDimensionalVariationGraph, - $this->projectionFactoryDependencies->propertyConverter + $this->projectionFactoryDependencies->propertyConverter, ), new DimensionSpaceCommandHandler( $this->projectionFactoryDependencies->contentDimensionZookeeper, - $this->projectionFactoryDependencies->interDimensionalVariationGraph + $this->projectionFactoryDependencies->interDimensionalVariationGraph, ), new NodeDuplicationCommandHandler( $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->contentDimensionZookeeper, - $this->projectionFactoryDependencies->interDimensionalVariationGraph + $this->projectionFactoryDependencies->interDimensionalVariationGraph, ) ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 468091c3ab4..54923882db6 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\Common; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; @@ -26,7 +26,12 @@ 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\Filter\FindPrecedingSiblingNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsClosed; use Neos\ContentRepository\Core\SharedModel\Exception\DimensionSpacePointIsNotYetOccupied; @@ -35,12 +40,13 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateDoesCurrentlyNotCoverDimensionSpacePointSet; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsDescendant; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsNoChild; +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\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsAbstract; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsNotOfTypeRoot; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsOfTypeRoot; @@ -74,16 +80,18 @@ abstract protected function getAllowedDimensionSubspace(): DimensionSpacePointSe */ protected function requireContentStream( WorkspaceName $workspaceName, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): ContentStreamId { - $contentStreamId = ContentStreamIdOverride::resolveContentStreamIdForWorkspace($contentRepository, $workspaceName); - if (!$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { + $contentStreamId = $commandHandlingDependencies->getContentGraph($workspaceName)->getContentStreamId(); + $state = $commandHandlingDependencies->getContentStreamFinder()->findStateForContentStream($contentStreamId); + if ($state === null) { throw new ContentStreamDoesNotExistYet( - 'Content stream "' . $contentStreamId->value . '" does not exist yet.', + 'Content stream for "' . $workspaceName->value . '" does not exist yet.', 1521386692 ); } - if ($contentRepository->getContentStreamFinder()->findStateForContentStream($contentStreamId) === ContentStreamState::STATE_CLOSED) { + + if ($state === ContentStreamState::STATE_CLOSED) { throw new ContentStreamIsClosed( 'Content stream "' . $contentStreamId->value . '" is closed.', 1710260081 @@ -143,19 +151,16 @@ protected function requireNodeTypeToNotBeOfTypeRoot(NodeType $nodeType): void } protected function requireRootNodeTypeToBeUnoccupied( - NodeTypeName $nodeTypeName, - ContentStreamId $contentStreamId, - ContentRepository $contentRepository + ContentGraphInterface $contentGraph, + NodeTypeName $nodeTypeName ): void { try { - $rootNodeAggregate = $contentRepository->getContentGraph()->findRootNodeAggregateByType( - $contentStreamId, - $nodeTypeName - ); - throw RootNodeAggregateTypeIsAlreadyOccupied::butWasExpectedNotTo($nodeTypeName); - } catch (RootNodeAggregateDoesNotExist $exception) { - // all is well + $contentGraph->findRootNodeAggregateByType($nodeTypeName); + } catch (RootNodeAggregateDoesNotExist $_) { + return; } + + throw RootNodeAggregateTypeIsAlreadyOccupied::butWasExpectedNotTo($nodeTypeName); } /** @@ -207,6 +212,17 @@ protected function requireNodeTypeToDeclareReference(NodeTypeName $nodeTypeName, } } + protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void + { + $nodeType = $this->requireNodeType($nodeTypeName); + if ($nodeType->hasTetheredNode($nodeName)) { + throw new NodeNameIsAlreadyCovered( + 'Node name "' . $nodeName->value . '" is reserved for a tethered child of parent node aggregate of type "' + . $nodeTypeName->value . '".' + ); + } + } + protected function requireNodeTypeToAllowNodesOfTypeInReference( NodeTypeName $nodeTypeName, ReferenceName $referenceName, @@ -255,31 +271,23 @@ protected function requireNodeTypeToAllowNumberOfReferencesInReference(Serialize } /** - * NodeType and NodeName must belong together to the same node, which is the to-be-checked one. - * - * @param ContentStreamId $contentStreamId - * @param NodeType $nodeType - * @param NodeName|null $nodeName * @param array|NodeAggregateId[] $parentNodeAggregateIds * @throws NodeConstraintException */ protected function requireConstraintsImposedByAncestorsAreMet( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeType $nodeType, - ?NodeName $nodeName, array $parentNodeAggregateIds, - ContentRepository $contentRepository ): void { foreach ($parentNodeAggregateIds as $parentNodeAggregateId) { $parentAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $parentNodeAggregateId, - $contentRepository + $contentGraph, + $parentNodeAggregateId ); if (!$parentAggregate->classification->isTethered()) { try { $parentsNodeType = $this->requireNodeType($parentAggregate->nodeTypeName); - $this->requireNodeTypeConstraintsImposedByParentToBeMet($parentsNodeType, $nodeName, $nodeType); + $this->requireNodeTypeConstraintsImposedByParentToBeMet($parentsNodeType, $nodeType); } catch (NodeTypeNotFound $e) { // skip constraint check; Once the parent is changed to be of an available type, // the constraint checks are executed again. See handleChangeNodeAggregateType @@ -287,8 +295,7 @@ protected function requireConstraintsImposedByAncestorsAreMet( } foreach ( - $contentRepository->getContentGraph()->findParentNodeAggregates( - $contentStreamId, + $contentGraph->findParentNodeAggregates( $parentNodeAggregateId ) as $grandParentNodeAggregate ) { @@ -314,7 +321,6 @@ protected function requireConstraintsImposedByAncestorsAreMet( */ protected function requireNodeTypeConstraintsImposedByParentToBeMet( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): void { // !!! IF YOU ADJUST THIS METHOD, also adjust the method below. @@ -325,37 +331,16 @@ protected function requireNodeTypeConstraintsImposedByParentToBeMet( 1707561400 ); } - if ( - $nodeName - && $parentsNodeType->hasTetheredNode($nodeName) - && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) - ) { - throw new NodeConstraintException( - 'Node type "' . $nodeType->name->value . '" does not match configured "' - . $this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->value - . '" for auto created child nodes for parent type "' . $parentsNodeType->name->value - . '" with name "' . $nodeName->value . '"', - 1707561404 - ); - } } protected function areNodeTypeConstraintsImposedByParentValid( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): bool { // !!! IF YOU ADJUST THIS METHOD, also adjust the method above. if (!$parentsNodeType->allowsChildNodeType($nodeType)) { return false; } - if ( - $nodeName - && $parentsNodeType->hasTetheredNode($nodeName) - && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) - ) { - return false; - } return true; } @@ -387,14 +372,9 @@ protected function areNodeTypeConstraintsImposedByGrandparentValid( ?NodeName $parentNodeName, NodeType $nodeType ): bool { - if ( - $parentNodeName + return !($parentNodeName && $grandParentsNodeType->hasTetheredNode($parentNodeName) - && !$this->getNodeTypeManager()->isNodeTypeAllowedAsChildToTetheredNode($grandParentsNodeType, $parentNodeName, $nodeType) - ) { - return false; - } - return true; + && !$this->getNodeTypeManager()->isNodeTypeAllowedAsChildToTetheredNode($grandParentsNodeType, $parentNodeName, $nodeType)); } /** @@ -402,12 +382,10 @@ protected function areNodeTypeConstraintsImposedByGrandparentValid( * @throws NodeAggregateCurrentlyDoesNotExist */ protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphInterface $contentGraph, + NodeAggregateId $nodeAggregateId ): NodeAggregate { - $nodeAggregate = $contentRepository->getContentGraph()->findNodeAggregateById( - $contentStreamId, + $nodeAggregate = $contentGraph->findNodeAggregateById( $nodeAggregateId ); @@ -426,12 +404,10 @@ protected function requireProjectedNodeAggregate( * @throws NodeAggregateCurrentlyExists */ protected function requireProjectedNodeAggregateToNotExist( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphInterface $contentGraph, + NodeAggregateId $nodeAggregateId ): void { - $nodeAggregate = $contentRepository->getContentGraph()->findNodeAggregateById( - $contentStreamId, + $nodeAggregate = $contentGraph->findNodeAggregateById( $nodeAggregateId ); @@ -447,14 +423,12 @@ protected function requireProjectedNodeAggregateToNotExist( * @throws NodeAggregateCurrentlyDoesNotExist */ public function requireProjectedParentNodeAggregate( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeAggregateId $childNodeAggregateId, - OriginDimensionSpacePoint $childOriginDimensionSpacePoint, - ContentRepository $contentRepository + OriginDimensionSpacePoint $childOriginDimensionSpacePoint ): NodeAggregate { - $parentNodeAggregate = $contentRepository->getContentGraph() + $parentNodeAggregate = $contentGraph ->findParentNodeAggregateByChildOriginDimensionSpacePoint( - $contentStreamId, $childNodeAggregateId, $childOriginDimensionSpacePoint ); @@ -463,7 +437,7 @@ public function requireProjectedParentNodeAggregate( throw new NodeAggregateCurrentlyDoesNotExist( 'Parent node aggregate for ' . $childNodeAggregateId->value . ' does currently not exist in origin dimension space point ' . $childOriginDimensionSpacePoint->toJson() - . ' and content stream ' . $contentStreamId->value, + . ' and workspace ' . $contentGraph->getWorkspaceName()->value, 1645368685 ); } @@ -482,7 +456,7 @@ protected function requireNodeAggregateToCoverDimensionSpacePoint( throw new NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint( 'Node aggregate "' . $nodeAggregate->nodeAggregateId->value . '" does currently not cover dimension space point ' - . json_encode($dimensionSpacePoint) . '.', + . json_encode($dimensionSpacePoint, JSON_THROW_ON_ERROR) . '.', 1541678877 ); } @@ -534,10 +508,9 @@ protected function requireNodeAggregateToBeUntethered(NodeAggregate $nodeAggrega * @throws NodeAggregateIsDescendant */ protected function requireNodeAggregateToNotBeDescendant( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeAggregate $nodeAggregate, - NodeAggregate $referenceNodeAggregate, - ContentRepository $contentRepository + NodeAggregate $referenceNodeAggregate ): void { if ($nodeAggregate->nodeAggregateId->equals($referenceNodeAggregate->nodeAggregateId)) { throw new NodeAggregateIsDescendant( @@ -547,80 +520,95 @@ protected function requireNodeAggregateToNotBeDescendant( ); } foreach ( - $contentRepository->getContentGraph()->findChildNodeAggregates( - $contentStreamId, + $contentGraph->findChildNodeAggregates( $referenceNodeAggregate->nodeAggregateId ) as $childReferenceNodeAggregate ) { $this->requireNodeAggregateToNotBeDescendant( - $contentStreamId, + $contentGraph, $nodeAggregate, - $childReferenceNodeAggregate, - $contentRepository + $childReferenceNodeAggregate ); } } /** - * @throws NodeNameIsAlreadyOccupied + * @throws NodeAggregateIsNoSibling */ - protected function requireNodeNameToBeUnoccupied( - ContentStreamId $contentStreamId, - ?NodeName $nodeName, - NodeAggregateId $parentNodeAggregateId, - OriginDimensionSpacePoint $parentOriginDimensionSpacePoint, - DimensionSpacePointSet $dimensionSpacePoints, - ContentRepository $contentRepository + protected function requireNodeAggregateToBeSibling( + ContentGraphInterface $contentGraph, + NodeAggregateId $referenceNodeAggregateId, + NodeAggregateId $siblingNodeAggregateId, + DimensionSpacePoint $dimensionSpacePoint, ): void { - if ($nodeName === null) { + $succeedingSiblings = $contentGraph->getSubgraph( + $dimensionSpacePoint, + VisibilityConstraints::withoutRestrictions() + )->findSucceedingSiblingNodes($referenceNodeAggregateId, FindSucceedingSiblingNodesFilter::create()); + if ($succeedingSiblings->toNodeAggregateIds()->contain($siblingNodeAggregateId)) { return; } - $dimensionSpacePointsOccupiedByChildNodeName = $contentRepository->getContentGraph() - ->getDimensionSpacePointsOccupiedByChildNodeName( - $contentStreamId, - $nodeName, - $parentNodeAggregateId, - $parentOriginDimensionSpacePoint, - $dimensionSpacePoints - ); - if (count($dimensionSpacePointsOccupiedByChildNodeName) > 0) { - throw new NodeNameIsAlreadyOccupied( - 'Child node name "' . $nodeName->value . '" is already occupied for parent "' - . $parentNodeAggregateId->value . '" in dimension space points ' - . $dimensionSpacePointsOccupiedByChildNodeName->toJson() - ); + + $precedingSiblings = $contentGraph->getSubgraph( + $dimensionSpacePoint, + VisibilityConstraints::withoutRestrictions() + )->findPrecedingSiblingNodes($referenceNodeAggregateId, FindPrecedingSiblingNodesFilter::create()); + if ($precedingSiblings->toNodeAggregateIds()->contain($siblingNodeAggregateId)) { + return; } + + throw NodeAggregateIsNoSibling::butWasExpectedToBeInDimensionSpacePoint( + $siblingNodeAggregateId, + $referenceNodeAggregateId, + $dimensionSpacePoint + ); + } + + /** + * @throws NodeAggregateIsNoChild + */ + protected function requireNodeAggregateToBeChild( + ContentGraphInterface $contentGraph, + NodeAggregateId $childNodeAggregateId, + NodeAggregateId $parentNodeAggregateId, + DimensionSpacePoint $dimensionSpacePoint, + ): void { + $childNodes = $contentGraph->getSubgraph( + $dimensionSpacePoint, + VisibilityConstraints::withoutRestrictions() + )->findChildNodes($parentNodeAggregateId, FindChildNodesFilter::create()); + if ($childNodes->toNodeAggregateIds()->contain($childNodeAggregateId)) { + return; + } + + throw NodeAggregateIsNoChild::butWasExpectedToBeInDimensionSpacePoint( + $childNodeAggregateId, + $parentNodeAggregateId, + $dimensionSpacePoint + ); } /** * @throws NodeNameIsAlreadyCovered */ protected function requireNodeNameToBeUncovered( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, ?NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, - DimensionSpacePointSet $dimensionSpacePointsToBeCovered, - ContentRepository $contentRepository ): void { if ($nodeName === null) { return; } - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( - $contentStreamId, + + $childNodeAggregate = $contentGraph->findChildNodeAggregateByName( $parentNodeAggregateId, $nodeName ); - foreach ($childNodeAggregates as $childNodeAggregate) { - /* @var $childNodeAggregate NodeAggregate */ - $alreadyCoveredDimensionSpacePoints = $childNodeAggregate->coveredDimensionSpacePoints - ->getIntersection($dimensionSpacePointsToBeCovered); - if (!$alreadyCoveredDimensionSpacePoints->isEmpty()) { - throw new NodeNameIsAlreadyCovered( - 'Node name "' . $nodeName->value . '" is already covered in dimension space points ' - . $alreadyCoveredDimensionSpacePoints->toJson() . ' by node aggregate "' - . $childNodeAggregate->nodeAggregateId->value . '".' - ); - } + if ($childNodeAggregate instanceof NodeAggregate) { + throw new NodeNameIsAlreadyCovered( + 'Node name "' . $nodeName->value . '" is already covered by node aggregate "' + . $childNodeAggregate->nodeAggregateId->value . '".' + ); } } @@ -633,7 +621,7 @@ protected function requireNodeAggregateToOccupyDimensionSpacePoint( ): void { if (!$nodeAggregate->occupiesDimensionSpacePoint($originDimensionSpacePoint)) { throw new DimensionSpacePointIsNotYetOccupied( - 'Dimension space point ' . json_encode($originDimensionSpacePoint) + 'Dimension space point ' . json_encode($originDimensionSpacePoint, JSON_PARTIAL_OUTPUT_ON_ERROR) . ' is not yet occupied by node aggregate "' . $nodeAggregate->nodeAggregateId->value . '"', 1552595396 ); @@ -649,7 +637,7 @@ protected function requireNodeAggregateToNotOccupyDimensionSpacePoint( ): void { if ($nodeAggregate->occupiesDimensionSpacePoint($originDimensionSpacePoint)) { throw new DimensionSpacePointIsAlreadyOccupied( - 'Dimension space point ' . json_encode($originDimensionSpacePoint) + 'Dimension space point ' . json_encode($originDimensionSpacePoint, JSON_PARTIAL_OUTPUT_ON_ERROR) . ' is already occupied by node aggregate "' . $nodeAggregate->nodeAggregateId->value . '"', 1552595441 ); @@ -693,12 +681,11 @@ protected function validateReferenceProperties( protected function getExpectedVersionOfContentStream( ContentStreamId $contentStreamId, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): ExpectedVersion { + return ExpectedVersion::fromVersion( - $contentRepository->getContentStreamFinder() - ->findVersionForContentStream($contentStreamId) - ->unwrap() + $commandHandlingDependencies->getContentStreamFinder()->findVersionForContentStream($contentStreamId)->unwrap() ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ContentStreamIdOverride.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ContentStreamIdOverride.php deleted file mode 100644 index 96bf09b3a80..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ContentStreamIdOverride.php +++ /dev/null @@ -1,67 +0,0 @@ -getWorkspaceFinder()->findOneByName($workspaceName)?->currentContentStreamId; - - if (!$contentStreamId) { - throw new ContentStreamDoesNotExistYet( - 'Content stream for workspace "' . $workspaceName->value . '" does not exist yet.', - 1710407870 - ); - } - - return $contentStreamId; - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php index 62c1f351f64..ef78ca2d0da 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeCreationInternals.php @@ -14,13 +14,12 @@ namespace Neos\ContentRepository\Core\Feature\Common; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation details of command handlers @@ -41,30 +40,21 @@ trait NodeCreationInternals * operates on the explicitly set succeeding sibling instead of the node itself. */ private function resolveInterdimensionalSiblingsForCreation( - ContentRepository $contentRepository, - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeAggregateId $requestedSucceedingSiblingNodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, DimensionSpacePointSet $coveredDimensionSpacePoints, ): InterdimensionalSiblings { - $originSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $sourceOrigin->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $originAlternativeSucceedingSiblings = $originSubgraph->findSucceedingSiblingNodes( + $subGraph = $contentGraph->getSubgraph($sourceOrigin->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions()); + $originAlternativeSucceedingSiblings = $subGraph->findSucceedingSiblingNodes( $requestedSucceedingSiblingNodeAggregateId, FindSucceedingSiblingNodesFilter::create() ); $interdimensionalSiblings = []; foreach ($coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { - $variantSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $coveredDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - $variantSucceedingSibling = $variantSubgraph->findNodeById($requestedSucceedingSiblingNodeAggregateId); + $subGraph = $contentGraph->getSubgraph($coveredDimensionSpacePoint, VisibilityConstraints::withoutRestrictions()); + $variantSucceedingSibling = $subGraph->findNodeById($requestedSucceedingSiblingNodeAggregateId); if ($variantSucceedingSibling) { // a) happy path, the explicitly requested succeeding sibling also exists in this dimension space point $interdimensionalSiblings[] = new InterdimensionalSibling( @@ -76,7 +66,7 @@ private function resolveInterdimensionalSiblingsForCreation( // check the other siblings succeeding in the origin dimension space point foreach ($originAlternativeSucceedingSiblings as $originSibling) { - $alternativeVariantSucceedingSibling = $variantSubgraph->findNodeById($originSibling->nodeAggregateId); + $alternativeVariantSucceedingSibling = $subGraph->findNodeById($originSibling->nodeAggregateId); if (!$alternativeVariantSucceedingSibling) { continue; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php index 54b8d0a60e2..08aaa79770a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/NodeVariationInternals.php @@ -23,12 +23,12 @@ 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\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; -use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * @internal implementation details of command handlers @@ -38,11 +38,10 @@ trait NodeVariationInternals abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\InterDimensionalVariationGraph; protected function createEventsForVariations( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { return match ( $this->getInterDimensionalVariationGraph()->getVariantType( @@ -51,45 +50,40 @@ protected function createEventsForVariations( ) ) { DimensionSpace\VariantType::TYPE_SPECIALIZATION => $this->handleCreateNodeSpecializationVariant( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ), DimensionSpace\VariantType::TYPE_GENERALIZATION => $this->handleCreateNodeGeneralizationVariant( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ), default => $this->handleCreateNodePeerVariant( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ), }; } protected function handleCreateNodeSpecializationVariant( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { $specializationVisibility = $this->calculateEffectiveVisibility($targetOrigin, $nodeAggregate); $events = $this->collectNodeSpecializationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, $nodeAggregate, $specializationVisibility, - [], - $contentRepository + [] ); return Events::fromArray($events); @@ -100,22 +94,20 @@ protected function handleCreateNodeSpecializationVariant( * @return array */ protected function collectNodeSpecializationVariantsThatWillHaveBeenCreated( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, NodeAggregate $nodeAggregate, DimensionSpacePointSet $specializationVisibility, - array $events, - ContentRepository $contentRepository + array $events ): array { $events[] = new NodeSpecializationVariantWasCreated( - $contentStreamId, + $contentGraph->getContentStreamId(), $nodeAggregate->nodeAggregateId, $sourceOrigin, $targetOrigin, $this->resolveInterdimensionalSiblings( - $contentRepository, - $contentStreamId, + $contentGraph, $nodeAggregate->nodeAggregateId, $sourceOrigin, $specializationVisibility @@ -123,19 +115,17 @@ protected function collectNodeSpecializationVariantsThatWillHaveBeenCreated( ); foreach ( - $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $contentStreamId, + $contentGraph->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ) as $tetheredChildNodeAggregate ) { $events = $this->collectNodeSpecializationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, $tetheredChildNodeAggregate, $specializationVisibility, - $events, - $contentRepository + $events ); } @@ -143,21 +133,19 @@ protected function collectNodeSpecializationVariantsThatWillHaveBeenCreated( } protected function handleCreateNodeGeneralizationVariant( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { $generalizationVisibility = $this->calculateEffectiveVisibility($targetOrigin, $nodeAggregate); $events = $this->collectNodeGeneralizationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, $nodeAggregate, $generalizationVisibility, - [], - $contentRepository + [] ); return Events::fromArray($events); @@ -168,22 +156,20 @@ protected function handleCreateNodeGeneralizationVariant( * @return array */ protected function collectNodeGeneralizationVariantsThatWillHaveBeenCreated( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, NodeAggregate $nodeAggregate, DimensionSpacePointSet $generalizationVisibility, - array $events, - ContentRepository $contentRepository + array $events ): array { $events[] = new NodeGeneralizationVariantWasCreated( - $contentStreamId, + $contentGraph->getContentStreamId(), $nodeAggregate->nodeAggregateId, $sourceOrigin, $targetOrigin, $this->resolveInterdimensionalSiblings( - $contentRepository, - $contentStreamId, + $contentGraph, $nodeAggregate->nodeAggregateId, $sourceOrigin, $generalizationVisibility @@ -191,19 +177,17 @@ protected function collectNodeGeneralizationVariantsThatWillHaveBeenCreated( ); foreach ( - $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $contentStreamId, + $contentGraph->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ) as $tetheredChildNodeAggregate ) { $events = $this->collectNodeGeneralizationVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, $tetheredChildNodeAggregate, $generalizationVisibility, - $events, - $contentRepository + $events ); } @@ -211,21 +195,19 @@ protected function collectNodeGeneralizationVariantsThatWillHaveBeenCreated( } protected function handleCreateNodePeerVariant( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events { $peerVisibility = $this->calculateEffectiveVisibility($targetOrigin, $nodeAggregate); $events = $this->collectNodePeerVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, $nodeAggregate, $peerVisibility, - [], - $contentRepository + [] ); return Events::fromArray($events); @@ -236,22 +218,20 @@ protected function handleCreateNodePeerVariant( * @return array */ protected function collectNodePeerVariantsThatWillHaveBeenCreated( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, NodeAggregate $nodeAggregate, DimensionSpacePointSet $peerVisibility, - array $events, - ContentRepository $contentRepository + array $events ): array { $events[] = new NodePeerVariantWasCreated( - $contentStreamId, + $contentGraph->getContentStreamId(), $nodeAggregate->nodeAggregateId, $sourceOrigin, $targetOrigin, $this->resolveInterdimensionalSiblings( - $contentRepository, - $contentStreamId, + $contentGraph, $nodeAggregate->nodeAggregateId, $sourceOrigin, $peerVisibility @@ -259,19 +239,17 @@ protected function collectNodePeerVariantsThatWillHaveBeenCreated( ); foreach ( - $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $contentStreamId, + $contentGraph->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ) as $tetheredChildNodeAggregate ) { $events = $this->collectNodePeerVariantsThatWillHaveBeenCreated( - $contentStreamId, + $contentGraph, $sourceOrigin, $targetOrigin, $tetheredChildNodeAggregate, $peerVisibility, - $events, - $contentRepository + $events ); } @@ -291,33 +269,20 @@ protected function collectNodePeerVariantsThatWillHaveBeenCreated( * except this operates on the to-be-varied node itself instead of an explicitly set succeeding sibling */ private function resolveInterdimensionalSiblings( - ContentRepository $contentRepository, - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeAggregateId $varyingNodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, DimensionSpacePointSet $variantCoverage, ): InterdimensionalSiblings { - $originSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $sourceOrigin->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $originSiblings = $originSubgraph->findSucceedingSiblingNodes( - $varyingNodeAggregateId, - FindSucceedingSiblingNodesFilter::create() - ); + $originSiblings = $contentGraph + ->getSubgraph($sourceOrigin->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions()) + ->findSucceedingSiblingNodes($varyingNodeAggregateId, FindSucceedingSiblingNodesFilter::create()); $interdimensionalSiblings = []; foreach ($variantCoverage as $variantDimensionSpacePoint) { - $variantSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $variantDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - // check the siblings succeeding in the origin dimension space point foreach ($originSiblings as $originSibling) { - $variantSibling = $variantSubgraph->findNodeById($originSibling->nodeAggregateId); + $variantSibling = $contentGraph->getSubgraph($variantDimensionSpacePoint, VisibilityConstraints::withoutRestrictions())->findNodeById($originSibling->nodeAggregateId); if (!$variantSibling) { continue; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php index 5d2a01fce2d..0a8f10ce699 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php @@ -14,7 +14,6 @@ * source code. */ -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; @@ -23,6 +22,7 @@ use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; @@ -40,11 +40,10 @@ trait TetheredNodeInternals abstract protected function getPropertyConverter(): PropertyConverter; abstract protected function createEventsForVariations( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - NodeAggregate $nodeAggregate, - ContentRepository $contentRepository + NodeAggregate $nodeAggregate ): Events; /** @@ -56,27 +55,19 @@ abstract protected function createEventsForVariations( * @throws \Exception */ protected function createEventsForMissingTetheredNode( + ContentGraphInterface $contentGraph, NodeAggregate $parentNodeAggregate, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeName $tetheredNodeName, ?NodeAggregateId $tetheredNodeAggregateId, - NodeType $expectedTetheredNodeType, - ContentRepository $contentRepository + NodeType $expectedTetheredNodeType ): Events { - $childNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( - $parentNodeAggregate->contentStreamId, + $childNodeAggregate = $contentGraph->findChildNodeAggregateByName( $parentNodeAggregate->nodeAggregateId, $tetheredNodeName ); - $tmp = []; - foreach ($childNodeAggregates as $childNodeAggregate) { - $tmp[] = $childNodeAggregate; - } - /** @var array $childNodeAggregates */ - $childNodeAggregates = $tmp; - - if (count($childNodeAggregates) === 0) { + if ($childNodeAggregate === null) { // there is no tethered child node aggregate already; let's create it! $nodeType = $this->nodeTypeManager->requireNodeType($parentNodeAggregate->nodeTypeName); if ($nodeType->isOfType(NodeTypeName::ROOT_NODE_TYPE_NAME)) { @@ -131,9 +122,7 @@ protected function createEventsForMissingTetheredNode( ) ); } - } elseif (count($childNodeAggregates) === 1) { - /** @var NodeAggregate $childNodeAggregate */ - $childNodeAggregate = current($childNodeAggregates); + } else { if (!$childNodeAggregate->classification->isTethered()) { throw new \RuntimeException( 'We found a child node aggregate through the given node path; but it is not tethered.' @@ -149,16 +138,10 @@ protected function createEventsForMissingTetheredNode( } /** @var Node $childNodeSource Node aggregates are never empty */ return $this->createEventsForVariations( - $parentNodeAggregate->contentStreamId, + $contentGraph, $childNodeSource->originDimensionSpacePoint, $originDimensionSpacePoint, - $parentNodeAggregate, - $contentRepository - ); - } else { - throw new \RuntimeException( - 'There is >= 2 ChildNodeAggregates with the same name reachable from the parent' . - '- this is ambiguous and we should analyze how this may happen. That is very likely a bug.' + $parentNodeAggregate ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php index 86fe2b2ac22..2c9d9160ccd 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; @@ -49,14 +50,14 @@ public function canHandle(CommandInterface $command): bool return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { return match ($command::class) { - CreateContentStream::class => $this->handleCreateContentStream($command, $contentRepository), - CloseContentStream::class => $this->handleCloseContentStream($command, $contentRepository), - ReopenContentStream::class => $this->handleReopenContentStream($command, $contentRepository), - ForkContentStream::class => $this->handleForkContentStream($command, $contentRepository), - RemoveContentStream::class => $this->handleRemoveContentStream($command, $contentRepository), + CreateContentStream::class => $this->handleCreateContentStream($command, $commandHandlingDependencies), + CloseContentStream::class => $this->handleCloseContentStream($command, $commandHandlingDependencies), + ReopenContentStream::class => $this->handleReopenContentStream($command, $commandHandlingDependencies), + ForkContentStream::class => $this->handleForkContentStream($command, $commandHandlingDependencies), + RemoveContentStream::class => $this->handleRemoveContentStream($command, $commandHandlingDependencies), default => throw new \DomainException('Cannot handle commands of class ' . get_class($command), 1710408206), }; } @@ -66,9 +67,9 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo */ private function handleCreateContentStream( CreateContentStream $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToNotExistYet($command->contentStreamId, $contentRepository); + $this->requireContentStreamToNotExistYet($command->contentStreamId, $commandHandlingDependencies); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId) ->getEventStreamName(); @@ -85,11 +86,11 @@ private function handleCreateContentStream( private function handleCloseContentStream( CloseContentStream $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $contentRepository); - $this->requireContentStreamToNotBeClosed($command->contentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotBeClosed($command->contentStreamId, $commandHandlingDependencies); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); return new EventsToPublish( @@ -105,11 +106,11 @@ private function handleCloseContentStream( private function handleReopenContentStream( ReopenContentStream $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $contentRepository); - $this->requireContentStreamToBeClosed($command->contentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToBeClosed($command->contentStreamId, $commandHandlingDependencies); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); return new EventsToPublish( @@ -130,14 +131,13 @@ private function handleReopenContentStream( */ private function handleForkContentStream( ForkContentStream $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireContentStreamToExist($command->sourceContentStreamId, $contentRepository); - $this->requireContentStreamToNotBeClosed($command->sourceContentStreamId, $contentRepository); - $this->requireContentStreamToNotExistYet($command->newContentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->sourceContentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotBeClosed($command->sourceContentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotExistYet($command->newContentStreamId, $commandHandlingDependencies); - $sourceContentStreamVersion = $contentRepository->getContentStreamFinder() - ->findVersionForContentStream($command->sourceContentStreamId); + $sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamFinder()->findVersionForContentStream($command->sourceContentStreamId); $streamName = ContentStreamEventStreamName::fromContentStreamId($command->newContentStreamId) ->getEventStreamName(); @@ -158,10 +158,10 @@ private function handleForkContentStream( private function handleRemoveContentStream( RemoveContentStream $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $contentRepository); + $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); $streamName = ContentStreamEventStreamName::fromContentStreamId( $command->contentStreamId @@ -180,13 +180,14 @@ private function handleRemoveContentStream( /** * @param ContentStreamId $contentStreamId + * @param CommandHandlingDependencies $commandHandlingDependencies * @throws ContentStreamAlreadyExists */ protected function requireContentStreamToNotExistYet( ContentStreamId $contentStreamId, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): void { - if ($contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { + if ($commandHandlingDependencies->getContentStreamFinder()->hasContentStream($contentStreamId)) { throw new ContentStreamAlreadyExists( 'Content stream "' . $contentStreamId->value . '" already exists.', 1521386345 @@ -196,13 +197,15 @@ protected function requireContentStreamToNotExistYet( /** * @param ContentStreamId $contentStreamId + * @param CommandHandlingDependencies $commandHandlingDependencies * @throws ContentStreamDoesNotExistYet */ protected function requireContentStreamToExist( ContentStreamId $contentStreamId, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): void { - if (!$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { + $maybeVersion = $commandHandlingDependencies->getContentStreamFinder()->findVersionForContentStream($contentStreamId); + if ($maybeVersion->isNothing()) { throw new ContentStreamDoesNotExistYet( 'Content stream "' . $contentStreamId->value . '" does not exist yet.', 1521386692 @@ -212,9 +215,10 @@ protected function requireContentStreamToExist( protected function requireContentStreamToNotBeClosed( ContentStreamId $contentStreamId, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): void { - if ($contentRepository->getContentStreamFinder()->findStateForContentStream($contentStreamId) === ContentStreamState::STATE_CLOSED) { + $contentStreamState = $commandHandlingDependencies->getContentStreamFinder()->findStateForContentStream($contentStreamId); + if ($contentStreamState === ContentStreamState::STATE_CLOSED) { throw new ContentStreamIsClosed( 'Content stream "' . $contentStreamId->value . '" is closed.', 1710260081 @@ -224,9 +228,10 @@ protected function requireContentStreamToNotBeClosed( protected function requireContentStreamToBeClosed( ContentStreamId $contentStreamId, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): void { - if ($contentRepository->getContentStreamFinder()->findStateForContentStream($contentStreamId) !== ContentStreamState::STATE_CLOSED) { + $contentStreamState = $commandHandlingDependencies->getContentStreamFinder()->findStateForContentStream($contentStreamId); + if ($contentStreamState !== ContentStreamState::STATE_CLOSED) { throw new ContentStreamIsNotClosed( 'Content stream "' . $contentStreamId->value . '" is not closed.', 1710405911 @@ -236,11 +241,11 @@ protected function requireContentStreamToBeClosed( protected function getExpectedVersionOfContentStream( ContentStreamId $contentStreamId, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): ExpectedVersion { + $maybeVersion = $commandHandlingDependencies->getContentStreamFinder()->findVersionForContentStream($contentStreamId); return ExpectedVersion::fromVersion( - $contentRepository->getContentStreamFinder() - ->findVersionForContentStream($contentStreamId) + $maybeVersion ->unwrap() ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php index ca0b66861b4..dadb48a00cf 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/DimensionSpaceAdjustment/DimensionSpaceCommandHandler.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\ContentDimensionZookeeper; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -33,9 +34,6 @@ use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Exception\DimensionSpacePointAlreadyExists; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; -use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Model\EventStream\ExpectedVersion; /** @@ -54,27 +52,26 @@ public function canHandle(CommandInterface $command): bool return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { - MoveDimensionSpacePoint::class => $this->handleMoveDimensionSpacePoint($command, $contentRepository), - AddDimensionShineThrough::class => $this->handleAddDimensionShineThrough($command, $contentRepository), + MoveDimensionSpacePoint::class => $this->handleMoveDimensionSpacePoint($command, $commandHandlingDependencies), + AddDimensionShineThrough::class => $this->handleAddDimensionShineThrough($command, $commandHandlingDependencies), }; } private function handleMoveDimensionSpacePoint( MoveDimensionSpacePoint $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStreamForWorkspaceName($command->workspaceName, $contentRepository); - $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(); self::requireDimensionSpacePointToBeEmptyInContentStream( - $command->target, - $contentStreamId, - $contentRepository->getContentGraph() + $contentGraph, + $command->target ); $this->requireDimensionSpacePointToExist($command->target); @@ -82,7 +79,7 @@ private function handleMoveDimensionSpacePoint( $streamName, Events::with( new DimensionSpacePointWasMoved( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->source, $command->target ), @@ -93,16 +90,15 @@ private function handleMoveDimensionSpacePoint( private function handleAddDimensionShineThrough( AddDimensionShineThrough $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStreamForWorkspaceName($command->workspaceName, $contentRepository); - $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(); self::requireDimensionSpacePointToBeEmptyInContentStream( - $command->target, - $contentStreamId, - $contentRepository->getContentGraph() + $contentGraph, + $command->target ); $this->requireDimensionSpacePointToExist($command->target); @@ -112,7 +108,7 @@ private function handleAddDimensionShineThrough( $streamName, Events::with( new DimensionShineThroughWasAdded( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->source, $command->target ) @@ -133,19 +129,14 @@ protected function requireDimensionSpacePointToExist(DimensionSpacePoint $dimens } private static function requireDimensionSpacePointToBeEmptyInContentStream( - DimensionSpacePoint $dimensionSpacePoint, - ContentStreamId $contentStreamId, - ContentGraphInterface $contentGraph + ContentGraphInterface $contentGraph, + DimensionSpacePoint $dimensionSpacePoint ): void { - $subgraph = $contentGraph->getSubgraph( - $contentStreamId, - $dimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - if ($subgraph->countNodes() > 0) { + $hasNodes = $contentGraph->getSubgraph($dimensionSpacePoint, VisibilityConstraints::withoutRestrictions())->countNodes(); + if ($hasNodes > 0) { throw new DimensionSpacePointAlreadyExists(sprintf( 'the content stream %s already contained nodes in dimension space point %s - this is not allowed.', - $contentStreamId->value, + $contentGraph->getContentStreamId()->value, $dimensionSpacePoint->toJson(), ), 1612898126); } @@ -164,23 +155,4 @@ private function requireDimensionSpacePointToBeSpecialization( throw DimensionSpacePointIsNoSpecialization::butWasSupposedToBe($target, $source); } } - - /** - * @throws ContentStreamDoesNotExistYet - */ - protected function requireContentStreamForWorkspaceName( - WorkspaceName $workspaceName, - ContentRepository $contentRepository - ): ContentStreamId { - $contentStreamId = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName) - ?->currentContentStreamId; - if (!$contentStreamId || !$contentRepository->getContentStreamFinder()->hasContentStream($contentStreamId)) { - throw new ContentStreamDoesNotExistYet( - 'Content stream "' . $contentStreamId?->value . '" does not exist yet.', - 1521386692 - ); - } - - return $contentStreamId; - } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php index 27b49905cb6..3830cf78e29 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; @@ -72,73 +73,51 @@ final class NodeAggregateCommandHandler implements CommandHandlerInterface use NodeVariation; use TetheredNodeInternals; - /** - * Used for constraint checks against the current outside configuration state of node types - */ - private NodeTypeManager $nodeTypeManager; - - /** - * Used for variation resolution from the current outside state of content dimensions - */ - private DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph; - - /** - * Used for constraint checks against the current outside configuration state of content dimensions - */ - private DimensionSpace\ContentDimensionZookeeper $contentDimensionZookeeper; - - protected PropertyConverter $propertyConverter; - /** * can be disabled in {@see NodeAggregateCommandHandler::withoutAncestorNodeTypeConstraintChecks()} */ private bool $ancestorNodeTypeConstraintChecksEnabled = true; public function __construct( - NodeTypeManager $nodeTypeManager, - DimensionSpace\ContentDimensionZookeeper $contentDimensionZookeeper, - DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph, - PropertyConverter $propertyConverter + private readonly NodeTypeManager $nodeTypeManager, + private readonly DimensionSpace\ContentDimensionZookeeper $contentDimensionZookeeper, + private readonly DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph, + private readonly PropertyConverter $propertyConverter, ) { - $this->nodeTypeManager = $nodeTypeManager; - $this->contentDimensionZookeeper = $contentDimensionZookeeper; - $this->interDimensionalVariationGraph = $interDimensionalVariationGraph; - $this->propertyConverter = $propertyConverter; } - public function canHandle(CommandInterface $command): bool { return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { - SetNodeProperties::class => $this->handleSetNodeProperties($command, $contentRepository), + SetNodeProperties::class => $this->handleSetNodeProperties($command, $commandHandlingDependencies), SetSerializedNodeProperties::class - => $this->handleSetSerializedNodeProperties($command, $contentRepository), - SetNodeReferences::class => $this->handleSetNodeReferences($command, $contentRepository), + => $this->handleSetSerializedNodeProperties($command, $commandHandlingDependencies), + SetNodeReferences::class => $this->handleSetNodeReferences($command, $commandHandlingDependencies), SetSerializedNodeReferences::class - => $this->handleSetSerializedNodeReferences($command, $contentRepository), - ChangeNodeAggregateType::class => $this->handleChangeNodeAggregateType($command, $contentRepository), - RemoveNodeAggregate::class => $this->handleRemoveNodeAggregate($command, $contentRepository), + => $this->handleSetSerializedNodeReferences($command, $commandHandlingDependencies), + ChangeNodeAggregateType::class => $this->handleChangeNodeAggregateType($command, $commandHandlingDependencies), + RemoveNodeAggregate::class => $this->handleRemoveNodeAggregate($command, $commandHandlingDependencies), CreateNodeAggregateWithNode::class - => $this->handleCreateNodeAggregateWithNode($command, $contentRepository), + => $this->handleCreateNodeAggregateWithNode($command, $commandHandlingDependencies), CreateNodeAggregateWithNodeAndSerializedProperties::class - => $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($command, $contentRepository), - MoveNodeAggregate::class => $this->handleMoveNodeAggregate($command, $contentRepository), - CreateNodeVariant::class => $this->handleCreateNodeVariant($command, $contentRepository), + => $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($command, $commandHandlingDependencies), + MoveNodeAggregate::class => $this->handleMoveNodeAggregate($command, $commandHandlingDependencies), + CreateNodeVariant::class => $this->handleCreateNodeVariant($command, $commandHandlingDependencies), CreateRootNodeAggregateWithNode::class - => $this->handleCreateRootNodeAggregateWithNode($command, $contentRepository), + => $this->handleCreateRootNodeAggregateWithNode($command, $commandHandlingDependencies), UpdateRootNodeAggregateDimensions::class - => $this->handleUpdateRootNodeAggregateDimensions($command, $contentRepository), - DisableNodeAggregate::class => $this->handleDisableNodeAggregate($command, $contentRepository), - EnableNodeAggregate::class => $this->handleEnableNodeAggregate($command, $contentRepository), - TagSubtree::class => $this->handleTagSubtree($command, $contentRepository), - UntagSubtree::class => $this->handleUntagSubtree($command, $contentRepository), - ChangeNodeAggregateName::class => $this->handleChangeNodeAggregateName($command, $contentRepository), + => $this->handleUpdateRootNodeAggregateDimensions($command, $commandHandlingDependencies), + DisableNodeAggregate::class => $this->handleDisableNodeAggregate($command, $commandHandlingDependencies), + EnableNodeAggregate::class => $this->handleEnableNodeAggregate($command, $commandHandlingDependencies), + TagSubtree::class => $this->handleTagSubtree($command, $commandHandlingDependencies), + UntagSubtree::class => $this->handleUntagSubtree($command, $commandHandlingDependencies), + ChangeNodeAggregateName::class => $this->handleChangeNodeAggregateName($command, $commandHandlingDependencies), }; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index 0683b5c6c63..0db539280a2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeCreation; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\Events; @@ -34,6 +34,7 @@ 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\NodePath; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; @@ -42,7 +43,6 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation detail of Command Handlers @@ -61,13 +61,15 @@ abstract protected function requireNodeType(NodeTypeName $nodeTypeName): NodeTyp abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): void; + abstract protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void; + abstract protected function getPropertyConverter(): PropertyConverter; abstract protected function getNodeTypeManager(): NodeTypeManager; private function handleCreateNodeAggregateWithNode( CreateNodeAggregateWithNode $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { $this->requireNodeType($command->nodeTypeName); $this->validateProperties($command->initialPropertyValues, $command->nodeTypeName); @@ -89,7 +91,7 @@ private function handleCreateNodeAggregateWithNode( $lowLevelCommand = $lowLevelCommand->withTetheredDescendantNodeAggregateIds($command->tetheredDescendantNodeAggregateIds); } - return $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($lowLevelCommand, $contentRepository); + return $this->handleCreateNodeAggregateWithNodeAndSerializedProperties($lowLevelCommand, $commandHandlingDependencies); } private function validateProperties(?PropertyValuesToWrite $propertyValues, NodeTypeName $nodeTypeName): void @@ -122,10 +124,11 @@ private function validateProperties(?PropertyValuesToWrite $propertyValues, Node */ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( CreateNodeAggregateWithNodeAndSerializedProperties $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $this->requireDimensionSpacePointToExist($command->originDimensionSpacePoint->toDimensionSpacePoint()); $nodeType = $this->requireNodeType($command->nodeTypeName); $this->requireNodeTypeToNotBeOfTypeRoot($nodeType); @@ -133,28 +136,23 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $this->requireTetheredDescendantNodeTypesToNotBeOfTypeRoot($nodeType); if ($this->areAncestorNodeTypeConstraintChecksEnabled()) { $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraph, $nodeType, - $command->nodeName, - [$command->parentNodeAggregateId], - $contentRepository + [$command->parentNodeAggregateId] ); } $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $parentNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->parentNodeAggregateId, - $contentRepository + $contentGraph, + $command->parentNodeAggregateId ); if ($command->succeedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->succeedingSiblingNodeAggregateId, - $contentRepository + $contentGraph, + $command->succeedingSiblingNodeAggregateId ); } $this->requireNodeAggregateToCoverDimensionSpacePoint( @@ -168,14 +166,12 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $parentNodeAggregate->coveredDimensionSpacePoints ); if ($command->nodeName) { - $this->requireNodeNameToBeUnoccupied( - $contentStreamId, + $this->requireNodeNameToBeUncovered( + $contentGraph, $command->nodeName, $command->parentNodeAggregateId, - $command->originDimensionSpacePoint, - $coveredDimensionSpacePoints, - $contentRepository ); + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($parentNodeAggregate->nodeTypeName, $command->nodeName); } $descendantNodeAggregateIds = $command->tetheredDescendantNodeAggregateIds->completeForNodeOfType( @@ -190,9 +186,8 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $descendantNodeAggregateIds->getNodeAggregateIds() as $descendantNodeAggregateId ) { $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $descendantNodeAggregateId, - $contentRepository + $contentGraph, + $descendantNodeAggregateId ); } @@ -201,26 +196,25 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $events = [ $this->createRegularWithNode( + $contentGraph, $command, - $contentStreamId, $coveredDimensionSpacePoints, - $initialPropertyValues, - $contentRepository + $initialPropertyValues ) ]; array_push($events, ...iterator_to_array($this->handleTetheredChildNodes( $command, - $contentStreamId, + $contentGraph, $nodeType, $coveredDimensionSpacePoints, $command->nodeAggregateId, $descendantNodeAggregateIds, - null, + null ))); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand($command, Events::fromArray($events)), $expectedVersion @@ -228,21 +222,19 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( } private function createRegularWithNode( + ContentGraphInterface $contentGraph, CreateNodeAggregateWithNodeAndSerializedProperties $command, - ContentStreamId $contentStreamId, DimensionSpacePointSet $coveredDimensionSpacePoints, SerializedPropertyValues $initialPropertyValues, - ContentRepository $contentRepository, ): NodeAggregateWithNodeWasCreated { return new NodeAggregateWithNodeWasCreated( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $command->nodeTypeName, $command->originDimensionSpacePoint, $command->succeedingSiblingNodeAggregateId ? $this->resolveInterdimensionalSiblingsForCreation( - $contentRepository, - $contentStreamId, + $contentGraph, $command->succeedingSiblingNodeAggregateId, $command->originDimensionSpacePoint, $coveredDimensionSpacePoints @@ -261,12 +253,12 @@ private function createRegularWithNode( */ private function handleTetheredChildNodes( CreateNodeAggregateWithNodeAndSerializedProperties $command, - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeType $nodeType, DimensionSpacePointSet $coveredDimensionSpacePoints, NodeAggregateId $parentNodeAggregateId, NodeAggregateIdsByNodePaths $nodeAggregateIds, - ?NodePath $nodePath, + ?NodePath $nodePath ): Events { $events = []; foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $rawNodeName => $childNodeType) { @@ -280,7 +272,7 @@ private function handleTetheredChildNodes( $initialPropertyValues = SerializedPropertyValues::defaultFromNodeType($childNodeType, $this->getPropertyConverter()); $events[] = new NodeAggregateWithNodeWasCreated( - $contentStreamId, + $contentGraph->getContentStreamId(), $childNodeAggregateId, $childNodeType->name, $command->originDimensionSpacePoint, @@ -293,12 +285,12 @@ private function handleTetheredChildNodes( array_push($events, ...iterator_to_array($this->handleTetheredChildNodes( $command, - $contentStreamId, + $contentGraph, $childNodeType, $coveredDimensionSpacePoints, $childNodeAggregateId, $nodeAggregateIds, - $childNodePath, + $childNodePath ))); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php index eca096a52c7..c74d9fce8c0 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/NodeDisabling.php @@ -14,7 +14,7 @@ * source code. */ -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\EventStore\Events; @@ -27,7 +27,6 @@ use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; /** @@ -38,23 +37,21 @@ trait NodeDisabling abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\InterDimensionalVariationGraph; /** + * @param DisableNodeAggregate $command + * @param CommandHandlingDependencies $commandHandlingDependencies * @return EventsToPublish - * @throws ContentStreamDoesNotExistYet - * @throws DimensionSpacePointNotFound - * @throws NodeAggregateCurrentlyDoesNotExist - * @throws NodeAggregatesTypeIsAmbiguous */ private function handleDisableNodeAggregate( DisableNodeAggregate $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, @@ -74,7 +71,7 @@ private function handleDisableNodeAggregate( $events = Events::with( new SubtreeWasTagged( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, SubtreeTag::disabled(), @@ -82,7 +79,7 @@ private function handleDisableNodeAggregate( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, @@ -94,6 +91,7 @@ private function handleDisableNodeAggregate( /** * @param EnableNodeAggregate $command + * @param CommandHandlingDependencies $commandHandlingDependencies * @return EventsToPublish * @throws ContentStreamDoesNotExistYet * @throws DimensionSpacePointNotFound @@ -101,15 +99,14 @@ private function handleDisableNodeAggregate( */ public function handleEnableNodeAggregate( EnableNodeAggregate $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, @@ -129,7 +126,7 @@ public function handleEnableNodeAggregate( $events = Events::with( new SubtreeWasUntagged( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, SubtreeTag::disabled(), @@ -137,7 +134,7 @@ public function handleEnableNodeAggregate( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand($command, $events), $expectedVersion ); diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index d88df293eab..33452d7be36 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -14,6 +14,8 @@ namespace Neos\ContentRepository\Core\Feature\NodeDuplication; +use Neos\ContentRepository\Core\CommandHandlingDependencies; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\ContentRepository; @@ -35,7 +37,7 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; 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; /** * @internal from userland, you'll use ContentRepository::handle to dispatch commands @@ -67,11 +69,11 @@ public function canHandle(CommandInterface $command): bool return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { - CopyNodesRecursively::class => $this->handleCopyNodesRecursively($command, $contentRepository), + CopyNodesRecursively::class => $this->handleCopyNodesRecursively($command, $commandHandlingDependencies), }; } @@ -80,11 +82,11 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo */ private function handleCopyNodesRecursively( CopyNodesRecursively $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { // Basic constraints (Content Stream / Dimension Space Point / Node Type of to-be-inserted root node) - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $this->requireDimensionSpacePointToExist( $command->targetDimensionSpacePoint->toDimensionSpacePoint() ); @@ -95,31 +97,26 @@ private function handleCopyNodesRecursively( // NOTE: we only check this for the *root* node of the to-be-inserted structure; and not for its // children (as we want to create the structure as-is; assuming it was already valid beforehand). $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraph, $nodeType, - $command->targetNodeName, - [$command->targetParentNodeAggregateId], - $contentRepository + [$command->targetParentNodeAggregateId] ); // Constraint: The new nodeAggregateIds are not allowed to exist yet. $this->requireNewNodeAggregateIdsToNotExist( - $contentStreamId, - $command->nodeAggregateIdMapping, - $contentRepository + $contentGraph, + $command->nodeAggregateIdMapping ); // Constraint: the parent node must exist in the command's DimensionSpacePoint as well $parentNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->targetParentNodeAggregateId, - $contentRepository + $contentGraph, + $command->targetParentNodeAggregateId ); if ($command->targetSucceedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->targetSucceedingSiblingNodeAggregateId, - $contentRepository + $contentGraph, + $command->targetSucceedingSiblingNodeAggregateId ); } $this->requireNodeAggregateToCoverDimensionSpacePoint( @@ -136,22 +133,19 @@ private function handleCopyNodesRecursively( $parentNodeAggregate->coveredDimensionSpacePoints ); - // Constraint: The node name must be free in all these dimension space points + // Constraint: The node name must be free for a new child of the parent node aggregate if ($command->targetNodeName) { - $this->requireNodeNameToBeUnoccupied( - $contentStreamId, + $this->requireNodeNameToBeUncovered( + $contentGraph, $command->targetNodeName, $command->targetParentNodeAggregateId, - $command->targetDimensionSpacePoint, - $coveredDimensionSpacePoints, - $contentRepository ); } // Now, we can start creating the recursive structure. $events = []; $this->createEventsForNodeToInsert( - $contentStreamId, + $contentGraph, $command->targetDimensionSpacePoint, $coveredDimensionSpacePoints, $command->targetParentNodeAggregateId, @@ -159,13 +153,12 @@ private function handleCopyNodesRecursively( $command->targetNodeName, $command->nodeTreeToInsert, $command->nodeAggregateIdMapping, - $contentRepository, $events ); return new EventsToPublish( ContentStreamEventStreamName::fromContentStreamId( - $contentStreamId + $contentGraph->getContentStreamId() )->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, @@ -176,15 +169,13 @@ private function handleCopyNodesRecursively( } private function requireNewNodeAggregateIdsToNotExist( - ContentStreamId $contentStreamId, - Dto\NodeAggregateIdMapping $nodeAggregateIdMapping, - ContentRepository $contentRepository + ContentGraphInterface $contentGraph, + Dto\NodeAggregateIdMapping $nodeAggregateIdMapping ): void { foreach ($nodeAggregateIdMapping->getAllNewNodeAggregateIds() as $nodeAggregateId) { $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $nodeAggregateId, - $contentRepository + $contentGraph, + $nodeAggregateId ); } } @@ -193,7 +184,7 @@ private function requireNewNodeAggregateIdsToNotExist( * @param array $events */ private function createEventsForNodeToInsert( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, OriginDimensionSpacePoint $originDimensionSpacePoint, DimensionSpacePointSet $coveredDimensionSpacePoints, NodeAggregateId $targetParentNodeAggregateId, @@ -201,11 +192,10 @@ private function createEventsForNodeToInsert( ?NodeName $targetNodeName, NodeSubtreeSnapshot $nodeToInsert, Dto\NodeAggregateIdMapping $nodeAggregateIdMapping, - ContentRepository $contentRepository, array &$events, ): void { $events[] = new NodeAggregateWithNodeWasCreated( - $contentStreamId, + $contentGraph->getContentStreamId(), $nodeAggregateIdMapping->getNewNodeAggregateId( $nodeToInsert->nodeAggregateId ) ?: NodeAggregateId::create(), @@ -213,8 +203,7 @@ private function createEventsForNodeToInsert( $originDimensionSpacePoint, $targetSucceedingSiblingNodeAggregateId ? $this->resolveInterdimensionalSiblingsForCreation( - $contentRepository, - $contentStreamId, + $contentGraph, $targetSucceedingSiblingNodeAggregateId, $originDimensionSpacePoint, $coveredDimensionSpacePoints @@ -228,7 +217,7 @@ private function createEventsForNodeToInsert( foreach ($nodeToInsert->childNodes as $childNodeToInsert) { $this->createEventsForNodeToInsert( - $contentStreamId, + $contentGraph, $originDimensionSpacePoint, $coveredDimensionSpacePoints, // the just-inserted node becomes the new parent node ID @@ -240,7 +229,6 @@ private function createEventsForNodeToInsert( $childNodeToInsert->nodeName, $childNodeToInsert, $nodeAggregateIdMapping, - $contentRepository, $events ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php index 49d71efa330..ec347b252fd 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/NodeModification.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeModification; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher; @@ -26,10 +26,10 @@ use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation detail of Command Handlers @@ -39,21 +39,20 @@ trait NodeModification abstract protected function requireNodeType(NodeTypeName $nodeTypeName): NodeType; abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphInterface $contentGraph, + NodeAggregateId $nodeAggregateId ): NodeAggregate; private function handleSetNodeProperties( SetNodeProperties $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); $this->requireDimensionSpacePointToExist($command->originDimensionSpacePoint->toDimensionSpacePoint()); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($nodeAggregate); $nodeTypeName = $nodeAggregate->nodeTypeName; @@ -71,20 +70,19 @@ private function handleSetNodeProperties( $command->propertyValues->getPropertiesToUnset() ); - return $this->handleSetSerializedNodeProperties($lowLevelCommand, $contentRepository); + return $this->handleSetSerializedNodeProperties($lowLevelCommand, $commandHandlingDependencies); } private function handleSetSerializedNodeProperties( SetSerializedNodeProperties $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); // Check if node exists $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $nodeType = $this->requireNodeType($nodeAggregate->nodeTypeName); $this->requireNodeAggregateToOccupyDimensionSpacePoint($nodeAggregate, $command->originDimensionSpacePoint); @@ -98,7 +96,7 @@ private function handleSetSerializedNodeProperties( ); foreach ($affectedOrigins as $affectedOrigin) { $events[] = new NodePropertiesWereSet( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $affectedOrigin, $nodeAggregate->getCoverageByOccupant($affectedOrigin), @@ -117,7 +115,7 @@ private function handleSetSerializedNodeProperties( ); foreach ($affectedOrigins as $affectedOrigin) { $events[] = new NodePropertiesWereSet( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $affectedOrigin, $nodeAggregate->getCoverageByOccupant($affectedOrigin), @@ -130,7 +128,7 @@ private function handleSetSerializedNodeProperties( $events = $this->mergeSplitEvents($events); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, @@ -152,7 +150,7 @@ private function splitPropertiesToUnsetByScope(PropertyNames $propertiesToUnset, } return array_map( - fn(array $propertyValues): PropertyNames => PropertyNames::fromArray($propertyValues), + PropertyNames::fromArray(...), $propertiesToUnsetByScope ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/CoverageNodeMoveMapping.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/CoverageNodeMoveMapping.php deleted file mode 100644 index 47273e8118d..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/CoverageNodeMoveMapping.php +++ /dev/null @@ -1,75 +0,0 @@ - $array - */ - public static function fromArray(array $array): self - { - if (!empty($array['newSucceedingSibling'])) { - return new self( - DimensionSpacePoint::fromArray($array['coveredDimensionSpacePoint']), - SucceedingSiblingNodeMoveDestination::fromArray($array['newSucceedingSibling']), - ); - } elseif (!empty($array['newParent'])) { - return new self( - DimensionSpacePoint::fromArray($array['coveredDimensionSpacePoint']), - ParentNodeMoveDestination::fromArray($array['newParent']), - ); - } else { - throw new \RuntimeException('!!!'); - } - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - if ($this->destination instanceof SucceedingSiblingNodeMoveDestination) { - return [ - 'coveredDimensionSpacePoint' => $this->coveredDimensionSpacePoint, - 'newSucceedingSibling' => $this->destination - ]; - } elseif ($this->destination instanceof ParentNodeMoveDestination) { - return [ - 'coveredDimensionSpacePoint' => $this->coveredDimensionSpacePoint, - 'newParent' => $this->destination - ]; - } else { - throw new \RuntimeException('!!!'); - } - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/CoverageNodeMoveMappings.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/CoverageNodeMoveMappings.php deleted file mode 100644 index 18b426add1b..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/CoverageNodeMoveMappings.php +++ /dev/null @@ -1,88 +0,0 @@ - - * @api DTO of {@see NodeAggregateWasMoved} event - */ -final class CoverageNodeMoveMappings implements \IteratorAggregate, \Countable, \JsonSerializable -{ - /** - * @var array - */ - private array $mappings; - - /** - * @param array $values - */ - private function __construct(array $values) - { - $this->mappings = $values; - } - - /** - * @param array|CoverageNodeMoveMapping> $mappings - */ - public static function fromArray(array $mappings): self - { - $processedMappings = []; - foreach ($mappings as $mapping) { - if (is_array($mapping)) { - $processedMappings[] = CoverageNodeMoveMapping::fromArray($mapping); - } elseif ($mapping instanceof CoverageNodeMoveMapping) { - $processedMappings[] = $mapping; - } else { - /** @var mixed $mapping */ - throw new \InvalidArgumentException( - sprintf( - 'Invalid NodeMoveMapping. Expected instance of %s, got: %s', - CoverageNodeMoveMapping::class, - is_object($mapping) ? get_class($mapping) : gettype($mapping) - ), - 1547811318 - ); - } - } - return new self($processedMappings); - } - - public static function create(CoverageNodeMoveMapping ...$coverageNodeMoveMappings): self - { - return new self(array_values($coverageNodeMoveMappings)); - } - - - /** - * @return \Traversable - */ - public function getIterator(): \Traversable - { - yield from $this->mappings; - } - - public function count(): int - { - return count($this->mappings); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->mappings; - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/OriginNodeMoveMapping.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/OriginNodeMoveMapping.php deleted file mode 100644 index 14bd2a5952b..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/OriginNodeMoveMapping.php +++ /dev/null @@ -1,46 +0,0 @@ - $array - */ - public static function fromArray(array $array): self - { - return new self( - OriginDimensionSpacePoint::fromArray($array['movedNodeOrigin']), - CoverageNodeMoveMappings::fromArray($array['newLocations']), - ); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return [ - 'movedNodeOrigin' => $this->movedNodeOrigin, - 'newLocations' => $this->newLocations, - ]; - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/OriginNodeMoveMappings.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/OriginNodeMoveMappings.php deleted file mode 100644 index b7fa09da0eb..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/OriginNodeMoveMappings.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @api DTO of {@see NodeAggregateWasMoved} event - */ -final class OriginNodeMoveMappings implements \IteratorAggregate, \Countable, \JsonSerializable -{ - /** - * @var array - */ - private array $mappings; - - /** - * @param array $values - */ - private function __construct(array $values) - { - $this->mappings = $values; - } - - /** - * @param array|OriginNodeMoveMapping> $mappings - */ - public static function fromArray(array $mappings): self - { - $processedMappings = []; - foreach ($mappings as $mapping) { - if (is_array($mapping)) { - $processedMappings[] = OriginNodeMoveMapping::fromArray($mapping); - } elseif ($mapping instanceof OriginNodeMoveMapping) { - $processedMappings[] = $mapping; - } else { - /** @var mixed $mapping */ - throw new \InvalidArgumentException( - sprintf( - 'Invalid NodeMoveMapping. Expected instance of %s, got: %s', - OriginNodeMoveMapping::class, - is_object($mapping) ? get_class($mapping) : gettype($mapping) - ), - 1547811318 - ); - } - } - return new self($processedMappings); - } - - public static function create(OriginNodeMoveMapping ...$originNodeMoveMappings): self - { - return new self(array_values($originNodeMoveMappings)); - } - - /** - * @return \Traversable - */ - public function getIterator(): \Traversable - { - yield from $this->mappings; - } - - public function count(): int - { - return count($this->mappings); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->mappings; - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/ParentNodeMoveDestination.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/ParentNodeMoveDestination.php deleted file mode 100644 index bfcbf872d10..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/ParentNodeMoveDestination.php +++ /dev/null @@ -1,87 +0,0 @@ - `ParentNodeMoveDestination(node1)` - * - * - you want to move the node INTO another node. - * ``` - * - node1 - * - node2 <-- source: we want to move this node - * - node3 - * - <-- destination - * ``` - * => `ParentNodeMoveDestination(node3)` - * - * For all other cases, use {@see SucceedingSiblingNodeMoveDestination}. - * - * @api DTO of {@see NodeAggregateWasMoved} event - */ -final readonly class ParentNodeMoveDestination implements \JsonSerializable -{ - private function __construct( - public NodeAggregateId $nodeAggregateId, - public OriginDimensionSpacePoint $originDimensionSpacePoint - ) { - } - - public static function create( - NodeAggregateId $nodeAggregateId, - OriginDimensionSpacePoint $originDimensionSpacePoint - ): self { - return new self($nodeAggregateId, $originDimensionSpacePoint); - } - - /** - * @param array $array - */ - public static function fromArray(array $array): self - { - return new self( - NodeAggregateId::fromString($array['nodeAggregateId']), - OriginDimensionSpacePoint::fromArray($array['originDimensionSpacePoint']) - ); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return [ - 'nodeAggregateId' => $this->nodeAggregateId, - 'originDimensionSpacePoint' => $this->originDimensionSpacePoint, - ]; - } - - public function toJson(): string - { - return json_encode($this, JSON_THROW_ON_ERROR); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/SucceedingSiblingNodeMoveDestination.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/SucceedingSiblingNodeMoveDestination.php deleted file mode 100644 index 182f1e80de2..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Dto/SucceedingSiblingNodeMoveDestination.php +++ /dev/null @@ -1,99 +0,0 @@ - `SucceedingSiblingNodeMoveDestination(node1)` - * - * - move nodes to a different parent, and before a specific sibling. - * ``` - * - node1 - * - <-- destination - * - node2 - * - node3 <-- source: we want to move this node - * ``` - * => `SucceedingSiblingNodeMoveDestination(node2)` - * - * @api DTO of {@see NodeAggregateWasMoved} event - */ -final readonly class SucceedingSiblingNodeMoveDestination implements \JsonSerializable -{ - private function __construct( - public NodeAggregateId $nodeAggregateId, - public OriginDimensionSpacePoint $originDimensionSpacePoint, - public NodeAggregateId $parentNodeAggregateId, - public OriginDimensionSpacePoint $parentOriginDimensionSpacePoint - ) { - } - - public static function create( - NodeAggregateId $nodeAggregateId, - OriginDimensionSpacePoint $originDimensionSpacePoint, - NodeAggregateId $parentNodeAggregateId, - OriginDimensionSpacePoint $parentOriginDimensionSpacePoint - ): self { - return new self( - $nodeAggregateId, - $originDimensionSpacePoint, - $parentNodeAggregateId, - $parentOriginDimensionSpacePoint - ); - } - - /** - * @param array $array - */ - public static function fromArray(array $array): self - { - return new self( - NodeAggregateId::fromString($array['nodeAggregateId']), - OriginDimensionSpacePoint::fromArray($array['originDimensionSpacePoint']), - NodeAggregateId::fromString($array['parentNodeAggregateId']), - OriginDimensionSpacePoint::fromArray($array['parentOriginDimensionSpacePoint']) - ); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return [ - 'nodeAggregateId' => $this->nodeAggregateId, - 'originDimensionSpacePoint' => $this->originDimensionSpacePoint, - 'parentNodeAggregateId' => $this->parentNodeAggregateId, - 'parentOriginDimensionSpacePoint' => $this->parentOriginDimensionSpacePoint, - ]; - } - - public function toJson(): string - { - return json_encode($this, JSON_THROW_ON_ERROR); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Event/NodeAggregateWasMoved.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Event/NodeAggregateWasMoved.php index a25538eef87..6bf52b0cf76 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Event/NodeAggregateWasMoved.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/Event/NodeAggregateWasMoved.php @@ -4,17 +4,17 @@ namespace Neos\ContentRepository\Core\Feature\NodeMove\Event; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\Feature\Common\EmbedsContentStreamAndNodeAggregateId; +use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSibling; +use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\Feature\Common\PublishableToOtherContentStreamsInterface; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMappings; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\ParentNodeMoveDestination; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** - * A node aggregate was moved in a content stream as defined in the node move mappings. + * A node aggregate was moved in a content stream * * We always move a node aggregate identified by a NodeAggregateId (in a given ContentStreamId); or * parts of the NodeAggregate. @@ -22,49 +22,29 @@ * The inner structure of the event is rather complicated, that's why the following picture shows it: * * ``` - *┌───────────────────────────┐ - *│ NodeAggregateWasMoved │ - *│-> contains NodeAggregateId│ - *└─────────┬─────────────────┘ + *┌───────────────────────────────────┐ + *│ NodeAggregateWasMoved │ + *│-> contains NodeAggregateId │ + *│-> contains parent NodeAggregateId │ + *└─────────┬─────────────────────────┘ * │ - * │ ┌────────────────────────────────────────────────┐ - * │ │ OriginNodeMoveMapping │ - * │ *│ -> contains OriginDimensionSpacePoint │ - * └───▶ │ - * │ (1 per OriginDimensionSpacePoint to be moved) │ - * └──────┬─────────────────────────────────────────┘ - * │ - * │ ┌───────────────────────────────────┐ - * │ │ CoverageNodeMoveMapping │ ┌─────────────────────────────┐ - * │ *│ -> coveredDimensionSpacePoint │ │ NoveMoveDestination │ - * └───▶ │ │ │ - * │ ?newSucceedingSibling, ?newParent ├───▶ nodeAggregateId │ - * │ (exactly one must be set) │ │ originDimensionSpacePoint │ - * │ │ └─────────────────────────────┘ - * │ (1 per coveredDimensionSpacePoint │ - * │ to be moved - for each edge in │ - * └───────────────────────────────────┘ + * │ ┌────────────────────────────────────────────────────┐ + * │ │ InterdimensionalSibling │ + * │ │ -> contains DimensionSpacePoint │ + * │ *│ -> contains succeeding sibling NodeAggregateId │ + * └───▶ │ + * │ (1 per affected dimension space point) │ + * └────────────────────────────────────────────────────┘ * ``` * * - We move some parts of a single NodeAggregate (`NodeAggregateWasMoved`). - * - For each OriginDimensionSpacePoint (where a materialized Node exists which should be moved), - * an `OriginNodeMoveMapping` exists. - * - For each Node which we want to move, we need to specify where the *incoming edges* of the node - * should be (if they should be moved). For each of these, a `CoverageNodeMoveMapping` exists. - * - * ## Specifying the Target of a Move inside `CoverageNodeMoveMapping` - * - * For a given `DimensionSpacePoint`, we specify the target node of the move as follows: - * - * - for *succeeding siblings* (see {@see SucceedingSiblingNodeMoveDestination}), we specify the - * newSucceedingSibling ID and its OriginDimensionSpacePoint; and additionally its parent ID - * and OriginDimensionSpacePoint. The parent is strictly redundant, but can ease projection - * implementer's lives. - * - * HINT: The parent is ALWAYS specified, so it is also specified *if it did not change*. - * - * - If you want to move something at the END of a children list (or as the first child, when no child - * exists yet), you specify `newParent` via {@see ParentNodeMoveDestination}. + * - If given, a single parent NodeAggregateId is provided and to be used for all affected DimensionSpacePoints. + * Else, no new parent will be set for any on the variants. + * - For each affected DimensionSpacePoint, an optional succeeding sibling is provided. + * -- If a single node is to be moved to the end, the succeeding sibling NodeAggregateId is null + * -- If a single node is to be moved to the start, the previous first sibling is to be set as succeeding sibling + * -- If a single node is not to be moved at all, e.g. if no siblings can be determined, it is considered unaffected + * and it (its DSP respectively) is not part of the InterdimensionalSibling collection * * @api events are the persistence-API of the content repository */ @@ -76,7 +56,8 @@ public function __construct( public ContentStreamId $contentStreamId, public NodeAggregateId $nodeAggregateId, - public OriginNodeMoveMappings $nodeMoveMappings, + public ?NodeAggregateId $newParentNodeAggregateId, + public InterdimensionalSiblings $succeedingSiblingsForCoverage, ) { } @@ -95,16 +76,43 @@ public function createCopyForContentStream(ContentStreamId $targetContentStreamI return new self( $targetContentStreamId, $this->nodeAggregateId, - $this->nodeMoveMappings, + $this->newParentNodeAggregateId, + $this->succeedingSiblingsForCoverage, ); } public static function fromArray(array $values): self { + if (array_key_exists('nodeMoveMappings', $values)) { + $newParentNodeAggregateId = null; + $succeedingSiblings = []; + foreach ($values['nodeMoveMappings'] as $nodeMoveMapping) { + // we don't care about origins anymore + foreach ($nodeMoveMapping['newLocations'] as $newLocation) { + if (array_key_exists('newParent', $newLocation)) { + $newParentNodeAggregateId = NodeAggregateId::fromString($newLocation['newParent']); + } + $succeedingSiblings[] = new InterdimensionalSibling( + DimensionSpacePoint::fromArray($newLocation['coveredDimensionSpacePoint']), + ($newLocation['newSucceedingSibling'] ?? null) + ? NodeAggregateId::fromString($newLocation['newSucceedingSibling']) + : null + ); + } + } + $succeedingSiblingsForCoverage = new InterdimensionalSiblings(...$succeedingSiblings); + } else { + $newParentNodeAggregateId = $values['newParentNodeAggregateId'] === null + ? null + : NodeAggregateId::fromString($values['newParentNodeAggregateId']); + $succeedingSiblingsForCoverage = InterdimensionalSiblings::fromArray($values['succeedingSiblingsForCoverage']); + } + return new self( ContentStreamId::fromString($values['contentStreamId']), NodeAggregateId::fromString($values['nodeAggregateId']), - OriginNodeMoveMappings::fromArray($values['nodeMoveMappings']), + $newParentNodeAggregateId, + $succeedingSiblingsForCoverage, ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php index 9290dfdb872..055407245ee 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php @@ -14,38 +14,35 @@ namespace Neos\ContentRepository\Core\Feature\NodeMove; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; 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\NodeAggregateEventPublisher; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMappings; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMappings; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\ParentNodeMoveDestination; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\RelationDistributionStrategy; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; -use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; -use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsDescendant; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsNoChild; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsNoSibling; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; /** * @internal implementation detail of Command Handlers @@ -56,31 +53,47 @@ abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\ abstract protected function areAncestorNodeTypeConstraintChecksEnabled(): bool; + abstract protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void; + abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository ): NodeAggregate; + abstract protected function requireNodeAggregateToBeSibling( + ContentGraphInterface $contentGraph, + NodeAggregateId $referenceNodeAggregateId, + NodeAggregateId $siblingNodeAggregateId, + DimensionSpacePoint $dimensionSpacePoint, + ): void; + + abstract protected function requireNodeAggregateToBeChild( + ContentGraphInterface $contentGraph, + NodeAggregateId $childNodeAggregateId, + NodeAggregateId $parentNodeAggregateId, + DimensionSpacePoint $dimensionSpacePoint, + ): void; + /** - * @return EventsToPublish * @throws ContentStreamDoesNotExistYet * @throws NodeAggregatesTypeIsAmbiguous * @throws NodeAggregateCurrentlyDoesNotExist * @throws DimensionSpacePointNotFound * @throws NodeAggregateIsDescendant + * @throws NodeAggregateIsNoSibling + * @throws NodeAggregateIsNoChild */ private function handleMoveNodeAggregate( MoveNodeAggregate $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $contentStreamId = $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); $this->requireDimensionSpacePointToExist($command->dimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, + $contentGraph, $command->nodeAggregateId, - $contentRepository ); $this->requireNodeAggregateToNotBeRoot($nodeAggregate); $this->requireNodeAggregateToBeUntethered($nodeAggregate); @@ -94,26 +107,24 @@ private function handleMoveNodeAggregate( if ($command->newParentNodeAggregateId) { $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraph, $this->requireNodeType($nodeAggregate->nodeTypeName), - $nodeAggregate->nodeName, [$command->newParentNodeAggregateId], - $contentRepository ); - $this->requireNodeNameToBeUncovered( - $contentStreamId, - $nodeAggregate->nodeName, + $newParentNodeAggregate = $this->requireProjectedNodeAggregate( + $contentGraph, $command->newParentNodeAggregateId, - $affectedDimensionSpacePoints, - $contentRepository ); - $newParentNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, + $this->requireNodeNameToBeUncovered( + $contentGraph, + $nodeAggregate->nodeName, $command->newParentNodeAggregateId, - $contentRepository ); + if ($nodeAggregate->nodeName) { + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($newParentNodeAggregate->nodeTypeName, $nodeAggregate->nodeName); + } $this->requireNodeAggregateToCoverDimensionSpacePoints( $newParentNodeAggregate, @@ -121,51 +132,71 @@ private function handleMoveNodeAggregate( ); $this->requireNodeAggregateToNotBeDescendant( - $contentStreamId, + $contentGraph, $newParentNodeAggregate, $nodeAggregate, - $contentRepository ); } if ($command->newPrecedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, + $contentGraph, $command->newPrecedingSiblingNodeAggregateId, - $contentRepository ); + if ($command->newParentNodeAggregateId) { + $this->requireNodeAggregateToBeChild( + $contentGraph, + $command->newPrecedingSiblingNodeAggregateId, + $command->newParentNodeAggregateId, + $command->dimensionSpacePoint, + ); + } else { + $this->requireNodeAggregateToBeSibling( + $contentGraph, + $command->nodeAggregateId, + $command->newPrecedingSiblingNodeAggregateId, + $command->dimensionSpacePoint, + ); + } } if ($command->newSucceedingSiblingNodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, + $contentGraph, $command->newSucceedingSiblingNodeAggregateId, - $contentRepository ); - } - - /** @var OriginNodeMoveMapping[] $originNodeMoveMappings */ - $originNodeMoveMappings = []; - foreach ($nodeAggregate->occupiedDimensionSpacePoints as $movedNodeOrigin) { - $originNodeMoveMappings[] = new OriginNodeMoveMapping( - $movedNodeOrigin, - $this->resolveCoverageNodeMoveMappings( - $contentStreamId, - $nodeAggregate, + if ($command->newParentNodeAggregateId) { + $this->requireNodeAggregateToBeChild( + $contentGraph, + $command->newSucceedingSiblingNodeAggregateId, $command->newParentNodeAggregateId, - $command->newPrecedingSiblingNodeAggregateId, + $command->dimensionSpacePoint, + ); + } else { + $this->requireNodeAggregateToBeSibling( + $contentGraph, + $command->nodeAggregateId, $command->newSucceedingSiblingNodeAggregateId, - $movedNodeOrigin, - $affectedDimensionSpacePoints, - $contentRepository - ) - ); + $command->dimensionSpacePoint, + ); + } } $events = Events::with( new NodeAggregateWasMoved( $contentStreamId, $command->nodeAggregateId, - OriginNodeMoveMappings::create(...$originNodeMoveMappings) + $command->newParentNodeAggregateId, + $this->resolveInterdimensionalSiblingsForMove( + $contentGraph, + $command->dimensionSpacePoint, + $affectedDimensionSpacePoints, + $command->nodeAggregateId, + $command->newParentNodeAggregateId, + $command->newSucceedingSiblingNodeAggregateId, + $command->newPrecedingSiblingNodeAggregateId, + ($command->newParentNodeAggregateId !== null) + || (($command->newSucceedingSiblingNodeAggregateId === null) && ($command->newPrecedingSiblingNodeAggregateId === null)), + ) ) ); @@ -183,43 +214,6 @@ private function handleMoveNodeAggregate( ); } - /** - * Resolves the new parents on a per-dimension-space-point basis - * - * If no parent node aggregate is defined, it will be resolved from the already evaluated new succeeding siblings. - * - * @todo move to content graph for more efficient calculation, if possible - */ - private function resolveNewParentAssignments( - /** The content stream the move operation is performed in */ - ContentStreamId $contentStreamId, - /** The parent node aggregate's id*/ - NodeAggregateId $parentId, - DimensionSpace\DimensionSpacePoint $coveredDimensionSpacePoint, - ContentRepository $contentRepository - ): CoverageNodeMoveMapping { - $contentSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $coveredDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - $parentNode = $contentSubgraph->findNodeById($parentId); - if ($parentNode === null) { - throw new \InvalidArgumentException( - 'Parent ' . $parentId->value . ' not found in subgraph ' . json_encode($contentSubgraph), - 1667596931 - ); - } - - return CoverageNodeMoveMapping::createForNewParent( - $coveredDimensionSpacePoint, - ParentNodeMoveDestination::create( - $parentId, - $parentNode->originDimensionSpacePoint - ) - ); - } - private function resolveAffectedDimensionSpacePointSet( NodeAggregate $nodeAggregate, Dto\RelationDistributionStrategy $relationDistributionStrategy, @@ -236,204 +230,141 @@ private function resolveAffectedDimensionSpacePointSet( }; } - private function findSibling( - ContentSubgraphInterface $contentSubgraph, - ?NodeAggregateId $parentId, - NodeAggregateId $siblingId - ): ?Node { - $siblingCandidate = $contentSubgraph->findNodeById($siblingId); - if ($parentId && $siblingCandidate) { - // If a parent node aggregate is explicitly given, all siblings must have this parent - $parent = $contentSubgraph->findParentNode($siblingId); - if (is_null($parent)) { - throw new \InvalidArgumentException( - 'Parent ' . $parentId->value . ' not found in subgraph ' . json_encode($contentSubgraph), - 1645366837 - ); - } - if ($parent->nodeAggregateId->equals($parentId)) { - return $siblingCandidate; - } - } else { - return $siblingCandidate; - } - - return null; - } - - private function resolveSucceedingSiblingFromOriginSiblings( + /** + * @param ?NodeAggregateId $parentNodeAggregateId the parent node aggregate ID to validate variant siblings against. + * If no new parent is given, the siblings are validated against the parent of the to-be-moved node in the respective dimension space point. + * @param bool $completeSet Whether unresolvable siblings should be added as null or not at all + * True when a new parent is set, which will result of the node being added at the end + * True when no preceding sibling is given and the succeeding sibling is explicitly set to null, which will result of the node being added at the end + * False when no new parent is set, which will result in the node not being moved + */ + private function resolveInterdimensionalSiblingsForMove( + ContentGraphInterface $contentGraph, + DimensionSpacePoint $selectedDimensionSpacePoint, + DimensionSpacePointSet $affectedDimensionSpacePoints, NodeAggregateId $nodeAggregateId, - ?NodeAggregateId $parentId, - ?NodeAggregateId $precedingSiblingId, + ?NodeAggregateId $parentNodeAggregateId, ?NodeAggregateId $succeedingSiblingId, - ContentSubgraphInterface $currentContentSubgraph, - ContentSubgraphInterface $originContentSubgraph - ): ?Node { - $succeedingSibling = null; - $precedingSiblingCandidates = iterator_to_array( - $precedingSiblingId - ? $originContentSubgraph->findPrecedingSiblingNodes($precedingSiblingId, FindPrecedingSiblingNodesFilter::create()) - : Nodes::createEmpty() - ); - $succeedingSiblingCandidates = iterator_to_array( - $succeedingSiblingId - ? $originContentSubgraph->findSucceedingSiblingNodes( - $succeedingSiblingId, - FindSucceedingSiblingNodesFilter::create() - ) - : Nodes::createEmpty() - ); - /* @var $precedingSiblingCandidates Node[] */ - /* @var $succeedingSiblingCandidates Node[] */ - $maximumIndex = max(count($succeedingSiblingCandidates), count($precedingSiblingCandidates)); - for ($i = 0; $i < $maximumIndex; $i++) { - // try successors of same distance first - if (isset($succeedingSiblingCandidates[$i])) { - if ($succeedingSiblingCandidates[$i]->nodeAggregateId->equals($nodeAggregateId)) { - \array_splice($succeedingSiblingCandidates, $i, 1); - } - $succeedingSibling = $this->findSibling( - $currentContentSubgraph, - $parentId, - $succeedingSiblingCandidates[$i]->nodeAggregateId - ); - if ($succeedingSibling) { - break; - } - } - if (isset($precedingSiblingCandidates[$i])) { - /** @var NodeAggregateId $precedingSiblingId can only be the case if not null */ - if ($precedingSiblingCandidates[$i]->nodeAggregateId->equals($nodeAggregateId)) { - \array_splice($precedingSiblingCandidates, $i, 1); - } - $precedingSibling = $this->findSibling( - $currentContentSubgraph, - $parentId, - $precedingSiblingCandidates[$i]->nodeAggregateId - ); - if ($precedingSibling) { - $alternateSucceedingSiblings = $currentContentSubgraph->findSucceedingSiblingNodes( - $precedingSiblingId, - FindSucceedingSiblingNodesFilter::create(pagination: Pagination::fromLimitAndOffset(1, 0)), - ); - if (count($alternateSucceedingSiblings) > 0) { - $succeedingSibling = $alternateSucceedingSiblings->first(); - break; - } - } - } - } - - return $succeedingSibling; - } - - private function resolveCoverageNodeMoveMappings( - /** The content stream the move operation is performed in */ - ContentStreamId $contentStreamId, - /** The node aggregate to be moved */ - NodeAggregate $nodeAggregate, - /** The parent node aggregate id, has precedence over siblings when in doubt */ - ?NodeAggregateId $parentId, - /** The planned preceding sibling's node aggregate id */ ?NodeAggregateId $precedingSiblingId, - /** The planned succeeding sibling's node aggregate id */ - ?NodeAggregateId $succeedingSiblingId, - /** A dimension space point occupied by the node aggregate to be moved */ - OriginDimensionSpacePoint $originDimensionSpacePoint, - /** The dimension space points affected by the move operation */ - DimensionSpacePointSet $affectedDimensionSpacePoints, - ContentRepository $contentRepository - ): CoverageNodeMoveMappings { - /** @var CoverageNodeMoveMapping[] $coverageNodeMoveMappings */ - $coverageNodeMoveMappings = []; - - $visibilityConstraints = VisibilityConstraints::withoutRestrictions(); - $originContentSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $originDimensionSpacePoint->toDimensionSpacePoint(), - $visibilityConstraints + bool $completeSet, + ): InterdimensionalSiblings { + $selectedSubgraph = $contentGraph->getSubgraph( + $selectedDimensionSpacePoint, + VisibilityConstraints::withoutRestrictions() ); - foreach ( - $nodeAggregate->getCoverageByOccupant($originDimensionSpacePoint) - ->getIntersection($affectedDimensionSpacePoints) as $dimensionSpacePoint - ) { - $contentSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, + $alternativeSucceedingSiblingIds = $succeedingSiblingId + ? $selectedSubgraph->findSucceedingSiblingNodes( + $succeedingSiblingId, + FindSucceedingSiblingNodesFilter::create() + )->toNodeAggregateIds() + : null; + $alternativePrecedingSiblingIds = $precedingSiblingId + ? $selectedSubgraph->findPrecedingSiblingNodes( + $precedingSiblingId, + FindPrecedingSiblingNodesFilter::create() + )->toNodeAggregateIds() + : null; + + $interdimensionalSiblings = []; + foreach ($affectedDimensionSpacePoints as $dimensionSpacePoint) { + $variantSubgraph = $contentGraph->getSubgraph( $dimensionSpacePoint, - $visibilityConstraints + VisibilityConstraints::withoutRestrictions() ); - - $succeedingSibling = $succeedingSiblingId - ? $this->findSibling($contentSubgraph, $parentId, $succeedingSiblingId) - : null; - if (!$succeedingSibling) { - $precedingSibling = $precedingSiblingId - ? $this->findSibling($contentSubgraph, $parentId, $precedingSiblingId) - : null; - if ($precedingSiblingId && $precedingSibling) { - $alternateSucceedingSiblings = $contentSubgraph->findSucceedingSiblingNodes( - $precedingSiblingId, - FindSucceedingSiblingNodesFilter::create(pagination: Pagination::fromLimitAndOffset(1, 0)), + if ($succeedingSiblingId) { + $variantSucceedingSibling = $variantSubgraph->findNodeById($succeedingSiblingId); + $variantParentId = $parentNodeAggregateId ?: $variantSubgraph->findParentNode($nodeAggregateId)?->nodeAggregateId; + $siblingParent = $variantSubgraph->findParentNode($succeedingSiblingId); + if ($variantSucceedingSibling && $siblingParent && $variantParentId?->equals($siblingParent->nodeAggregateId)) { + // a) happy path, the explicitly requested succeeding sibling also exists in this dimension space point + $interdimensionalSiblings[] = new InterdimensionalSibling( + $dimensionSpacePoint, + $variantSucceedingSibling->nodeAggregateId, ); - if (count($alternateSucceedingSiblings) > 0) { - $succeedingSibling = $alternateSucceedingSiblings->first(); + continue; + } + + // check the other siblings succeeding in the selected dimension space point + foreach ($alternativeSucceedingSiblingIds ?: [] as $alternativeSucceedingSiblingId) { + // the node itself is no valid succeeding sibling + if ($alternativeSucceedingSiblingId->equals($nodeAggregateId)) { + continue; + } + $alternativeVariantSucceedingSibling = $variantSubgraph->findNodeById($alternativeSucceedingSiblingId); + if (!$alternativeVariantSucceedingSibling) { + continue; + } + $siblingParent = $variantSubgraph->findParentNode($alternativeSucceedingSiblingId); + if (!$siblingParent || !$variantParentId?->equals($siblingParent->nodeAggregateId)) { + continue; } - } else { - $succeedingSibling = $this->resolveSucceedingSiblingFromOriginSiblings( - $nodeAggregate->nodeAggregateId, - $parentId, - $precedingSiblingId, - $succeedingSiblingId, - $contentSubgraph, - $originContentSubgraph + // b) one of the further succeeding sibling exists in this dimension space point + $interdimensionalSiblings[] = new InterdimensionalSibling( + $dimensionSpacePoint, + $alternativeVariantSucceedingSibling->nodeAggregateId, ); + continue 2; } } - if ($succeedingSibling) { - // for the event payload, we additionally need the parent of the succeeding sibling - $parentOfSucceedingSibling = $contentSubgraph->findParentNode($succeedingSibling->nodeAggregateId); - if ($parentOfSucceedingSibling === null) { - throw new \InvalidArgumentException( - 'Parent of succeeding sibling ' . $succeedingSibling->nodeAggregateId->value - . ' not found in subgraph ' . json_encode($contentSubgraph), - 1667817639 - ); + if ($precedingSiblingId) { + $variantPrecedingSiblingId = null; + $variantPrecedingSibling = $variantSubgraph->findNodeById($precedingSiblingId); + $variantParentId = $parentNodeAggregateId ?: $variantSubgraph->findParentNode($nodeAggregateId)?->nodeAggregateId; + $siblingParent = $variantSubgraph->findParentNode($precedingSiblingId); + if ($variantPrecedingSibling && $siblingParent && $variantParentId?->equals($siblingParent->nodeAggregateId)) { + // c) happy path, the explicitly requested preceding sibling also exists in this dimension space point + $variantPrecedingSiblingId = $precedingSiblingId; + } elseif ($alternativePrecedingSiblingIds) { + // check the other siblings preceding in the selected dimension space point + foreach ($alternativePrecedingSiblingIds as $alternativePrecedingSiblingId) { + // the node itself is no valid preceding sibling + if ($alternativePrecedingSiblingId->equals($nodeAggregateId)) { + continue; + } + $siblingParent = $variantSubgraph->findParentNode($alternativePrecedingSiblingId); + if (!$siblingParent || !$variantParentId?->equals($siblingParent->nodeAggregateId)) { + continue; + } + $alternativeVariantSucceedingSibling = $variantSubgraph->findNodeById($alternativePrecedingSiblingId); + if ($alternativeVariantSucceedingSibling) { + // d) one of the further preceding siblings exists in this dimension space point + $variantPrecedingSiblingId = $alternativePrecedingSiblingId; + break; + } + } } - $coverageNodeMoveMappings[] = CoverageNodeMoveMapping::createForNewSucceedingSibling( - $dimensionSpacePoint, - SucceedingSiblingNodeMoveDestination::create( - $succeedingSibling->nodeAggregateId, - $succeedingSibling->originDimensionSpacePoint, - $parentOfSucceedingSibling->nodeAggregateId, - $parentOfSucceedingSibling->originDimensionSpacePoint, - ) - ); - } else { - // preceding / succeeding siblings could not be resolved for a given covered DSP - // -> Fall back to resolving based on the parent - - if ($parentId === null) { - // if parent ID is not given, use the parent of the original node, because we want to move - // to the end of the sibling list. - $parentId = $contentSubgraph->findParentNode($nodeAggregate->nodeAggregateId)?->nodeAggregateId; - if ($parentId === null) { - throw new \InvalidArgumentException( - 'Parent ' . $parentId . ' not found in subgraph ' . json_encode($contentSubgraph), - 1667597013 - ); + if ($variantPrecedingSiblingId) { + // we fetch two siblings because the first might be the to-be-moved node itself + $variantSucceedingSiblingIds = $variantSubgraph->findSucceedingSiblingNodes( + $variantPrecedingSiblingId, + FindSucceedingSiblingNodesFilter::create(pagination: Pagination::fromLimitAndOffset(2, 0)) + )->toNodeAggregateIds(); + $relevantVariantSucceedingSiblingId = null; + foreach ($variantSucceedingSiblingIds as $variantSucceedingSiblingId) { + if (!$variantSucceedingSiblingId->equals($nodeAggregateId)) { + $relevantVariantSucceedingSiblingId = $variantSucceedingSiblingId; + break; + } } + $interdimensionalSiblings[] = new InterdimensionalSibling( + $dimensionSpacePoint, + $relevantVariantSucceedingSiblingId, + ); + continue; } - $coverageNodeMoveMappings[] = $this->resolveNewParentAssignments( - $contentStreamId, - $parentId, + } + + // e) fallback: if the set is to be completed, we add an empty sibling, otherwise we just don't + if ($completeSet) { + $interdimensionalSiblings[] = new InterdimensionalSibling( $dimensionSpacePoint, - $contentRepository + null, ); } } - return CoverageNodeMoveMappings::create(...$coverageNodeMoveMappings); + return new InterdimensionalSiblings(...$interdimensionalSiblings); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php index 507e5c6bb6c..9134d910691 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeReferencing/NodeReferencing.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeReferencing; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\ConstraintChecks; @@ -26,9 +26,10 @@ use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\NodeReferenceToWrite; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReference; use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; +use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * @internal implementation detail of Command Handlers @@ -38,22 +39,21 @@ trait NodeReferencing use ConstraintChecks; abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphInterface $contentGraph, + NodeAggregateId $nodeAggregateId ): NodeAggregate; private function handleSetNodeReferences( SetNodeReferences $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); $this->requireDimensionSpacePointToExist($command->sourceOriginDimensionSpacePoint->toDimensionSpacePoint()); $sourceNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->sourceNodeAggregateId, - $contentRepository + $contentGraph, + $command->sourceNodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($sourceNodeAggregate); $nodeTypeName = $sourceNodeAggregate->nodeTypeName; @@ -88,25 +88,24 @@ private function handleSetNodeReferences( )), ); - return $this->handleSetSerializedNodeReferences($lowLevelCommand, $contentRepository); + return $this->handleSetSerializedNodeReferences($lowLevelCommand, $commandHandlingDependencies); } /** - * @throws \Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet + * @throws ContentStreamDoesNotExistYet */ private function handleSetSerializedNodeReferences( SetSerializedNodeReferences $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $this->requireDimensionSpacePointToExist( $command->sourceOriginDimensionSpacePoint->toDimensionSpacePoint() ); $sourceNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->sourceNodeAggregateId, - $contentRepository + $contentGraph, + $command->sourceNodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($sourceNodeAggregate); $this->requireNodeAggregateToOccupyDimensionSpacePoint( @@ -124,9 +123,8 @@ private function handleSetSerializedNodeReferences( foreach ($command->references as $reference) { assert($reference instanceof SerializedNodeReference); $destinationNodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $reference->targetNodeAggregateId, - $contentRepository + $contentGraph, + $reference->targetNodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($destinationNodeAggregate); $this->requireNodeAggregateToCoverDimensionSpacePoint( @@ -152,7 +150,7 @@ private function handleSetSerializedNodeReferences( $events = Events::with( new NodeReferencesWereSet( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->sourceNodeAggregateId, $affectedOrigins, $command->referenceName, @@ -161,7 +159,7 @@ private function handleSetSerializedNodeReferences( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php index f169088ff73..085af255b8c 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/Command/RemoveNodeAggregate.php @@ -21,7 +21,6 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php index 446f7b58e45..41a8c5c7eed 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRemoval/NodeRemoval.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeRemoval; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\EventStore\Events; @@ -47,14 +47,14 @@ abstract protected function areAncestorNodeTypeConstraintChecksEnabled(): bool; */ private function handleRemoveNodeAggregate( RemoveNodeAggregate $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $this->requireNodeAggregateNotToBeTethered($nodeAggregate); @@ -64,15 +64,14 @@ private function handleRemoveNodeAggregate( ); if ($command->removalAttachmentPoint instanceof NodeAggregateId) { $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->removalAttachmentPoint, - $contentRepository + $contentGraph, + $command->removalAttachmentPoint ); } $events = Events::with( new NodeAggregateWasRemoved( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $command->nodeVariantSelectionStrategy->resolveAffectedOriginDimensionSpacePoints( $nodeAggregate->getOccupationByCovered($command->coveredDimensionSpacePoint), @@ -89,7 +88,7 @@ private function handleRemoveNodeAggregate( ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php index 40403f523ba..547a0693554 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeRenaming; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\ConstraintChecks; @@ -30,40 +30,36 @@ trait NodeRenaming { use ConstraintChecks; - private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, ContentRepository $contentRepository): EventsToPublish + private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $this->requireNodeAggregateToNotBeRoot($nodeAggregate, 'and Root Node Aggregates cannot be renamed'); $this->requireNodeAggregateToBeUntethered($nodeAggregate); - foreach ($contentRepository->getContentGraph()->findParentNodeAggregates($contentStreamId, $command->nodeAggregateId) as $parentNodeAggregate) { - foreach ($parentNodeAggregate->occupiedDimensionSpacePoints as $occupiedParentDimensionSpacePoint) { - $this->requireNodeNameToBeUnoccupied( - $contentStreamId, - $command->newNodeName, - $parentNodeAggregate->nodeAggregateId, - $occupiedParentDimensionSpacePoint, - $parentNodeAggregate->coveredDimensionSpacePoints, - $contentRepository - ); - } + foreach ($contentGraph->findParentNodeAggregates($command->nodeAggregateId) as $parentNodeAggregate) { + $this->requireNodeNameToBeUncovered( + $contentGraph, + $command->newNodeName, + $parentNodeAggregate->nodeAggregateId, + ); + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($parentNodeAggregate->nodeTypeName, $command->newNodeName); } $events = Events::with( new NodeAggregateNameWasChanged( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $command->newNodeName, ), ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, $events diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index 25e60f27449..39219ddfd1b 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeTypeChange; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; @@ -28,6 +28,7 @@ use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; @@ -38,7 +39,6 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @codingStandardsIgnoreStart */ /** @codingStandardsIgnoreEnd */ @@ -50,29 +50,26 @@ trait NodeTypeChange { abstract protected function getNodeTypeManager(): NodeTypeManager; + abstract protected function requireNodeAggregateToBeUntethered(NodeAggregate $nodeAggregate): void; + abstract protected function requireProjectedNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId, - ContentRepository $contentRepository + ContentGraphInterface $contentRepository, + NodeAggregateId $nodeAggregateId ): NodeAggregate; abstract protected function requireConstraintsImposedByAncestorsAreMet( - ContentStreamId $contentStreamId, + ContentGraphInterface $contentGraph, NodeType $nodeType, - ?NodeName $nodeName, - array $parentNodeAggregateIds, - ContentRepository $contentRepository + array $parentNodeAggregateIds ): void; abstract protected function requireNodeTypeConstraintsImposedByParentToBeMet( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): void; abstract protected function areNodeTypeConstraintsImposedByParentValid( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): bool; @@ -89,12 +86,12 @@ abstract protected function areNodeTypeConstraintsImposedByGrandparentValid( ): bool; abstract protected function createEventsForMissingTetheredNode( + ContentGraphInterface $contentGraph, NodeAggregate $parentNodeAggregate, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeName $tetheredNodeName, NodeAggregateId $tetheredNodeAggregateId, - NodeType $expectedTetheredNodeType, - ContentRepository $contentRepository + NodeType $expectedTetheredNodeType ): Events; /** @@ -102,23 +99,24 @@ abstract protected function createEventsForMissingTetheredNode( * @throws NodeConstraintException * @throws NodeTypeNotFoundException * @throws NodeAggregatesTypeIsAmbiguous + * @throws \Exception */ private function handleChangeNodeAggregateType( ChangeNodeAggregateType $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { /************** * Constraint checks **************/ // existence of content stream, node type and node aggregate - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $newNodeType = $this->requireNodeType($command->newNodeTypeName); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); + $this->requireNodeAggregateToBeUntethered($nodeAggregate); // node type detail checks $this->requireNodeTypeToNotBeOfTypeRoot($newNodeType); @@ -126,25 +124,22 @@ private function handleChangeNodeAggregateType( $this->requireTetheredDescendantNodeTypesToNotBeOfTypeRoot($newNodeType); // the new node type must be allowed at this position in the tree - $parentNodeAggregates = $contentRepository->getContentGraph()->findParentNodeAggregates( - $nodeAggregate->contentStreamId, + $parentNodeAggregates = $contentGraph->findParentNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($parentNodeAggregates as $parentNodeAggregate) { assert($parentNodeAggregate instanceof NodeAggregate); $this->requireConstraintsImposedByAncestorsAreMet( - $contentStreamId, + $contentGraph, $newNodeType, - $nodeAggregate->nodeName, - [$parentNodeAggregate->nodeAggregateId], - $contentRepository + [$parentNodeAggregate->nodeAggregateId] ); } /** @codingStandardsIgnoreStart */ match ($command->strategy) { NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_HAPPY_PATH - => $this->requireConstraintsImposedByHappyPathStrategyAreMet($nodeAggregate, $newNodeType, $contentRepository), + => $this->requireConstraintsImposedByHappyPathStrategyAreMet($contentGraph, $nodeAggregate, $newNodeType), NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE => null }; /** @codingStandardsIgnoreStop */ @@ -165,7 +160,7 @@ private function handleChangeNodeAggregateType( **************/ $events = [ new NodeAggregateTypeWasChanged( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $command->newNodeTypeName ), @@ -174,14 +169,14 @@ private function handleChangeNodeAggregateType( // remove disallowed nodes if ($command->strategy === NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE) { array_push($events, ...iterator_to_array($this->deleteDisallowedNodesWhenChangingNodeType( + $contentGraph, $nodeAggregate, - $newNodeType, - $contentRepository + $newNodeType ))); array_push($events, ...iterator_to_array($this->deleteObsoleteTetheredNodesWhenChangingNodeType( + $contentGraph, $nodeAggregate, - $newNodeType, - $contentRepository + $newNodeType ))); } @@ -192,33 +187,32 @@ private function handleChangeNodeAggregateType( foreach ($expectedTetheredNodes as $serializedTetheredNodeName => $expectedTetheredNodeType) { $tetheredNodeName = NodeName::fromString($serializedTetheredNodeName); - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $node->subgraphIdentity->contentStreamId, + $tetheredNode = $contentGraph->getSubgraph( $node->originDimensionSpacePoint->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions() - ); - $tetheredNode = $subgraph->findNodeByPath( + )->findNodeByPath( $tetheredNodeName, - $node->nodeAggregateId + $node->nodeAggregateId, ); + if ($tetheredNode === null) { $tetheredNodeAggregateId = $command->tetheredDescendantNodeAggregateIds ->getNodeAggregateId(NodePath::fromString($tetheredNodeName->value)) ?: NodeAggregateId::create(); array_push($events, ...iterator_to_array($this->createEventsForMissingTetheredNode( + $contentGraph, $nodeAggregate, $node->originDimensionSpacePoint, $tetheredNodeName, $tetheredNodeAggregateId, - $expectedTetheredNodeType, - $contentRepository + $expectedTetheredNodeType ))); } } } return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, Events::fromArray($events), @@ -234,14 +228,13 @@ private function handleChangeNodeAggregateType( * @throws NodeConstraintException|NodeTypeNotFoundException */ private function requireConstraintsImposedByHappyPathStrategyAreMet( + ContentGraphInterface $contentGraph, NodeAggregate $nodeAggregate, - NodeType $newNodeType, - ContentRepository $contentRepository + NodeType $newNodeType ): void { // 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 = $contentRepository->getContentGraph()->findChildNodeAggregates( - $nodeAggregate->contentStreamId, + $childNodeAggregates = $contentGraph->findChildNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($childNodeAggregates as $childNodeAggregate) { @@ -250,17 +243,14 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( // so we use $newNodeType (the target node type of $node after the operation) here. $this->requireNodeTypeConstraintsImposedByParentToBeMet( $newNodeType, - $childNodeAggregate->nodeName, $this->requireNodeType($childNodeAggregate->nodeTypeName) ); // 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 = $contentRepository->getContentGraph()->findChildNodeAggregates( - $childNodeAggregate->contentStreamId, + $grandchildNodeAggregates = $contentGraph->findChildNodeAggregates( $childNodeAggregate->nodeAggregateId ); foreach ($grandchildNodeAggregates as $grandchildNodeAggregate) { @@ -282,15 +272,14 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( * needs to be modified as well (as they are structurally the same) */ private function deleteDisallowedNodesWhenChangingNodeType( + ContentGraphInterface $contentGraph, NodeAggregate $nodeAggregate, - NodeType $newNodeType, - ContentRepository $contentRepository + 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 = $contentRepository->getContentGraph()->findChildNodeAggregates( - $nodeAggregate->contentStreamId, + $childNodeAggregates = $contentGraph->findChildNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($childNodeAggregates as $childNodeAggregate) { @@ -301,16 +290,15 @@ private function deleteDisallowedNodesWhenChangingNodeType( !$childNodeAggregate->classification->isTethered() && !$this->areNodeTypeConstraintsImposedByParentValid( $newNodeType, - $childNodeAggregate->nodeName, $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, - $contentRepository + $childNodeAggregate ); // AND REMOVE THEM $events[] = $this->removeNodeInDimensionSpacePointSet( @@ -321,13 +309,9 @@ private function deleteDisallowedNodesWhenChangingNodeType( // 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 = $contentRepository->getContentGraph()->findChildNodeAggregates( - $childNodeAggregate->contentStreamId, - $childNodeAggregate->nodeAggregateId - ); + $grandchildNodeAggregates = $contentGraph->findChildNodeAggregates($childNodeAggregate->nodeAggregateId); foreach ($grandchildNodeAggregates as $grandchildNodeAggregate) { /* @var $grandchildNodeAggregate NodeAggregate */ // we do not need to test for the parent of grandchild (=child), @@ -344,9 +328,9 @@ private function deleteDisallowedNodesWhenChangingNodeType( // 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, - $contentRepository + $grandchildNodeAggregate ); // AND REMOVE THEM $events[] = $this->removeNodeInDimensionSpacePointSet( @@ -361,18 +345,15 @@ private function deleteDisallowedNodesWhenChangingNodeType( } private function deleteObsoleteTetheredNodesWhenChangingNodeType( + ContentGraphInterface $contentGraph, NodeAggregate $nodeAggregate, - NodeType $newNodeType, - ContentRepository $contentRepository + NodeType $newNodeType ): Events { $expectedTetheredNodes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($newNodeType); $events = []; // find disallowed tethered nodes - $tetheredNodeAggregates = $contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $nodeAggregate->contentStreamId, - $nodeAggregate->nodeAggregateId - ); + $tetheredNodeAggregates = $contentGraph->findTetheredChildNodeAggregates($nodeAggregate->nodeAggregateId); foreach ($tetheredNodeAggregates as $tetheredNodeAggregate) { /* @var $tetheredNodeAggregate NodeAggregate */ @@ -380,9 +361,9 @@ private function deleteObsoleteTetheredNodesWhenChangingNodeType( // 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, - $contentRepository + $tetheredNodeAggregate ); // AND REMOVE THEM $events[] = $this->removeNodeInDimensionSpacePointSet( @@ -419,18 +400,15 @@ private function deleteObsoleteTetheredNodesWhenChangingNodeType( * we originated from) */ private function findDimensionSpacePointsConnectingParentAndChildAggregate( + ContentGraphInterface $contentGraph, NodeAggregate $parentNodeAggregate, - NodeAggregate $childNodeAggregate, - ContentRepository $contentRepository + NodeAggregate $childNodeAggregate ): DimensionSpacePointSet { $points = []; foreach ($childNodeAggregate->coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $childNodeAggregate->contentStreamId, - $coveredDimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() + $parentNode = $contentGraph->getSubgraph($coveredDimensionSpacePoint, VisibilityConstraints::withoutRestrictions())->findParentNode( + $childNodeAggregate->nodeAggregateId ); - $parentNode = $subgraph->findParentNode($childNodeAggregate->nodeAggregateId); if ( $parentNode && $parentNode->nodeAggregateId->equals($parentNodeAggregate->nodeAggregateId) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php index c842644e845..001f9bd66e9 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/Command/CreateNodeVariant.php @@ -20,7 +20,6 @@ use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php index 7df4a3acb5b..fe9a58f7d3e 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeVariation/NodeVariation.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\NodeVariation; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace\Exception\DimensionSpacePointNotFound; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\Common\ConstraintChecks; @@ -48,14 +48,14 @@ trait NodeVariation */ private function handleCreateNodeVariant( CreateNodeVariant $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); // we do this check first, because it gives a more meaningful error message on what you need to do. // we cannot use sentences with "." because the UI will only print the 1st sentence :/ @@ -66,10 +66,9 @@ private function handleCreateNodeVariant( $this->requireNodeAggregateToOccupyDimensionSpacePoint($nodeAggregate, $command->sourceOrigin); $this->requireNodeAggregateToNotOccupyDimensionSpacePoint($nodeAggregate, $command->targetOrigin); $parentNodeAggregate = $this->requireProjectedParentNodeAggregate( - $contentStreamId, + $contentGraph, $command->nodeAggregateId, - $command->sourceOrigin, - $contentRepository + $command->sourceOrigin ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $parentNodeAggregate, @@ -77,15 +76,14 @@ private function handleCreateNodeVariant( ); $events = $this->createEventsForVariations( - $contentStreamId, + $contentGraph, $command->sourceOrigin, $command->targetOrigin, - $nodeAggregate, - $contentRepository + $nodeAggregate ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, $events diff --git a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php index 28d9183679e..dd2c6ea07d9 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/RootNodeCreation/RootNodeHandling.php @@ -14,7 +14,7 @@ namespace Neos\ContentRepository\Core\Feature\RootNodeCreation; -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\Events; @@ -57,6 +57,7 @@ abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): v /** * @param CreateRootNodeAggregateWithNode $command + * @param CommandHandlingDependencies $commandHandlingDependencies * @return EventsToPublish * @throws ContentStreamDoesNotExistYet * @throws NodeAggregateCurrentlyExists @@ -66,21 +67,20 @@ abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): v */ private function handleCreateRootNodeAggregateWithNode( CreateRootNodeAggregateWithNode $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $this->requireProjectedNodeAggregateToNotExist( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $nodeType = $this->requireNodeType($command->nodeTypeName); $this->requireNodeTypeToBeOfTypeRoot($nodeType); $this->requireRootNodeTypeToBeUnoccupied( - $nodeType->name, - $contentStreamId, - $contentRepository + $contentGraph, + $nodeType->name ); $descendantNodeAggregateIds = $command->tetheredDescendantNodeAggregateIds->completeForNodeOfType( @@ -94,25 +94,24 @@ private function handleCreateRootNodeAggregateWithNode( $events = [ $this->createRootWithNode( $command, - $contentStreamId, + $contentGraph->getContentStreamId(), $this->getAllowedDimensionSubspace() ) ]; foreach ($this->getInterDimensionalVariationGraph()->getRootGeneralizations() as $rootGeneralization) { array_push($events, ...iterator_to_array($this->handleTetheredRootChildNodes( - $contentStreamId, + $contentGraph->getContentStreamId(), $nodeType, OriginDimensionSpacePoint::fromDimensionSpacePoint($rootGeneralization), $this->getInterDimensionalVariationGraph()->getSpecializationSet($rootGeneralization, true), $command->nodeAggregateId, $command->tetheredDescendantNodeAggregateIds, - null, - $contentRepository + null ))); } - $contentStreamEventStream = ContentStreamEventStreamName::fromContentStreamId($contentStreamId); + $contentStreamEventStream = ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()); return new EventsToPublish( $contentStreamEventStream->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( @@ -143,14 +142,13 @@ private function createRootWithNode( */ private function handleUpdateRootNodeAggregateDimensions( UpdateRootNodeAggregateDimensions $command, - ContentRepository $contentRepository + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); if (!$nodeAggregate->classification->isRoot()) { throw new NodeAggregateIsNotRoot('The node aggregate ' . $nodeAggregate->nodeAggregateId->value . ' is not classified as root, but should be for command UpdateRootNodeAggregateDimensions.', 1678647355); @@ -158,14 +156,14 @@ private function handleUpdateRootNodeAggregateDimensions( $events = Events::with( new RootNodeAggregateDimensionsWereUpdated( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $this->getAllowedDimensionSubspace() ) ); $contentStreamEventStream = ContentStreamEventStreamName::fromContentStreamId( - $contentStreamId + $contentGraph->getContentStreamId() ); return new EventsToPublish( $contentStreamEventStream->getEventStreamName(), @@ -188,8 +186,7 @@ private function handleTetheredRootChildNodes( DimensionSpacePointSet $coveredDimensionSpacePoints, NodeAggregateId $parentNodeAggregateId, NodeAggregateIdsByNodePaths $nodeAggregateIdsByNodePath, - ?NodePath $nodePath, - ContentRepository $contentRepository, + ?NodePath $nodePath ): Events { $events = []; foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $rawNodeName => $childNodeType) { @@ -220,8 +217,7 @@ private function handleTetheredRootChildNodes( $coveredDimensionSpacePoints, $childNodeAggregateId, $nodeAggregateIdsByNodePath, - $childNodePath, - $contentRepository + $childNodePath ))); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php index 05f3c58248e..d16e300dfe5 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php @@ -14,7 +14,7 @@ * source code. */ -use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\DimensionSpace; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; @@ -36,11 +36,11 @@ trait SubtreeTagging abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\InterDimensionalVariationGraph; - private function handleTagSubtree(TagSubtree $command, ContentRepository $contentRepository): EventsToPublish + private function handleTagSubtree(TagSubtree $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); - $nodeAggregate = $this->requireProjectedNodeAggregate($contentStreamId, $command->nodeAggregateId, $contentRepository); + $nodeAggregate = $this->requireProjectedNodeAggregate($contentGraph, $command->nodeAggregateId); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, $command->coveredDimensionSpacePoint @@ -60,7 +60,7 @@ private function handleTagSubtree(TagSubtree $command, ContentRepository $conten $events = Events::with( new SubtreeWasTagged( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, $command->tag, @@ -68,7 +68,7 @@ private function handleTagSubtree(TagSubtree $command, ContentRepository $conten ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId()) ->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand( $command, @@ -78,14 +78,13 @@ private function handleTagSubtree(TagSubtree $command, ContentRepository $conten ); } - public function handleUntagSubtree(UntagSubtree $command, ContentRepository $contentRepository): EventsToPublish + public function handleUntagSubtree(UntagSubtree $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { - $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); $nodeAggregate = $this->requireProjectedNodeAggregate( - $contentStreamId, - $command->nodeAggregateId, - $contentRepository + $contentGraph, + $command->nodeAggregateId ); $this->requireNodeAggregateToCoverDimensionSpacePoint( $nodeAggregate, @@ -106,7 +105,7 @@ public function handleUntagSubtree(UntagSubtree $command, ContentRepository $con $events = Events::with( new SubtreeWasUntagged( - $contentStreamId, + $contentGraph->getContentStreamId(), $command->nodeAggregateId, $affectedDimensionSpacePoints, $command->tag, @@ -114,7 +113,7 @@ public function handleUntagSubtree(UntagSubtree $command, ContentRepository $con ); return new EventsToPublish( - ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + ContentStreamEventStreamName::fromContentStreamId($contentGraph->getContentStreamId())->getEventStreamName(), NodeAggregateEventPublisher::enrichWithCommand($command, $events), ExpectedVersion::ANY() ); diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index f03b3a20633..d4de04228a2 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandler\CommandResult; +use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; use Neos\ContentRepository\Core\EventStore\EventInterface; @@ -24,7 +25,6 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; -use Neos\ContentRepository\Core\Feature\Common\ContentStreamIdOverride; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\PublishableToOtherContentStreamsInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -68,6 +68,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; +use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; @@ -97,21 +98,21 @@ public function canHandle(CommandInterface $command): bool return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, ContentRepository $contentRepository): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { /** @phpstan-ignore-next-line */ return match ($command::class) { - CreateWorkspace::class => $this->handleCreateWorkspace($command, $contentRepository), - RenameWorkspace::class => $this->handleRenameWorkspace($command, $contentRepository), - CreateRootWorkspace::class => $this->handleCreateRootWorkspace($command, $contentRepository), - PublishWorkspace::class => $this->handlePublishWorkspace($command, $contentRepository), - RebaseWorkspace::class => $this->handleRebaseWorkspace($command, $contentRepository), - PublishIndividualNodesFromWorkspace::class => $this->handlePublishIndividualNodesFromWorkspace($command, $contentRepository), - DiscardIndividualNodesFromWorkspace::class => $this->handleDiscardIndividualNodesFromWorkspace($command, $contentRepository), - DiscardWorkspace::class => $this->handleDiscardWorkspace($command, $contentRepository), - DeleteWorkspace::class => $this->handleDeleteWorkspace($command, $contentRepository), - ChangeWorkspaceOwner::class => $this->handleChangeWorkspaceOwner($command, $contentRepository), - ChangeBaseWorkspace::class => $this->handleChangeBaseWorkspace($command, $contentRepository), + CreateWorkspace::class => $this->handleCreateWorkspace($command, $commandHandlingDependencies), + RenameWorkspace::class => $this->handleRenameWorkspace($command, $commandHandlingDependencies), + CreateRootWorkspace::class => $this->handleCreateRootWorkspace($command, $commandHandlingDependencies), + PublishWorkspace::class => $this->handlePublishWorkspace($command, $commandHandlingDependencies), + RebaseWorkspace::class => $this->handleRebaseWorkspace($command, $commandHandlingDependencies), + PublishIndividualNodesFromWorkspace::class => $this->handlePublishIndividualNodesFromWorkspace($command, $commandHandlingDependencies), + DiscardIndividualNodesFromWorkspace::class => $this->handleDiscardIndividualNodesFromWorkspace($command, $commandHandlingDependencies), + DiscardWorkspace::class => $this->handleDiscardWorkspace($command, $commandHandlingDependencies), + DeleteWorkspace::class => $this->handleDeleteWorkspace($command, $commandHandlingDependencies), + ChangeWorkspaceOwner::class => $this->handleChangeWorkspaceOwner($command, $commandHandlingDependencies), + ChangeBaseWorkspace::class => $this->handleChangeBaseWorkspace($command, $commandHandlingDependencies), }; } @@ -123,17 +124,11 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo */ private function handleCreateWorkspace( CreateWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $existingWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($command->workspaceName); - if ($existingWorkspace !== null) { - throw new WorkspaceAlreadyExists(sprintf( - 'The workspace %s already exists', - $command->workspaceName->value - ), 1505830958921); - } + $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); - $baseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($command->baseWorkspaceName); + $baseWorkspace = $commandHandlingDependencies->getWorkspaceFinder()->findOneByName($command->baseWorkspaceName); if ($baseWorkspace === null) { throw new BaseWorkspaceDoesNotExist(sprintf( 'The workspace %s (base workspace of %s) does not exist', @@ -142,11 +137,12 @@ private function handleCreateWorkspace( ), 1513890708); } + $baseWorkspaceContentGraph = $commandHandlingDependencies->getContentGraph($command->baseWorkspaceName); // When the workspace is created, we first have to fork the content stream - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $command->newContentStreamId, - $baseWorkspace->currentContentStreamId, + $baseWorkspaceContentGraph->getContentStreamId(), ) )->block(); @@ -171,9 +167,11 @@ private function handleCreateWorkspace( /** * @throws WorkspaceDoesNotExist */ - private function handleRenameWorkspace(RenameWorkspace $command, ContentRepository $contentRepository): EventsToPublish - { - $this->requireWorkspace($command->workspaceName, $contentRepository); + private function handleRenameWorkspace( + RenameWorkspace $command, + CommandHandlingDependencies $commandHandlingDependencies + ): EventsToPublish { + $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); $events = Events::with( new WorkspaceWasRenamed( @@ -198,18 +196,12 @@ private function handleRenameWorkspace(RenameWorkspace $command, ContentReposito */ private function handleCreateRootWorkspace( CreateRootWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $existingWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($command->workspaceName); - if ($existingWorkspace !== null) { - throw new WorkspaceAlreadyExists(sprintf( - 'The workspace %s already exists', - $command->workspaceName->value - ), 1505848624450); - } + $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); $newContentStreamId = $command->newContentStreamId; - $contentRepository->handle( + $commandHandlingDependencies->handle( CreateContentStream::create( $newContentStreamId, ) @@ -242,10 +234,10 @@ private function handleCreateRootWorkspace( */ private function handlePublishWorkspace( PublishWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); + $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies->getWorkspaceFinder()); $this->publishContentStream( $workspace->currentContentStreamId, @@ -253,7 +245,7 @@ private function handlePublishWorkspace( )?->block(); // After publishing a workspace, we need to again fork from Base. - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $command->newContentStreamId, $baseWorkspace->currentContentStreamId, @@ -269,6 +261,7 @@ private function handlePublishWorkspace( $workspace->currentContentStreamId, ) ); + // if we got so far without an Exception, we can switch the Workspace's active Content stream. return new EventsToPublish( $streamName, @@ -356,25 +349,25 @@ private function publishContentStream( */ private function handleRebaseWorkspace( RebaseWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); + $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies->getWorkspaceFinder()); $oldWorkspaceContentStreamId = $workspace->currentContentStreamId; - $oldWorkspaceContentStreamIdState = $contentRepository->getContentStreamFinder() + $oldWorkspaceContentStreamIdState = $commandHandlingDependencies->getContentStreamFinder() ->findStateForContentStream($oldWorkspaceContentStreamId); if ($oldWorkspaceContentStreamIdState === null) { throw new \DomainException('Cannot rebase a workspace with a stateless content stream', 1711718314); } // 0) close old content stream - $contentRepository->handle( + $commandHandlingDependencies->handle( CloseContentStream::create($oldWorkspaceContentStreamId) )->block(); // 1) fork a new content stream $rebasedContentStreamId = $command->rebasedContentStreamId; - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $command->rebasedContentStreamId, $baseWorkspace->currentContentStreamId, @@ -389,13 +382,15 @@ private function handleRebaseWorkspace( // 2) extract the commands from the to-be-rebased content stream; and applies them on the new content stream $originalCommands = $this->extractCommandsFromContentStreamMetadata($workspaceContentStreamName); $commandsThatFailed = new CommandsThatFailedDuringRebase(); - ContentStreamIdOverride::applyContentStreamIdToClosure( + $commandHandlingDependencies->overrideContentStreamId( + $command->workspaceName, $command->rebasedContentStreamId, - function () use ($originalCommands, $contentRepository, &$commandsThatFailed): void { + function () use ($originalCommands, $commandHandlingDependencies, &$commandsThatFailed): void { foreach ($originalCommands as $sequenceNumber => $originalCommand) { // We no longer need to adjust commands as the workspace stays the same try { - $contentRepository->handle($originalCommand)->block(); + $commandHandlingDependencies->handle($originalCommand)->block(); + // if we came this far, we know the command was applied successfully. } catch (\Exception $e) { $commandsThatFailed = $commandsThatFailed->add( new CommandThatFailedDuringRebase( @@ -424,23 +419,23 @@ function () use ($originalCommands, $contentRepository, &$commandsThatFailed): v $events, ExpectedVersion::ANY() ); - } else { - // 3.E) In case of an exception, reopen the old content stream... - $contentRepository->handle( - ReopenContentStream::create( - $oldWorkspaceContentStreamId, - $oldWorkspaceContentStreamIdState, - ) - )->block(); + } - // ... remove the newly created one... - $contentRepository->handle(RemoveContentStream::create( - $rebasedContentStreamId - ))->block(); + // 3.E) In case of an exception, reopen the old content stream... + $commandHandlingDependencies->handle( + ReopenContentStream::create( + $oldWorkspaceContentStreamId, + $oldWorkspaceContentStreamIdState, + ) + )->block(); - // ...and throw an exception that contains all the information about what exactly failed - throw new WorkspaceRebaseFailed($commandsThatFailed, 'Rebase failed', 1711713880); - } + // ... remove the newly created one... + $commandHandlingDependencies->handle(RemoveContentStream::create( + $rebasedContentStreamId + ))->block(); + + // ...and throw an exception that contains all the information about what exactly failed + throw new WorkspaceRebaseFailed($commandsThatFailed, 'Rebase failed', 1711713880); } /** @@ -488,19 +483,20 @@ private function extractCommandsFromContentStreamMetadata( */ private function handlePublishIndividualNodesFromWorkspace( PublishIndividualNodesFromWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); $oldWorkspaceContentStreamId = $workspace->currentContentStreamId; - $oldWorkspaceContentStreamIdState = $contentRepository->getContentStreamFinder()->findStateForContentStream($oldWorkspaceContentStreamId); + $oldWorkspaceContentStreamIdState = $commandHandlingDependencies->getContentStreamFinder()->findStateForContentStream($oldWorkspaceContentStreamId); if ($oldWorkspaceContentStreamIdState === null) { throw new \DomainException('Cannot publish nodes on a workspace with a stateless content stream', 1710410114); } - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies->getWorkspaceFinder()); // 1) close old content stream - $contentRepository->handle( - CloseContentStream::create($oldWorkspaceContentStreamId) + $commandHandlingDependencies->handle( + CloseContentStream::create($contentGraph->getContentStreamId()) ); // 2) separate commands in two parts - the ones MATCHING the nodes from the command, and the REST @@ -512,7 +508,7 @@ private function handlePublishIndividualNodesFromWorkspace( /** @var array $remainingCommands */ // 3) fork a new contentStream, based on the base WS, and apply MATCHING - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $command->contentStreamIdForMatchingPart, $baseWorkspace->currentContentStreamId, @@ -521,9 +517,10 @@ private function handlePublishIndividualNodesFromWorkspace( try { // 4) using the new content stream, apply the matching commands - ContentStreamIdOverride::applyContentStreamIdToClosure( + $commandHandlingDependencies->overrideContentStreamId( + $baseWorkspace->workspaceName, $command->contentStreamIdForMatchingPart, - function () use ($matchingCommands, $contentRepository, $baseWorkspace): void { + function () use ($matchingCommands, $commandHandlingDependencies, $baseWorkspace): void { foreach ($matchingCommands as $matchingCommand) { if (!($matchingCommand instanceof RebasableToOtherWorkspaceInterface)) { throw new \RuntimeException( @@ -532,7 +529,7 @@ function () use ($matchingCommands, $contentRepository, $baseWorkspace): void { ); } - $contentRepository->handle($matchingCommand->createCopyForWorkspace( + $commandHandlingDependencies->handle($matchingCommand->createCopyForWorkspace( $baseWorkspace->workspaceName, ))->block(); } @@ -546,37 +543,39 @@ function () use ($matchingCommands, $contentRepository, $baseWorkspace): void { )?->block(); // 6) fork a new content stream, based on the base WS, and apply REST - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $command->contentStreamIdForRemainingPart, $baseWorkspace->currentContentStreamId ) )->block(); + // 7) apply REMAINING commands to the workspace's new content stream - ContentStreamIdOverride::applyContentStreamIdToClosure( + $commandHandlingDependencies->overrideContentStreamId( + $command->workspaceName, $command->contentStreamIdForRemainingPart, - function () use ($contentRepository, $remainingCommands) { + function () use ($commandHandlingDependencies, $remainingCommands) { foreach ($remainingCommands as $remainingCommand) { - $contentRepository->handle($remainingCommand)->block(); + $commandHandlingDependencies->handle($remainingCommand)->block(); } } ); } catch (\Exception $exception) { // 4.E) In case of an exception, reopen the old content stream and remove the newly created - $contentRepository->handle( + $commandHandlingDependencies->handle( ReopenContentStream::create( $oldWorkspaceContentStreamId, $oldWorkspaceContentStreamIdState, ) )->block(); - $contentRepository->handle(RemoveContentStream::create( + $commandHandlingDependencies->handle(RemoveContentStream::create( $command->contentStreamIdForMatchingPart ))->block(); try { - $contentRepository->handle(RemoveContentStream::create( + $commandHandlingDependencies->handle(RemoveContentStream::create( $command->contentStreamIdForRemainingPart ))->block(); } catch (ContentStreamDoesNotExistYet $contentStreamDoesNotExistYet) { @@ -588,10 +587,10 @@ function () use ($contentRepository, $remainingCommands) { // 8) to avoid dangling content streams, we need to remove our temporary content stream (whose events // have already been published) as well as the old one - $contentRepository->handle(RemoveContentStream::create( + $commandHandlingDependencies->handle(RemoveContentStream::create( $command->contentStreamIdForMatchingPart )); - $contentRepository->handle(RemoveContentStream::create( + $commandHandlingDependencies->handle(RemoveContentStream::create( $oldWorkspaceContentStreamId )); @@ -624,18 +623,19 @@ function () use ($contentRepository, $remainingCommands) { */ private function handleDiscardIndividualNodesFromWorkspace( DiscardIndividualNodesFromWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $oldWorkspaceContentStreamId = $workspace->currentContentStreamId; - $oldWorkspaceContentStreamIdState = $contentRepository->getContentStreamFinder()->findStateForContentStream($oldWorkspaceContentStreamId); + $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); + $oldWorkspaceContentStreamId = $contentGraph->getContentStreamId(); + $oldWorkspaceContentStreamIdState = $commandHandlingDependencies->getContentStreamFinder()->findStateForContentStream($contentGraph->getContentStreamId()); if ($oldWorkspaceContentStreamIdState === null) { throw new \DomainException('Cannot discard nodes on a workspace with a stateless content stream', 1710408112); } - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies->getWorkspaceFinder()); // 1) close old content stream - $contentRepository->handle( + $commandHandlingDependencies->handle( CloseContentStream::create($oldWorkspaceContentStreamId) )->block(); @@ -648,7 +648,7 @@ private function handleDiscardIndividualNodesFromWorkspace( $this->separateMatchingAndRemainingCommands($command, $workspace, $commandsToDiscard, $commandsToKeep); // 3) fork a new contentStream, based on the base WS, and apply the commands to keep - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $command->newContentStreamId, $baseWorkspace->currentContentStreamId, @@ -657,9 +657,10 @@ private function handleDiscardIndividualNodesFromWorkspace( // 4) using the new content stream, apply the commands to keep try { - ContentStreamIdOverride::applyContentStreamIdToClosure( + $commandHandlingDependencies->overrideContentStreamId( + $baseWorkspace->workspaceName, $command->newContentStreamId, - function () use ($commandsToKeep, $contentRepository, $baseWorkspace): void { + function () use ($commandsToKeep, $commandHandlingDependencies, $baseWorkspace): void { foreach ($commandsToKeep as $matchingCommand) { if (!($matchingCommand instanceof RebasableToOtherWorkspaceInterface)) { throw new \RuntimeException( @@ -668,7 +669,7 @@ function () use ($commandsToKeep, $contentRepository, $baseWorkspace): void { ); } - $contentRepository->handle($matchingCommand->createCopyForWorkspace( + $commandHandlingDependencies->handle($matchingCommand->createCopyForWorkspace( $baseWorkspace->workspaceName, ))->block(); } @@ -676,14 +677,14 @@ function () use ($commandsToKeep, $contentRepository, $baseWorkspace): void { ); } catch (\Exception $exception) { // 4.E) In case of an exception, reopen the old content stream and remove the newly created - $contentRepository->handle( + $commandHandlingDependencies->handle( ReopenContentStream::create( $oldWorkspaceContentStreamId, $oldWorkspaceContentStreamIdState, ) )->block(); - $contentRepository->handle(RemoveContentStream::create( + $commandHandlingDependencies->handle(RemoveContentStream::create( $command->newContentStreamId ))->block(); @@ -691,7 +692,7 @@ function () use ($commandsToKeep, $contentRepository, $baseWorkspace): void { } // 5) If everything worked, to avoid dangling content streams, we need to remove the old content stream - $contentRepository->handle(RemoveContentStream::create( + $commandHandlingDependencies->handle(RemoveContentStream::create( $oldWorkspaceContentStreamId ))->block(); @@ -767,13 +768,13 @@ private function commandMatchesAtLeastOneNode( */ private function handleDiscardWorkspace( DiscardWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); - $baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); + $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies->getWorkspaceFinder()); $newContentStream = $command->newContentStreamId; - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $newContentStream, $baseWorkspace->currentContentStreamId, @@ -809,17 +810,16 @@ private function handleDiscardWorkspace( */ private function handleChangeBaseWorkspace( ChangeBaseWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); $this->requireEmptyWorkspace($workspace); - $this->requireBaseWorkspace($workspace, $contentRepository); - - $baseWorkspace = $this->requireWorkspace($command->baseWorkspaceName, $contentRepository); + $this->requireBaseWorkspace($workspace, $commandHandlingDependencies->getWorkspaceFinder()); + $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies->getWorkspaceFinder()); - $this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace, $contentRepository); + $this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace, $commandHandlingDependencies->getWorkspaceFinder()); - $contentRepository->handle( + $commandHandlingDependencies->handle( ForkContentStream::create( $command->newContentStreamId, $baseWorkspace->currentContentStreamId, @@ -847,11 +847,11 @@ private function handleChangeBaseWorkspace( */ private function handleDeleteWorkspace( DeleteWorkspace $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); - $contentRepository->handle( + $commandHandlingDependencies->handle( RemoveContentStream::create( $workspace->currentContentStreamId ) @@ -876,9 +876,9 @@ private function handleDeleteWorkspace( */ private function handleChangeWorkspaceOwner( ChangeWorkspaceOwner $command, - ContentRepository $contentRepository, + CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireWorkspace($command->workspaceName, $contentRepository); + $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies->getWorkspaceFinder()); $events = Events::with( new WorkspaceOwnerWasChanged( @@ -895,12 +895,27 @@ private function handleChangeWorkspaceOwner( ); } + private function requireWorkspaceToNotExist(WorkspaceName $workspaceName, CommandHandlingDependencies $commandHandlingDependencies): void + { + try { + $commandHandlingDependencies->getContentGraph($workspaceName); + } catch (WorkspaceDoesNotExist) { + // Desired outcome + return; + } + + throw new WorkspaceAlreadyExists(sprintf( + 'The workspace %s already exists', + $workspaceName->value + ), 1715341085); + } + /** * @throws WorkspaceDoesNotExist */ - private function requireWorkspace(WorkspaceName $workspaceName, ContentRepository $contentRepository): Workspace + private function requireWorkspace(WorkspaceName $workspaceName, WorkspaceFinder $workspaceFinder): Workspace { - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName); + $workspace = $workspaceFinder->findOneByName($workspaceName); if (is_null($workspace)) { throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); } @@ -912,14 +927,21 @@ private function requireWorkspace(WorkspaceName $workspaceName, ContentRepositor * @throws WorkspaceHasNoBaseWorkspaceName * @throws BaseWorkspaceDoesNotExist */ - private function requireBaseWorkspace(Workspace $workspace, ContentRepository $contentRepository): Workspace - { + private function requireBaseWorkspace( + Workspace $workspace, + WorkspaceFinder $workspaceFinder + ): Workspace { if (is_null($workspace->baseWorkspaceName)) { throw WorkspaceHasNoBaseWorkspaceName::butWasSupposedTo($workspace->workspaceName); } - $baseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspace->baseWorkspaceName); - if ($baseWorkspace === null) { + try { + $baseWorkspace = $workspaceFinder->findOneByName($workspace->baseWorkspaceName); + } catch (WorkspaceDoesNotExist $_) { + $baseWorkspace = null; + } + + if (is_null($baseWorkspace)) { throw BaseWorkspaceDoesNotExist::butWasSupposedTo($workspace->workspaceName); } @@ -930,18 +952,18 @@ private function requireBaseWorkspace(Workspace $workspace, ContentRepository $c * @throws BaseWorkspaceEqualsWorkspaceException * @throws CircularRelationBetweenWorkspacesException */ - private function requireNonCircularRelationBetweenWorkspaces(Workspace $workspace, Workspace $baseWorkspace, ContentRepository $contentRepository): void + private function requireNonCircularRelationBetweenWorkspaces(Workspace $workspace, Workspace $baseWorkspace, WorkspaceFinder $workspaceFinder): void { if ($workspace->workspaceName->equals($baseWorkspace->workspaceName)) { throw new BaseWorkspaceEqualsWorkspaceException(sprintf('The base workspace of the target must be different from the given workspace "%s".', $workspace->workspaceName->value)); } $nextBaseWorkspace = $baseWorkspace; - while ($nextBaseWorkspace?->baseWorkspaceName !== null) { + while ($nextBaseWorkspace->baseWorkspaceName !== null) { if ($workspace->workspaceName->equals($nextBaseWorkspace->baseWorkspaceName)) { throw new CircularRelationBetweenWorkspacesException(sprintf('The workspace "%s" is already on the path of the target workspace "%s".', $workspace->workspaceName->value, $baseWorkspace->workspaceName->value)); } - $nextBaseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($nextBaseWorkspace->baseWorkspaceName); + $nextBaseWorkspace = $this->requireBaseWorkspace($workspace, $workspaceFinder); } } diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php b/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php index 43bb0a80d24..3b816d0446c 100644 --- a/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php +++ b/Neos.ContentRepository.Core/Classes/NodeType/NodeType.php @@ -16,13 +16,8 @@ use Neos\ContentRepository\Core\NodeType\Exception\TetheredNodeNotConfigured; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Exception\InvalidNodeTypePostprocessorException; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; -use Neos\Utility\Arrays; -use Neos\Utility\ObjectAccess; -use Neos\Utility\PositionalArraySorter; /** * A Node Type diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeNameNodeLabelGenerator.php b/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeNameNodeLabelGenerator.php deleted file mode 100644 index 9a87f7b2d6c..00000000000 --- a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeNameNodeLabelGenerator.php +++ /dev/null @@ -1,30 +0,0 @@ -nodeTypeName->value, \mb_strrpos($node->nodeTypeName->value, '.') + 1); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php index 4d8d7e4a4a7..6a5ef9a7948 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php @@ -19,11 +19,13 @@ use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateDoesNotExist; 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; /** * This is the MAIN ENTRY POINT for the Content Repository. This class exists only @@ -36,17 +38,26 @@ */ interface ContentGraphInterface extends ProjectionStateInterface { + /** + * @api + */ + public function getContentRepositoryId(): ContentRepositoryId; + + /** + * The workspace this content graph is operating on + * @api + */ + public function getWorkspaceName(): WorkspaceName; + /** * @api main API method of ContentGraph */ public function getSubgraph( - ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints ): ContentSubgraphInterface; /** - * @api * Throws exception if no root aggregate found, because a Content Repository needs at least * one root node to function. * @@ -54,9 +65,9 @@ public function getSubgraph( * as this would lead to nondeterministic results in your code. * * @throws RootNodeAggregateDoesNotExist + * @api */ public function findRootNodeAggregateByType( - ContentStreamId $contentStreamId, NodeTypeName $nodeTypeName ): NodeAggregate; @@ -64,7 +75,6 @@ public function findRootNodeAggregateByType( * @api */ public function findRootNodeAggregates( - ContentStreamId $contentStreamId, Filter\FindRootNodeAggregatesFilter $filter, ): NodeAggregates; @@ -73,7 +83,6 @@ public function findRootNodeAggregates( * @api */ public function findNodeAggregatesByType( - ContentStreamId $contentStreamId, NodeTypeName $nodeTypeName ): iterable; @@ -82,7 +91,6 @@ public function findNodeAggregatesByType( * @api */ public function findNodeAggregateById( - ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId ): ?NodeAggregate; @@ -98,7 +106,6 @@ public function findUsedNodeTypeNames(): iterable; * @internal only for consumption inside the Command Handler */ public function findParentNodeAggregateByChildOriginDimensionSpacePoint( - ContentStreamId $contentStreamId, NodeAggregateId $childNodeAggregateId, OriginDimensionSpacePoint $childOriginDimensionSpacePoint ): ?NodeAggregate; @@ -108,7 +115,6 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint( * @internal only for consumption inside the Command Handler */ public function findParentNodeAggregates( - ContentStreamId $contentStreamId, NodeAggregateId $childNodeAggregateId ): iterable; @@ -117,29 +123,24 @@ public function findParentNodeAggregates( * @internal only for consumption inside the Command Handler */ public function findChildNodeAggregates( - ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId ): iterable; /** - * A node aggregate may have multiple child node aggregates with the same name - * as long as they do not share dimension space coverage + * A node aggregate can have no or exactly one child node aggregate with a given name as enforced by constraint checks * - * @return iterable * @internal only for consumption inside the Command Handler */ - public function findChildNodeAggregatesByName( - ContentStreamId $contentStreamId, + public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable; + ): ?NodeAggregate; /** * @return iterable * @internal only for consumption inside the Command Handler */ public function findTetheredChildNodeAggregates( - ContentStreamId $contentStreamId, NodeAggregateId $parentNodeAggregateId ): iterable; @@ -147,7 +148,6 @@ public function findTetheredChildNodeAggregates( * @internal only for consumption inside the Command Handler */ public function getDimensionSpacePointsOccupiedByChildNodeName( - ContentStreamId $contentStreamId, NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, @@ -155,7 +155,12 @@ public function getDimensionSpacePointsOccupiedByChildNodeName( ): DimensionSpacePointSet; /** + * Provides the total number of projected nodes regardless of workspace or content stream. + * * @internal only for consumption in testcases */ public function countNodes(): int; + + /** @internal The content stream id where the workspace name points to for this instance */ + public function getContentStreamId(): ContentStreamId; } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphProjection.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphProjection.php index 018bcb8414d..01de943a906 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphProjection.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphProjection.php @@ -4,6 +4,7 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; +use Neos\ContentRepository\Core\ContentGraphFinder; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\Projection\CheckpointStorageInterface; use Neos\ContentRepository\Core\Projection\ProjectionInterface; @@ -12,16 +13,16 @@ use Neos\EventStore\Model\EventEnvelope; /** - * @implements ProjectionInterface + * @implements ProjectionInterface * @api people load this projection class name to access the Content Graph */ final class ContentGraphProjection implements ProjectionInterface, WithMarkStaleInterface { /** - * @param WithMarkStaleInterface&ProjectionInterface $projectionImplementation + * @param ProjectionInterface $projectionImplementation */ public function __construct( - private readonly ProjectionInterface&WithMarkStaleInterface $projectionImplementation + private readonly ProjectionInterface $projectionImplementation ) { } @@ -45,7 +46,7 @@ public function canHandle(EventInterface $event): bool return $this->projectionImplementation->canHandle($event); } - public function getState(): ContentGraphInterface + public function getState(): ContentGraphFinder { return $this->projectionImplementation->getState(); } @@ -62,6 +63,10 @@ public function getCheckpointStorage(): CheckpointStorageInterface public function markStale(): void { - $this->projectionImplementation->markStale(); + if ($this->projectionImplementation instanceof WithMarkStaleInterface) { + $this->projectionImplementation->markStale(); + } + + $this->getState()->forgetInstances(); } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php index 96ec21720d7..72510b43e62 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphWithRuntimeCaches/ContentSubgraphWithRuntimeCaches.php @@ -14,9 +14,9 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountBackReferencesFilter; @@ -34,8 +34,11 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\References; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * Wrapper for a concrete implementation of the {@see ContentSubgraphInterface} that @@ -53,9 +56,24 @@ public function __construct( $this->inMemoryCache = new InMemoryCache(); } - public function getIdentity(): ContentSubgraphIdentity + public function getContentRepositoryId(): ContentRepositoryId { - return $this->wrappedContentSubgraph->getIdentity(); + return $this->wrappedContentSubgraph->getContentRepositoryId(); + } + + public function getWorkspaceName(): WorkspaceName + { + return $this->wrappedContentSubgraph->getWorkspaceName(); + } + + public function getDimensionSpacePoint(): DimensionSpacePoint + { + return $this->wrappedContentSubgraph->getDimensionSpacePoint(); + } + + public function getVisibilityConstraints(): VisibilityConstraints + { + return $this->wrappedContentSubgraph->getVisibilityConstraints(); } public function findChildNodes(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter $filter): Nodes @@ -247,7 +265,10 @@ private static function isFilterEmpty(object $filter): bool return array_filter(get_object_vars($filter), static fn ($value) => $value !== null) === []; } - public function jsonSerialize(): ContentSubgraphIdentity + /** + * @return array + */ + public function jsonSerialize(): array { return $this->wrappedContentSubgraph->jsonSerialize(); } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphIdentity.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphIdentity.php index 40b35d78116..8c00f8e1268 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphIdentity.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphIdentity.php @@ -4,6 +4,7 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @@ -31,7 +32,8 @@ * usually need this method instead of the Origin DimensionSpacePoint inside the read model; and you'll * need the OriginDimensionSpacePoint when constructing commands on the write side. * - * @api + * @deprecated please use the {@see NodeAddress} instead {@see Node::$subgraphIdentity}. Will be removed before the Final Neos 9 release. + * @internal */ final readonly class ContentSubgraphIdentity implements \JsonSerializable { @@ -46,9 +48,6 @@ private function __construct( ) { } - /** - * @api - */ public static function create( ContentRepositoryId $contentRepositoryId, ContentStreamId $contentStreamId, diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php index 0d1f7d3f8f5..083e85cdcf2 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentSubgraphInterface.php @@ -17,9 +17,10 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Feature\RootNodeCreation\RootNodeHandling; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; 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; /** * This is the most important read model of a content repository. @@ -33,7 +34,7 @@ * From the central Content Repository instance, you can fetch the singleton * {@see ContentGraphInterface}. There, you can call * {@see ContentGraphInterface::getSubgraph()} and pass in - * the {@see ContentStreamId}, {@see DimensionSpacePoint} and + * the {@see DimensionSpacePoint} and * {@see VisibilityConstraints} you want to have. * * @@ -48,14 +49,13 @@ */ interface ContentSubgraphInterface extends \JsonSerializable { - /** - * Returns the subgraph's identity, i.e. the current perspective we look at content from, composed of - * * the content repository the subgraph belongs to - * * the ID of the content stream we are currently working in - * * the dimension space point we are currently looking at - * * the applied visibility constraints - */ - public function getIdentity(): ContentSubgraphIdentity; + public function getContentRepositoryId(): ContentRepositoryId; + + public function getWorkspaceName(): WorkspaceName; + + public function getDimensionSpacePoint(): DimensionSpacePoint; + + public function getVisibilityConstraints(): VisibilityConstraints; /** * Find a single node by its aggregate id @@ -204,5 +204,9 @@ public function retrieveNodePath(NodeAggregateId $nodeAggregateId): AbsoluteNode */ public function countNodes(): int; - public function jsonSerialize(): ContentSubgraphIdentity; + /** + * @deprecated will be removed before Neos 9 release + * @return array + */ + public function jsonSerialize(): array; } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php index a49300813e8..37edc50e81c 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php @@ -14,12 +14,17 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; 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; /** * Main read model of the {@see ContentSubgraphInterface}. @@ -27,52 +32,138 @@ * Immutable, Read Only. In case you want to modify it, you need * to create Commands and send them to ContentRepository::handle. * + * ## Identity of a Node + * + * The node's "Read Model" identity is summarized here {@see NodeAddress}, consisting of: + * + * - {@see ContentRepositoryId} + * - {@see WorkspaceName} + * - {@see DimensionSpacePoint} + * - {@see NodeAggregateId} + * + * The node address can be constructed via {@see NodeAddress::fromNode()} and serialized. + * + * ## Traversing the graph + * * The node does not have structure information, i.e. no infos * about its children. To f.e. fetch children, you need to fetch - * the subgraph {@see ContentGraphInterface::getSubgraph()} via - * $subgraphIdentity {@see Node::$subgraphIdentity}. and then - * call findChildNodes() {@see ContentSubgraphInterface::findChildNodes()} - * on the subgraph. + * the subgraph and use findChildNodes on the subgraph: + * + * $subgraph = $contentRepository->getContentGraph($node->workspaceName)->getSubgraph( + * $node->dimensionSpacePoint, + * $node->visibilityConstraints + * ); + * $childNodes = $subgraph->findChildNodes($node->aggregateId, FindChildNodesFilter::create()); + * + * ## A note about the {@see DimensionSpacePoint} and the {@see OriginDimensionSpacePoint} + * + * The {@see Node::dimensionSpacePoint} is the DimensionSpacePoint this node has been accessed in, + * and NOT the DimensionSpacePoint where the node is "at home". + * The DimensionSpacePoint where the node is (at home) is called the ORIGIN DimensionSpacePoint, + * and this can be accessed using {@see Node::originDimensionSpacePoint}. If in doubt, you'll + * usually need the DimensionSpacePoint instead of the OriginDimensionSpacePoint; + * you'll only need the OriginDimensionSpacePoint when constructing commands on the write side. * * @api Note: The constructor is not part of the public API */ final readonly class Node { /** - * @param ContentSubgraphIdentity $subgraphIdentity This is part of the node's "Read Model" identity which is defined by: {@see self::subgraphIdentity} and {@see self::nodeAggregateId}. With this information, you can fetch a Subgraph using {@see ContentGraphInterface::getSubgraph()}. - * @param NodeAggregateId $nodeAggregateId NodeAggregateId (identifier) of this node. This is part of the node's "Read Model" identity which is defined by: {@see self::subgraphIdentity} and {@see self::nodeAggregateId} + * This was intermediate part of the node's "Read Model" identity. + * Please use {@see $contentRepositoryId} {@see $workspaceName} {@see $dimensionSpacePoint} and {@see $aggregateId} instead, + * or see {@see NodeAddress::fromNode()} for passing the identity around. + * The visibility-constraints now reside in {@see $visibilityConstraints}. + * There is no replacement for the previously attached content-stream-id. Please refactor the code to use the newly available workspace-name. + * @deprecated will be removed before the final 9.0 release + */ + public ContentSubgraphIdentity $subgraphIdentity; + + /** + * In PHP please use {@see $aggregateId} instead. + * + * For Fusion please use the upcoming FlowQuery operation: + * ``` + * ${q(node).id()} + * ``` + * @deprecated will be removed before the final 9.0 release + */ + public NodeAggregateId $nodeAggregateId; + + /** + * In PHP please use {@see $name} instead. + * + * For Fusion use: + * ``` + * ${node.name.value} + * ``` + * @deprecated will be removed before the final 9.0 release + */ + public ?NodeName $nodeName; + + /** + * In PHP please fetch the NodeType via the NodeTypeManager or the NodeTypeWithFallbackProvider trait instead. + * {@see $nodeTypeName} + * + * For Fusion please use the EEL Helper: + * ``` + * ${Neos.Node.getNodeType(node)} + * ``` + * @deprecated will be removed before the final 9.0 release + */ + public ?NodeType $nodeType; + + /** + * @param ContentRepositoryId $contentRepositoryId The content-repository this Node belongs to + * @param WorkspaceName $workspaceName The workspace of this Node + * @param DimensionSpacePoint $dimensionSpacePoint DimensionSpacePoint a node has been accessed in + * @param NodeAggregateId $aggregateId NodeAggregateId (identifier) of this node. This is part of the node's "Read Model" identity, which is defined in {@see NodeAddress} * @param OriginDimensionSpacePoint $originDimensionSpacePoint The DimensionSpacePoint the node originates in. Usually needed to address a Node in a NodeAggregate in order to update it. * @param NodeAggregateClassification $classification The classification (regular, root, tethered) of this node * @param NodeTypeName $nodeTypeName The node's node type name; always set, even if unknown to the NodeTypeManager * @param NodeType|null $nodeType The node's node type, null if unknown to the NodeTypeManager - @deprecated Don't rely on this too much, as the capabilities of the NodeType here will probably change a lot; Ask the {@see NodeTypeManager} instead * @param PropertyCollection $properties All properties of this node. References are NOT part of this API; To access references, {@see ContentSubgraphInterface::findReferences()} can be used; To read the serialized properties use {@see PropertyCollection::serialized()}. - * @param NodeName|null $nodeName The optionally named hierarchy relation to the node's parent. + * @param NodeName|null $name The optional name of the node, describing its relation to its parent * @param NodeTags $tags explicit and inherited SubtreeTags of this node * @param Timestamps $timestamps Creation and modification timestamps of this node + * @param VisibilityConstraints $visibilityConstraints Information which subgraph filter was used to access this node */ private function __construct( - public ContentSubgraphIdentity $subgraphIdentity, - public NodeAggregateId $nodeAggregateId, + public ContentRepositoryId $contentRepositoryId, + public WorkspaceName $workspaceName, + public DimensionSpacePoint $dimensionSpacePoint, + public NodeAggregateId $aggregateId, public OriginDimensionSpacePoint $originDimensionSpacePoint, public NodeAggregateClassification $classification, public NodeTypeName $nodeTypeName, - public ?NodeType $nodeType, public PropertyCollection $properties, - public ?NodeName $nodeName, + public ?NodeName $name, public NodeTags $tags, public Timestamps $timestamps, + public VisibilityConstraints $visibilityConstraints, + ?NodeType $nodeType, + ContentStreamId $contentStreamId ) { - if ($this->classification->isTethered() && $this->nodeName === null) { + if ($this->classification->isTethered() && $this->name === null) { throw new \InvalidArgumentException('The NodeName must be set if the Node is tethered.', 1695118377); } + // legacy to be removed before Neos9 + $this->nodeAggregateId = $this->aggregateId; + $this->nodeName = $this->name; + $this->nodeType = $nodeType; + $this->subgraphIdentity = ContentSubgraphIdentity::create( + $contentRepositoryId, + $contentStreamId, + $dimensionSpacePoint, + $visibilityConstraints + ); } /** * @internal The signature of this method can change in the future! */ - public static function create(ContentSubgraphIdentity $subgraphIdentity, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeAggregateClassification $classification, NodeTypeName $nodeTypeName, ?NodeType $nodeType, PropertyCollection $properties, ?NodeName $nodeName, NodeTags $tags, Timestamps $timestamps): self + public static function create(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName, DimensionSpacePoint $dimensionSpacePoint, NodeAggregateId $aggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeAggregateClassification $classification, NodeTypeName $nodeTypeName, PropertyCollection $properties, ?NodeName $nodeName, NodeTags $tags, Timestamps $timestamps, VisibilityConstraints $visibilityConstraints, ?NodeType $nodeType, ContentStreamId $contentStreamId): self { - return new self($subgraphIdentity, $nodeAggregateId, $originDimensionSpacePoint, $classification, $nodeTypeName, $nodeType, $properties, $nodeName, $tags, $timestamps); + return new self($contentRepositoryId, $workspaceName, $dimensionSpacePoint, $aggregateId, $originDimensionSpacePoint, $classification, $nodeTypeName, $properties, $nodeName, $tags, $timestamps, $visibilityConstraints, $nodeType, $contentStreamId); } /** @@ -102,18 +193,35 @@ public function hasProperty(string $propertyName): bool } /** - * Returns the node label as generated by the configured node label generator + * Returned the node label as generated by the configured node label generator. + * + * In PHP please use Neos' {@see NodeLabelGeneratorInterface} instead. + * + * For Fusion please use the FlowQuery operation: + * ``` + * ${q(node).label()} + * ``` * - * @return string + * @deprecated will be removed before the final 9.0 release */ public function getLabel(): string { - return $this->nodeType?->renderNodeLabel($this) ?: $this->nodeTypeName->value; + if (!class_exists(\Neos\Neos\Domain\NodeLabel\DelegatingNodeLabelRenderer::class)) { + throw new \BadMethodCallException('node labels are removed from standalone cr.'); + } + // highly illegal + /** @phpstan-ignore-next-line */ + return (new \Neos\Neos\Domain\NodeLabel\DelegatingNodeLabelRenderer())->getLabel($this); } + /** + * Checks if the node's "Read Model" identity equals with the given one + */ public function equals(Node $other): bool { - return $this->subgraphIdentity->equals($other->subgraphIdentity) - && $this->nodeAggregateId->equals($other->nodeAggregateId); + return $this->contentRepositoryId->equals($other->contentRepositoryId) + && $this->workspaceName->equals($other->workspaceName) + && $this->dimensionSpacePoint->equals($other->dimensionSpacePoint) + && $this->aggregateId->equals($other->aggregateId); } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php index 15fa8537905..d75692f9369 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php @@ -65,6 +65,7 @@ * @param OriginByCoverage $occupationByCovered * @param DimensionSpacePointsBySubtreeTags $dimensionSpacePointsBySubtreeTags dimension space points for every subtree tag this aggregate is *explicitly* tagged with (excluding inherited tags) */ + // todo add workspace name and content repository id and remove cs id public function __construct( public ContentStreamId $contentStreamId, public NodeAggregateId $nodeAggregateId, diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeTags.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeTags.php index 8317643ab22..06794251a50 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeTags.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeTags.php @@ -23,7 +23,7 @@ * * Internally, this consists of two collection: * - One for tags that are _explicitly_ set on the respective node. - * - And one that contains tags that are _inherited_ by one of the ancestor nodes + * - And one that contains tags that are only _inherited_ by one of the ancestor nodes and _not_ explicitly set * * In most cases, it shouldn't matter whether a tag is inherited or set explicitly. But sometimes the behavior is slightly different (e.g. the "disabled" checkbox in the Neos UI inspector is only checked if the node has the `disabled` tag set explicitly) * diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php index 1ea87139400..13fc62b379a 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php @@ -14,6 +14,9 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; + /** * An immutable, type-safe collection of Node objects * @@ -194,4 +197,20 @@ public function nextAll(Node $referenceNode): self return new self(array_slice($this->nodes, $referenceNodeIndex + 1)); } + + /** + * @param \Closure(Node $node): mixed $callback + * @return array + */ + public function map(\Closure $callback): array + { + return array_map($callback, $this->nodes); + } + + public function toNodeAggregateIds(): NodeAggregateIds + { + return NodeAggregateIds::create(...$this->map( + fn (Node $node): NodeAggregateId => $node->nodeAggregateId, + )); + } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php index b3d6f466c7e..7462e28a4e1 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php @@ -39,6 +39,10 @@ public function getHash(): string return md5(implode('|', $this->tagConstraints->toStringArray())); } + /** + * A non restricted subgraph can find all nodes without filtering. + * Disabled nodes are this way also findable. + */ public static function withoutRestrictions(): self { return new self(SubtreeTags::createEmpty()); diff --git a/Neos.ContentRepository.Core/Classes/Projection/Projections.php b/Neos.ContentRepository.Core/Classes/Projection/Projections.php index e9910356908..0bc7dffec20 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/Projections.php +++ b/Neos.ContentRepository.Core/Classes/Projection/Projections.php @@ -10,7 +10,7 @@ * @implements \IteratorAggregate * @internal */ -final class Projections implements \IteratorAggregate +final class Projections implements \IteratorAggregate, \Countable { /** * @var array>, ProjectionInterface> @@ -93,4 +93,9 @@ public function getIterator(): \Traversable { yield from $this->projections; } + + public function count(): int + { + return count($this->projections); + } } diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php index 0b65f505f1c..5a138b0df7e 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryBootstrapper.php @@ -70,9 +70,7 @@ public function getOrCreateRootNodeAggregate( NodeTypeName $rootNodeTypeName ): NodeAggregateId { try { - $contentStreamId = $workspace->currentContentStreamId; - return $this->contentRepository->getContentGraph()->findRootNodeAggregateByType( - $contentStreamId, + return $this->contentRepository->getContentGraph($workspace->workspaceName)->findRootNodeAggregateByType( $rootNodeTypeName )->nodeAggregateId; diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsNoChild.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsNoChild.php new file mode 100644 index 00000000000..80807f60f97 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsNoChild.php @@ -0,0 +1,41 @@ +value + . '" is no child of node aggregate "' . $parentNodeAggregateId->value + . '" but was expected to be in dimension space point ' + . json_encode($dimensionSpacePoint, JSON_THROW_ON_ERROR), + 1713081351 + ); + } +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsNoSibling.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsNoSibling.php new file mode 100644 index 00000000000..9835d927baa --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeAggregateIsNoSibling.php @@ -0,0 +1,38 @@ +value + . '" is no sibling of node aggregate "' . $referenceNodeAggregateId->value + . '" but was expected to be in dimension space point ' + . json_encode($dimensionSpacePoint, JSON_THROW_ON_ERROR), + 1713081020 + ); + } +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php deleted file mode 100644 index a457d6d1fa9..00000000000 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php +++ /dev/null @@ -1,23 +0,0 @@ -getContentGraph($nodeAddress->workspaceName)->getSubgraph( + * $nodeAddress->dimensionSpacePoint, + * VisibilityConstraints::withoutRestrictions() + * ); + * $node = $subgraph->findNodeById($nodeAddress->aggregateId); + * + * @api + */ +final readonly class NodeAddress implements \JsonSerializable +{ + private function __construct( + public ContentRepositoryId $contentRepositoryId, + public WorkspaceName $workspaceName, + public DimensionSpacePoint $dimensionSpacePoint, + public NodeAggregateId $aggregateId, + ) { + } + + public static function create( + ContentRepositoryId $contentRepositoryId, + WorkspaceName $workspaceName, + DimensionSpacePoint $dimensionSpacePoint, + NodeAggregateId $aggregateId, + ): self { + return new self($contentRepositoryId, $workspaceName, $dimensionSpacePoint, $aggregateId); + } + + public static function fromNode(Node $node): self + { + return new self( + $node->contentRepositoryId, + $node->workspaceName, + $node->dimensionSpacePoint, + $node->nodeAggregateId + ); + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + ContentRepositoryId::fromString($array['contentRepositoryId']), + WorkspaceName::fromString($array['workspaceName']), + DimensionSpacePoint::fromArray($array['dimensionSpacePoint']), + NodeAggregateId::fromString($array['aggregateId']) + ); + } + + public static function fromJsonString(string $jsonString): self + { + return self::fromArray(\json_decode($jsonString, true, JSON_THROW_ON_ERROR)); + } + + public function withAggregateId(NodeAggregateId $aggregateId): self + { + return new self($this->contentRepositoryId, $this->workspaceName, $this->dimensionSpacePoint, $aggregateId); + } + + public function equals(self $other): bool + { + return $this->contentRepositoryId->equals($other->contentRepositoryId) + && $this->workspaceName->equals($other->workspaceName) + && $this->dimensionSpacePoint->equals($other->dimensionSpacePoint) + && $this->aggregateId->equals($other->aggregateId); + } + + public function toJson(): string + { + try { + return json_encode($this, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to JSON-encode NodeAddress: %s', $e->getMessage()), 1715608338, $e); + } + } + + public function jsonSerialize(): mixed + { + return get_object_vars($this); + } +} diff --git a/Neos.ContentRepository.Core/Tests/Functional/Domain/Fixtures/TestNodePostprocessor.php b/Neos.ContentRepository.Core/Tests/Functional/Domain/Fixtures/TestNodePostprocessor.php deleted file mode 100644 index 4ed06672849..00000000000 --- a/Neos.ContentRepository.Core/Tests/Functional/Domain/Fixtures/TestNodePostprocessor.php +++ /dev/null @@ -1,36 +0,0 @@ -isOfType('Neos.ContentRepository.Testing:NodeTypeWithProcessor')) { - $someOption = isset($options['someOption']) ? $options['someOption'] : ''; - $someOtherOption = isset($options['someOtherOption']) ? $options['someOtherOption'] : ''; - $configuration['properties']['test1']['defaultValue'] = sprintf('The value of "someOption" is "%s", the value of "someOtherOption" is "%s"', $someOption, $someOtherOption); - } - } -} diff --git a/Neos.ContentRepository.Core/Tests/Functional/Domain/Fixtures/TestObjectForSerialization.php b/Neos.ContentRepository.Core/Tests/Functional/Domain/Fixtures/TestObjectForSerialization.php deleted file mode 100644 index ca8d3ef0fbb..00000000000 --- a/Neos.ContentRepository.Core/Tests/Functional/Domain/Fixtures/TestObjectForSerialization.php +++ /dev/null @@ -1,47 +0,0 @@ -value = $value; - } - - /** - * @return object - */ - public function getValue() - { - return $this->value; - } - - /** - * @return array - */ - public function __sleep() - { - return ['value']; - } -} diff --git a/Neos.ContentRepository.Core/Tests/Functional/Fixtures/NodeStructure.xml b/Neos.ContentRepository.Core/Tests/Functional/Fixtures/NodeStructure.xml deleted file mode 100644 index f2baf33b053..00000000000 --- a/Neos.ContentRepository.Core/Tests/Functional/Fixtures/NodeStructure.xml +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Neos.ContentRepository.Core/Tests/Functional/Fixtures/NodesWithAndWithoutDimensions.xml b/Neos.ContentRepository.Core/Tests/Functional/Fixtures/NodesWithAndWithoutDimensions.xml deleted file mode 100644 index cc2515be305..00000000000 --- a/Neos.ContentRepository.Core/Tests/Functional/Fixtures/NodesWithAndWithoutDimensions.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - diff --git a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php index 45a346cd05e..7d3df5c6c46 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php @@ -12,7 +12,6 @@ * source code. */ -use Neos\ContentRepository\Core\NodeType\DefaultNodeLabelGeneratorFactory; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException; @@ -129,7 +128,7 @@ class NodeTypeTest extends TestCase */ public function aNodeTypeHasAName() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Text'), [], [], new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Text'), [], []); self::assertSame('Neos.ContentRepository.Testing:Text', $nodeType->name->value); } @@ -147,7 +146,7 @@ public function aNodeTypeMustHaveDistinctNamesForPropertiesReferences() 'references' => [ 'foo' => [] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); $this->expectException(NodeConfigurationException::class); $this->expectExceptionCode(1708022344); // initialize the node type @@ -165,12 +164,14 @@ public function aNodeTypeMustHaveDistinctNamesForPropertiesReferencesInInheritan 'type' => 'string', ] ] - ], new DefaultNodeLabelGeneratorFactory()); - $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Invalid'), ['ContentRepository:Super' => $superNodeType], [ - 'references' => [ - 'foo' => [] - ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Invalid'), + ['ContentRepository:Super' => $superNodeType], + [ + 'references' => [ + 'foo' => [] + ] + ]); $this->expectException(NodeConfigurationException::class); $this->expectExceptionCode(1708022344); // initialize the node type @@ -182,11 +183,11 @@ public function aNodeTypeMustHaveDistinctNamesForPropertiesReferencesInInheritan */ public function nodeTypesCanHaveAnyNumberOfSuperTypes() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], []); $timeableNodeType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:TimeableContent'), - [], [], new DefaultNodeLabelGeneratorFactory() + [], [] ); $documentType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:Document'), @@ -194,12 +195,12 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() 'Neos.ContentRepository:Base' => $baseType, 'Neos.ContentRepository.Testing:TimeableContent' => $timeableNodeType, ], - [], new DefaultNodeLabelGeneratorFactory() + [] ); $hideableNodeType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:HideableContent'), - [], [], new DefaultNodeLabelGeneratorFactory() + [], [] ); $pageType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:Page'), @@ -208,8 +209,7 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() 'Neos.ContentRepository.Testing:HideableContent' => $hideableNodeType, 'Neos.ContentRepository.Testing:TimeableContent' => null, ], - [], - new DefaultNodeLabelGeneratorFactory() + [] ); self::assertEquals( @@ -234,7 +234,7 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() */ public function labelIsEmptyStringByDefault() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], []); self::assertSame('', $baseType->getLabel()); } @@ -243,7 +243,7 @@ public function labelIsEmptyStringByDefault() */ public function propertiesAreEmptyArrayByDefault() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], []); self::assertSame([], $baseType->getProperties()); } @@ -256,7 +256,7 @@ public function hasConfigurationReturnsTrueIfSpecifiedConfigurationPathExists() 'someKey' => [ 'someSubKey' => 'someValue' ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); self::assertTrue($nodeType->hasConfiguration('someKey.someSubKey')); } @@ -265,7 +265,7 @@ public function hasConfigurationReturnsTrueIfSpecifiedConfigurationPathExists() */ public function hasConfigurationReturnsFalseIfSpecifiedConfigurationPathDoesNotExist() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], []); self::assertFalse($nodeType->hasConfiguration('some.nonExisting.path')); } @@ -278,7 +278,7 @@ public function getConfigurationReturnsTheConfigurationWithTheSpecifiedPath() 'someKey' => [ 'someSubKey' => 'someValue' ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); self::assertSame('someValue', $nodeType->getConfiguration('someKey.someSubKey')); } @@ -287,7 +287,7 @@ public function getConfigurationReturnsTheConfigurationWithTheSpecifiedPath() */ public function getConfigurationReturnsNullIfTheSpecifiedPathDoesNotExist() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], []); self::assertNull($nodeType->getConfiguration('some.nonExisting.path')); } @@ -403,7 +403,7 @@ public function propertyDeclaration() 'defaultValue' => false ] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); self::assertTrue($nodeType->hasProperty('someProperty')); self::assertFalse($nodeType->hasReference('someProperty')); self::assertSame('bool', $nodeType->getPropertyType('someProperty')); @@ -429,7 +429,7 @@ public function getPropertyTypeThrowsOnInvalidProperty() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(1708025421); - $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], [], new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('ContentRepository:Node'), [], []); $nodeType->getPropertyType('nonExistent'); self::assertSame('string', $nodeType->getPropertyType('nonExistent')); } @@ -443,7 +443,7 @@ public function getPropertyTypeFallback() 'properties' => [ 'someProperty' => [] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); self::assertSame('string', $nodeType->getPropertyType('someProperty')); } @@ -466,7 +466,7 @@ public function getDefaultValuesForPropertiesIgnoresNullAndUnset() 'type' => 'string' ] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); self::assertSame(['someProperty' => 'lol'], $nodeType->getDefaultValuesForProperties()); } @@ -479,7 +479,7 @@ public function referencesDeclaration() 'references' => [ 'someReferences' => [] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); self::assertFalse($nodeType->hasProperty('someReferences')); self::assertTrue($nodeType->hasReference('someReferences')); self::assertThrows(fn() => $nodeType->getPropertyType('someReferences'), \InvalidArgumentException::class); @@ -506,7 +506,7 @@ public function legacyPropertyReferenceDeclaration() 'type' => 'reference', ] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); // will be available as _real_ reference self::assertFalse($nodeType->hasProperty('referenceProperty')); self::assertTrue($nodeType->hasReference('referenceProperty')); @@ -538,7 +538,7 @@ public function legacyPropertyReferencesDeclaration() 'type' => 'references', ] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); // will be available as _real_ reference self::assertFalse($nodeType->hasProperty('referencesProperty')); self::assertTrue($nodeType->hasReference('referencesProperty')); @@ -569,7 +569,7 @@ public function legacyPropertyReferencesDeclarationMustNotUseConstraintFeatures( ], ] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); $this->expectException(NodeConfigurationException::class); $this->expectExceptionCode(1708022344); $nodeType->getReferences(); @@ -591,7 +591,7 @@ public function legacyPropertyReferencesDeclarationMustNotUsePropertiesFeatures( ], ] ] - ], new DefaultNodeLabelGeneratorFactory()); + ]); $this->expectException(NodeConfigurationException::class); $this->expectExceptionCode(1708022344); $nodeType->getReferences(); @@ -618,8 +618,7 @@ protected function getNodeType(string $nodeTypeName): ?NodeType return new NodeType( NodeTypeName::fromString($nodeTypeName), $declaredSuperTypes, - $configuration, - new DefaultNodeLabelGeneratorFactory() + $configuration ); } diff --git a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php index 12a2152bbb6..2b1ce7fd096 100644 --- a/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php +++ b/Neos.ContentRepository.Export/Tests/Behavior/Features/Bootstrap/CrImportExportTrait.php @@ -18,7 +18,6 @@ use Behat\Gherkin\Node\TableNode; use League\Flysystem\Filesystem; use League\Flysystem\InMemory\InMemoryFilesystemAdapter; -use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -206,11 +205,4 @@ public function iExpectAMigrationErrorWithTheMessage(PyStringNode $expectedMessa * @return T */ abstract private function getObject(string $className): object; - - protected function getTableNamePrefix(): string - { - return DoctrineDbalContentGraphProjectionFactory::graphProjectionTableNamePrefix( - $this->currentContentRepository->id - ); - } } diff --git a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php index 1efa5bd062f..df2101f2dc5 100644 --- a/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php +++ b/Neos.ContentRepository.Export/src/Processors/EventStoreImportProcessor.php @@ -113,14 +113,14 @@ public function run(): ProcessorResult return ProcessorResult::error(sprintf('Failed to read events. %s is not expected in imported event stream.', $event->type)); } $domainEvent = DecoratedEvent::create($domainEvent, eventId: EventId::fromString($event->identifier), metadata: $event->metadata); - $domainEvents[] = $this->normalizeEvent($domainEvent); + $domainEvents[] = $this->eventNormalizer->normalize($domainEvent); } assert($this->contentStreamId !== null); $contentStreamStreamName = ContentStreamEventStreamName::fromContentStreamId($this->contentStreamId)->getEventStreamName(); $events = Events::with( - $this->normalizeEvent( + $this->eventNormalizer->normalize( new ContentStreamWasCreated( $this->contentStreamId, ) @@ -135,7 +135,7 @@ public function run(): ProcessorResult $workspaceName = WorkspaceName::forLive(); $workspaceStreamName = WorkspaceEventStreamName::fromWorkspaceName($workspaceName)->getEventStreamName(); $events = Events::with( - $this->normalizeEvent( + $this->eventNormalizer->normalize( new RootWorkspaceWasCreated( $workspaceName, WorkspaceTitle::fromString('live workspace'), @@ -158,29 +158,6 @@ public function run(): ProcessorResult return ProcessorResult::success(sprintf('Imported %d event%s into stream "%s"', count($domainEvents), count($domainEvents) === 1 ? '' : 's', $contentStreamStreamName->value)); } - /** - * Copied from {@see EventPersister::normalizeEvent()} - * - * @param EventInterface|DecoratedEvent $event - * @return Event - */ - private function normalizeEvent(EventInterface|DecoratedEvent $event): Event - { - $eventId = $event instanceof DecoratedEvent && $event->eventId !== null ? $event->eventId : EventId::create(); - $eventMetadata = $event instanceof DecoratedEvent ? $event->eventMetadata : null; - $causationId = $event instanceof DecoratedEvent ? $event->causationId : null; - $correlationId = $event instanceof DecoratedEvent ? $event->correlationId : null; - $event = $event instanceof DecoratedEvent ? $event->innerEvent : $event; - return new Event( - $eventId, - $this->eventNormalizer->getEventType($event), - $this->eventNormalizer->getEventData($event), - $eventMetadata, - $causationId, - $correlationId, - ); - } - /** --------------------------- */ /** diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index 4e6f49e8f66..e4a05e25127 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php @@ -15,15 +15,11 @@ use Neos\ContentRepository\Core\DimensionSpace\VariantType; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\EventStore\EventNormalizer; +use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSibling; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMappings; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMappings; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; @@ -170,11 +166,12 @@ private function resetRuntimeState(): void private function exportEvent(EventInterface $event): void { + $normalizedEvent = $this->eventNormalizer->normalize($event); $exportedEvent = new ExportedEvent( - Uuid::uuid4()->toString(), - $this->eventNormalizer->getEventType($event)->value, - json_decode($this->eventNormalizer->getEventData($event)->value, true), - [] + $normalizedEvent->id->value, + $normalizedEvent->type->value, + json_decode($normalizedEvent->data->value, true), + [], ); assert($this->eventFileResource !== null); fwrite($this->eventFileResource, $exportedEvent->toJson() . chr(10)); @@ -376,9 +373,9 @@ public function extractPropertyValuesAndReferences(array $nodeDataRow, NodeType } } - // hiddenInIndex is stored as separate column in the nodedata table, but we need it as (internal) property + // hiddenInIndex is stored as separate column in the nodedata table, but we need it as property if ($nodeDataRow['hiddeninindex']) { - $properties['_hiddenInIndex'] = true; + $properties['hiddenInMenu'] = true; } if ($nodeType->isOfType(NodeTypeName::fromString('Neos.TimeableNodeVisibility:Timeable'))) { @@ -475,23 +472,13 @@ private function createNodeVariant(NodeAggregateId $nodeAggregateId, OriginDimen $this->exportEvent(new NodeAggregateWasMoved( $this->contentStreamId, $nodeAggregateId, - OriginNodeMoveMappings::fromArray([ - new OriginNodeMoveMapping( - $originDimensionSpacePoint, - CoverageNodeMoveMappings::create( - CoverageNodeMoveMapping::createForNewSucceedingSibling( - $originDimensionSpacePoint->toDimensionSpacePoint(), - SucceedingSiblingNodeMoveDestination::create( - $parentNodeAggregate->nodeAggregateId, - $variantSourceOriginDimensionSpacePoint, - - $nodeAggregate->getVariant($variantSourceOriginDimensionSpacePoint)->parentNodeAggregateId, - $nodeAggregate->getVariant($variantSourceOriginDimensionSpacePoint)->originDimensionSpacePoint - ) - ) - ) + $parentNodeAggregate->nodeAggregateId, + new InterdimensionalSiblings( + new InterdimensionalSibling( + $originDimensionSpacePoint->toDimensionSpacePoint(), + null ) - ]) + ) )); } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php index ff9511a1011..dd5302406c3 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Bootstrap/FeatureContext.php @@ -18,6 +18,7 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Asset\AssetExporter; use Neos\ContentRepository\Export\Asset\AssetLoaderInterface; use Neos\ContentRepository\Export\Asset\ResourceLoaderInterface; @@ -123,10 +124,14 @@ public function iRunTheEventMigration(string $contentStream = null): void { $nodeTypeManager = $this->currentContentRepository->getNodeTypeManager(); $propertyMapper = $this->getObject(PropertyMapper::class); - $contentGraph = $this->currentContentRepository->getContentGraph(); - $nodeFactory = (new \ReflectionClass($contentGraph)) + $contentGraphFinder = $this->currentContentRepository->projectionState(\Neos\ContentRepository\Core\ContentGraphFinder::class); + // FIXME: Dirty + $contentGraphFactory = (new \ReflectionClass($contentGraphFinder)) + ->getProperty('contentGraphFactory') + ->getValue($contentGraphFinder); + $nodeFactory = (new \ReflectionClass($contentGraphFactory)) ->getProperty('nodeFactory') - ->getValue($contentGraph); + ->getValue($contentGraphFactory); $propertyConverter = (new \ReflectionClass($nodeFactory)) ->getProperty('propertyConverter') ->getValue($nodeFactory); diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature index 00ed5bcfe98..693b379ce3b 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature @@ -73,14 +73,14 @@ Feature: Migrating nodes with content dimensions | a1 | /sites/site/b/a1 | Some.Package:Thing | {"language": ["ch"]} | And I run the event migration Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site", "parentNodeAggregateId": "sites"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "parentNodeAggregateId": "site"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a1", "parentNodeAggregateId": "a", "originDimensionSpacePoint": {"language": "de"}, "succeedingSiblingsForCoverage": [{"dimensionSpacePoint":{"language": "de"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b", "parentNodeAggregateId": "site"} | - | NodeSpecializationVariantWasCreated | {"nodeAggregateId": "a1", "sourceOrigin": {"language": "de"}, "specializationOrigin": {"language": "ch"}, "specializationSiblings": [{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | - | NodeAggregateWasMoved | {"nodeAggregateId": "a1", "nodeMoveMappings": [{"movedNodeOrigin":{"language":"ch"},"newLocations":[{"coveredDimensionSpacePoint":{"language":"ch"},"newSucceedingSibling":{"nodeAggregateId":"b","originDimensionSpacePoint":{"language":"de"},"parentNodeAggregateId":"a","parentOriginDimensionSpacePoint":{"language":"de"}}}]}]} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site", "parentNodeAggregateId": "sites"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "parentNodeAggregateId": "site"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a1", "parentNodeAggregateId": "a", "originDimensionSpacePoint": {"language": "de"}, "succeedingSiblingsForCoverage": [{"dimensionSpacePoint":{"language": "de"},"nodeAggregateId":null},{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b", "parentNodeAggregateId": "site"} | + | NodeSpecializationVariantWasCreated | {"nodeAggregateId": "a1", "sourceOrigin": {"language": "de"}, "specializationOrigin": {"language": "ch"}, "specializationSiblings": [{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | + | NodeAggregateWasMoved | {"nodeAggregateId": "a1", "newParentNodeAggregateId": "b", "succeedingSiblingsForCoverage": [{"dimensionSpacePoint":{"language":"ch"},"nodeAggregateId":null}]} | Scenario: Node variant with different grand parent node (ancestor node was moved) - Note: There is only NodeAggregateWasMoved event for "a" and not for "a1" @@ -95,13 +95,13 @@ Feature: Migrating nodes with content dimensions | a1 | /sites/site/b/a/a1 | Some.Package:Thing | {"language": ["ch"]} | And I run the event migration Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site", "parentNodeAggregateId": "sites"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "parentNodeAggregateId": "site"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a1", "parentNodeAggregateId": "a", "originDimensionSpacePoint": {"language": "de"}, "succeedingSiblingsForCoverage": [{"dimensionSpacePoint":{"language": "de"},"nodeAggregateId":null}, {"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b", "parentNodeAggregateId": "site"} | - | NodeSpecializationVariantWasCreated | {"nodeAggregateId": "a", "sourceOrigin": {"language": "de"}, "specializationOrigin": {"language": "ch"}, "specializationSiblings": [{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | - | NodeAggregateWasMoved | {"nodeAggregateId": "a", "nodeMoveMappings": [{"movedNodeOrigin":{"language":"ch"},"newLocations":[{"coveredDimensionSpacePoint":{"language":"ch"},"newSucceedingSibling":{"nodeAggregateId":"b","originDimensionSpacePoint":{"language":"de"},"parentNodeAggregateId":"site","parentOriginDimensionSpacePoint":{"language":"de"}}}]}]} | - | NodeSpecializationVariantWasCreated | {"nodeAggregateId": "a1", "sourceOrigin": {"language": "de"}, "specializationOrigin": {"language": "ch"}, "specializationSiblings": [{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site", "parentNodeAggregateId": "sites"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "parentNodeAggregateId": "site"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a1", "parentNodeAggregateId": "a", "originDimensionSpacePoint": {"language": "de"}, "succeedingSiblingsForCoverage": [{"dimensionSpacePoint":{"language": "de"},"nodeAggregateId":null}, {"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b", "parentNodeAggregateId": "site"} | + | NodeSpecializationVariantWasCreated | {"nodeAggregateId": "a", "sourceOrigin": {"language": "de"}, "specializationOrigin": {"language": "ch"}, "specializationSiblings": [{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | + | NodeAggregateWasMoved | {"nodeAggregateId": "a", "newParentNodeAggregateId": "b", "succeedingSiblingsForCoverage": [{"dimensionSpacePoint":{"language":"ch"},"nodeAggregateId":null}]} | + | NodeSpecializationVariantWasCreated | {"nodeAggregateId": "a1", "sourceOrigin": {"language": "de"}, "specializationOrigin": {"language": "ch"}, "specializationSiblings": [{"dimensionSpacePoint":{"language": "ch"},"nodeAggregateId":null}]} | diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/CreateNodeHashTrait.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/CreateNodeHashTrait.php index 3e7e8b821f4..8475f3ddcbd 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/CreateNodeHashTrait.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/CreateNodeHashTrait.php @@ -9,8 +9,8 @@ trait CreateNodeHashTrait { /** - * Create a string hash containing the nodeAggregateId, cr-id, contentStream->id, dimensionSpacePoint->hash - * and visibilityConstraints->hash. To be used for ensuring uniqueness or removing nodes. + * Create a string hash containing the node-aggregateId, cr-id, workspace-name, dimensionSpacePoint-hash + * and visibilityConstraints-hash. To be used for ensuring uniqueness or removing nodes. * * @see Node::equals() for comparison */ @@ -21,10 +21,10 @@ protected function createNodeHash(Node $node): string ':', [ $node->nodeAggregateId->value, - $node->subgraphIdentity->contentRepositoryId->value, - $node->subgraphIdentity->contentStreamId->value, - $node->subgraphIdentity->dimensionSpacePoint->hash, - $node->subgraphIdentity->visibilityConstraints->getHash() + $node->contentRepositoryId->value, + $node->workspaceName->value, + $node->dimensionSpacePoint->hash, + $node->visibilityConstraints->getHash() ] ) ); diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php index 97d586a8e92..ea12e20c0b1 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php @@ -122,7 +122,7 @@ protected function getPropertyPath($element, $propertyPath) if ($propertyPath === '_identifier') { // TODO: deprecated (Neos <9 case) return $element->nodeAggregateId->value; - } elseif ($propertyPath[0] === '_' && $propertyPath !== '_hiddenInIndex') { + } elseif ($propertyPath[0] === '_') { return ObjectAccess::getPropertyPath($element, substr($propertyPath, 1)); } else { return $element->getProperty($propertyPath); diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php index ba8aa21e5ba..c3f5908b6e1 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php @@ -62,6 +62,8 @@ */ class FindOperation extends AbstractOperation { + use CreateNodeHashTrait; + /** * {@inheritdoc} * @@ -179,7 +181,7 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void $uniqueResult = []; $usedKeys = []; foreach ($result as $item) { - $identifier = $item->subgraphIdentity->contentStreamId->value . '@' . $item->subgraphIdentity->dimensionSpacePoint->hash . '@' . $item->nodeAggregateId->value; + $identifier = $this->createNodeHash($item); if (!isset($usedKeys[$identifier])) { $uniqueResult[] = $item; $usedKeys[$identifier] = $identifier; @@ -199,8 +201,8 @@ protected function getEntryPoints(array $contextNodes): array foreach ($contextNodes as $contextNode) { assert($contextNode instanceof Node); $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); - $subgraphIdentifier = md5($contextNode->subgraphIdentity->contentStreamId->value - . '@' . $contextNode->subgraphIdentity->dimensionSpacePoint->toJson()); + $subgraphIdentifier = md5($subgraph->getWorkspaceName()->value + . '@' . $subgraph->getDimensionSpacePoint()->toJson()); if (!isset($entryPoints[(string) $subgraphIdentifier])) { $entryPoints[(string) $subgraphIdentifier] = [ 'subgraph' => $subgraph, diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/IdOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/IdOperation.php new file mode 100644 index 00000000000..d98ad6ee3c8 --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/IdOperation.php @@ -0,0 +1,79 @@ + $context $context onto which this operation should be applied (array or array-like object) + * @return boolean + */ + public function canEvaluate($context): bool + { + return (isset($context[0]) && $context[0] instanceof Node); + } + + /** + * {@inheritdoc} + * + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation + * @return mixed + * @throws FlowQueryException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments) + { + if ($arguments !== []) { + throw new FlowQueryException(static::$shortName . '() does not require any argument.', 1715510778); + } + /** @var array $context */ + $context = $flowQuery->getContext(); + $node = $context[0] ?? null; + if (!$node instanceof Node) { + return null; + } + return $node->nodeAggregateId->value; + } +} diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/LabelOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/LabelOperation.php new file mode 100644 index 00000000000..37e4a02df83 --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/LabelOperation.php @@ -0,0 +1,84 @@ + $context $context onto which this operation should be applied (array or array-like object) + * @return boolean + */ + public function canEvaluate($context): bool + { + return (isset($context[0]) && $context[0] instanceof Node); + } + + /** + * {@inheritdoc} + * + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation + * @return mixed + * @throws FlowQueryException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments) + { + if ($arguments !== []) { + throw new FlowQueryException(static::$shortName . '() does not require any argument.', 1715510778); + } + /** @var array $context */ + $context = $flowQuery->getContext(); + $node = $context[0] ?? null; + if (!$node instanceof Node) { + return null; + } + return $this->nodeLabelGenerator->getLabel($node); + } +} diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NodeTypeNameOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NodeTypeNameOperation.php new file mode 100644 index 00000000000..82d5770c315 --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NodeTypeNameOperation.php @@ -0,0 +1,79 @@ + $context $context onto which this operation should be applied (array or array-like object) + * @return boolean + */ + public function canEvaluate($context): bool + { + return (isset($context[0]) && $context[0] instanceof Node); + } + + /** + * {@inheritdoc} + * + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation + * @return mixed + * @throws FlowQueryException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments) + { + if ($arguments !== []) { + throw new FlowQueryException(static::$shortName . '() does not require any argument.', 1715510778); + } + /** @var array $context */ + $context = $flowQuery->getContext(); + $node = $context[0] ?? null; + if (!$node instanceof Node) { + return null; + } + return $node->nodeTypeName->value; + } +} diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php index e1021c05e0f..31690fd9b04 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php @@ -57,8 +57,6 @@ class PropertyOperation extends AbstractOperation */ protected $contentRepositoryRegistry; - use NodeTypeWithFallbackProvider; - /** * {@inheritdoc} * @@ -113,7 +111,10 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): mixed return ObjectAccess::getPropertyPath($element, substr($propertyName, 1)); } - $referenceDefinition = $this->getNodeType($element)->referenceDefinitions->get($propertyName); + $contentRepository = $this->contentRepositoryRegistry->get($element->subgraphIdentity->contentRepositoryId); + $nodeTypeManager = $contentRepository->getNodeTypeManager(); + + $referenceDefinition = $nodeTypeManager->getNodeType($element->nodeTypeName)?->referenceDefinitions->get($propertyName); if ($referenceDefinition !== null) { // legacy access layer for references $subgraph = $this->contentRepositoryRegistry->subgraphForNode($element); diff --git a/Neos.ContentRepository.NodeAccess/Configuration/Settings.yaml b/Neos.ContentRepository.NodeAccess/Configuration/Settings.yaml deleted file mode 100644 index 34c0b75a38a..00000000000 --- a/Neos.ContentRepository.NodeAccess/Configuration/Settings.yaml +++ /dev/null @@ -1,52 +0,0 @@ -Neos: - - ContentRepository: - - # Configure available content dimensions for nodes, after adding a dimension the database has to be filled with - # the dimension default values. - # Also add named presets with fallback chains that can happen in your dimensions. - # - # Example - # - # contentDimensions: - # language: - # default: mul_ZZ - # defaultPreset: 'all' - # presets: - # 'all': - # values: ['mul_ZZ'] - # - # persona: - # default: everybody - # defaultPreset: 'all' - # presets: - # 'all': - # values: ['everybody'] - contentDimensions: { } - - # Configures defaults for node label generation - labelGenerator: - eel: - defaultContext: - String: Neos\Eel\Helper\StringHelper - Array: Neos\Eel\Helper\ArrayHelper - Date: Neos\Eel\Helper\DateHelper - Configuration: Neos\Eel\Helper\ConfigurationHelper - Math: Neos\Eel\Helper\MathHelper - Json: Neos\Eel\Helper\JsonHelper - I18n: Neos\Flow\I18n\EelHelper\TranslationHelper - q: Neos\Eel\FlowQuery\FlowQuery::q - - unstableInternalWillChangeLater: - projection: - defaultProjectorsToBeBlocked: - 'Neos\ContentRepository\Core\Projection\ContentStream\ContentStreamProjector': true - 'Neos\ContentRepository\Core\Projection\Workspace\WorkspaceProjector': true - - - NodeAccess: - nodeAccessorFactories: - contentSubgraph: - position: end - className: Neos\ContentRepository\NodeAccess\NodeAccessor\Implementation\ContentSubgraphAccessorFactory - diff --git a/Neos.ContentRepository.NodeAccess/Configuration/Testing/Behat/Settings.yaml b/Neos.ContentRepository.NodeAccess/Configuration/Testing/Behat/Settings.yaml deleted file mode 100644 index f75fc0b2009..00000000000 --- a/Neos.ContentRepository.NodeAccess/Configuration/Testing/Behat/Settings.yaml +++ /dev/null @@ -1,5 +0,0 @@ -Neos: - Flow: - persistence: - backendOptions: - driver: pdo_mysql diff --git a/Neos.ContentRepository.NodeAccess/Configuration/Testing/NodeTypes.yaml b/Neos.ContentRepository.NodeAccess/Configuration/Testing/NodeTypes.yaml deleted file mode 100644 index 2093f507f19..00000000000 --- a/Neos.ContentRepository.NodeAccess/Configuration/Testing/NodeTypes.yaml +++ /dev/null @@ -1,207 +0,0 @@ -# Node Types for Functional Tests - -'Neos.ContentRepository.Testing:NodeType': - properties: - test1: - defaultValue: 'default value 1' - test2: - defaultValue: 'default value 2' - constraints: - nodeTypes: - 'Neos.ContentRepository.Testing:NodeType': true - 'Neos.ContentRepository.Testing:NodeTypeWithSubnodes': true - -'Neos.ContentRepository.Testing:NodeTypeWithSubnodes': - childNodes: - subnode1: - type: 'Neos.ContentRepository.Testing:NodeType' - constraints: - nodeTypes: - 'Neos.ContentRepository.Testing:NodeType': true - 'Neos.ContentRepository.Testing:Document': true - -'Neos.ContentRepository.Testing:NodeTypeWithProcessor': - properties: - test1: - defaultValue: 'default value 1' - postprocessors: - 'processor1': - postprocessor: 'Neos\ContentRepository\Core\Tests\Functional\Domain\Fixtures\TestNodePostprocessor' - postprocessorOptions: - someOption: 'someOverriddenValue' - someOtherOption: 'someOtherValue' - 'processor2': - position: start - postprocessor: 'Neos\ContentRepository\Core\Tests\Functional\Domain\Fixtures\TestNodePostprocessor' - postprocessorOptions: - someOption: 'someValue' - - -'Neos.ContentRepository.Testing:NodeTypeWithReferences': - properties: - property1: - type: string - property2: - type: reference - property3: - type: references - -'Neos.ContentRepository.Testing:Headline': - superTypes: - 'Neos.ContentRepository.Testing:Content': true - properties: - title: - type: string - -'Neos.ContentRepository.Testing:NodeTypeWithEntities': - properties: - image: - type: 'Neos\Flow\Tests\Functional\Persistence\Fixtures\Image' - wrappedImage: - type: 'Neos\ContentRepository\Core\Tests\Functional\Domain\Fixtures\TestObjectForSerialization' - images: - type: 'array' - -'Neos.ContentRepository.Testing:HappyTestingNode': - class: 'Neos\ContentRepository\Core\Tests\Functional\Domain\Fixtures\HappyNode' - -# Constraint behat test nodetypes -'Neos.ContentRepository.Testing:Node': - abstract: true - -'Neos.ContentRepository.Testing:ContentCollection': - superTypes: - 'Neos.ContentRepository.Testing:Node': true - constraints: - nodeTypes: - 'Neos.ContentRepository.Testing:Headline': true - 'Neos.ContentRepository.Testing:TwoColumn': true - 'Neos.ContentRepository.Testing:ThreeColumn': true - 'Neos.ContentRepository.Testing:Text': true - 'Neos.ContentRepository.Testing:Html': true - 'Neos.ContentRepository.Testing:List': true - -'Neos.ContentRepository.Testing:Document': - aggregate: true - -'Neos.ContentRepository.Testing:Page': - superTypes: - 'Neos.ContentRepository.Testing:Document': true - properties: - title: - type: string - childNodes: - 'main': - type: 'Neos.ContentRepository.Testing:ContentCollection' - constraints: - nodeTypes: - '*': true - -'Neos.ContentRepository.Testing:Chapter': - superTypes: - 'Neos.ContentRepository.Testing:Page': true - -'Neos.ContentRepository.Testing:PageWithConfiguredLabel': - superTypes: - 'Neos.ContentRepository.Testing:Page': true - label: "${q(node).property('title') || q(node).property('text') || ((node.nodeType.label || node.nodeType.name) + ' (' + node.name + ')')}" - ui: - label: 'Labeled Page' - -'Neos.ContentRepository.Testing:Column': - superTypes: - 'Neos.ContentRepository.Testing:Content': true - 'Neos.ContentRepository.Testing:ContentMixin': false - abstract: true - -'Neos.ContentRepository.Testing:TwoColumn': - superTypes: - 'Neos.ContentRepository.Testing:Column': true - childNodes: - column0: - type: 'Neos.ContentRepository.Testing:ContentCollection' - column1: - type: 'Neos.ContentRepository.Testing:ContentCollection' - -'Neos.ContentRepository.Testing:ThreeColumn': - superTypes: - 'Neos.ContentRepository.Testing:Column': true - childNodes: - column0: - type: 'Neos.ContentRepository.Testing:ContentCollection' - column1: - type: 'Neos.ContentRepository.Testing:ContentCollection' - column2: - type: 'Neos.ContentRepository.Testing:ContentCollection' - -'Neos.ContentRepository.Testing:Content': - superTypes: - 'Neos.ContentRepository.Testing:ContentMixin': true - constraints: - nodeTypes: - '*': false - -'Neos.ContentRepository.Testing:ContentMixin': [] - -'Neos.ContentRepository.Testing:Text': - superTypes: - 'Neos.ContentRepository.Testing:Content': true - properties: - text: - type: string - -'Neos.ContentRepository.Testing:NodeTypeWithSubnodesAndConstraints': - childNodes: - subnode1: - type: 'Neos.ContentRepository.Testing:NodeType' - constraints: - nodeTypes: - 'Neos.ContentRepository.Testing:Headline': true # allowed - -'Neos.ContentRepository.Testing:Image': - superTypes: - 'Neos.ContentRepository.Testing:Content': true - -'Neos.ContentRepository.Testing:TextWithImage': - superTypes: - 'Neos.ContentRepository.Testing:Text': true - 'Neos.ContentRepository.Testing:Image': true - -'Neos.ContentRepository.Testing:Html': - superTypes: - 'Neos.ContentRepository.Testing:Content': true - properties: - source: - type: string - defaultValue: '

Enter HTML here

' - -'Neos.ContentRepository.Testing:List': - superTypes: - 'Neos.ContentRepository.Testing:Content': true - childNodes: - items: - type: 'Neos.ContentRepository.Testing:ContentCollection' - constraints: - nodeTypes: - 'Neos.ContentRepository.Testing:ListItem': true - '*': false - -'Neos.ContentRepository.Testing:ListItem': - superTypes: - 'Neos.ContentRepository.Testing:Content': true - properties: - text: - type: 'string' - -'Neos.ContentRepository.Testing:ImportExport': - properties: - description: - type: string - someDate: - type: DateTime - -'Neos.ContentRepository.Testing:NodeTypeWithPlainLabel': - label: 'Test nodetype' - -'Neos.ContentRepository.Testing:NodeTypeWithEelExpressionLabel': - label: '${"Test" + " " + "nodetype"}' diff --git a/Neos.ContentRepository.NodeAccess/Configuration/Testing/Policy.yaml b/Neos.ContentRepository.NodeAccess/Configuration/Testing/Policy.yaml deleted file mode 100644 index a04a7a5245e..00000000000 --- a/Neos.ContentRepository.NodeAccess/Configuration/Testing/Policy.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# # -# Security policy for the ContentRepository package # -# # - -roles: - 'Neos.ContentRepository:TestingAdministrator': - parentRoles: ['Neos.ContentRepository:Administrator'] diff --git a/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php b/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php index 8bd7c065860..23b18fc4bee 100644 --- a/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php +++ b/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php @@ -130,10 +130,10 @@ protected function executeSubMigrationAndBlock( if ($transformations->containsGlobal()) { $transformations->executeGlobalAndBlock($workspaceNameForWriting); } elseif ($transformations->containsNodeAggregateBased()) { - foreach ($this->contentRepository->getContentGraph()->findUsedNodeTypeNames() as $nodeTypeName) { + $contentGraph = $this->contentRepository->getContentGraph($workspaceForReading->workspaceName); + foreach ($contentGraph->findUsedNodeTypeNames() as $nodeTypeName) { foreach ( - $this->contentRepository->getContentGraph()->findNodeAggregatesByType( - $workspaceForReading->currentContentStreamId, + $contentGraph->findNodeAggregatesByType( $nodeTypeName ) as $nodeAggregate ) { @@ -143,10 +143,10 @@ protected function executeSubMigrationAndBlock( } } } elseif ($transformations->containsNodeBased()) { - foreach ($this->contentRepository->getContentGraph()->findUsedNodeTypeNames() as $nodeTypeName) { + $contentGraph = $this->contentRepository->getContentGraph($workspaceForReading->workspaceName); + foreach ($contentGraph->findUsedNodeTypeNames() as $nodeTypeName) { foreach ( - $this->contentRepository->getContentGraph()->findNodeAggregatesByType( - $workspaceForReading->currentContentStreamId, + $contentGraph->findNodeAggregatesByType( $nodeTypeName ) as $nodeAggregate ) { diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DimensionAdjustment.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DimensionAdjustment.php index b35ce49b8f7..554519dad57 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DimensionAdjustment.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DimensionAdjustment.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\StructureAdjustment\Adjustment; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\VariantType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; @@ -29,6 +30,16 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): iterable return []; } if ($nodeType->isOfType(NodeTypeName::ROOT_NODE_TYPE_NAME)) { + foreach ($this->projectedNodeIterator->nodeAggregatesOfType($nodeTypeName) as $nodeAggregate) { + if ( + !$nodeAggregate->coveredDimensionSpacePoints->equals($this->interDimensionalVariationGraph->getDimensionSpacePoints()) + ) { + throw new \Exception( + 'Cannot determine structure adjustments for root node type ' . $nodeTypeName->value + . ', run UpdateRootNodeAggregateDimensions first' + ); + } + } return []; } foreach ($this->projectedNodeIterator->nodeAggregatesOfType($nodeTypeName) as $nodeAggregate) { diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php index c715e22748c..5f602315ddd 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php @@ -24,7 +24,6 @@ class DisallowedChildNodeAdjustment use RemoveNodeAggregateTrait; public function __construct( - private readonly ContentRepository $contentRepository, private readonly ProjectedNodeIterator $projectedNodeIterator, private readonly NodeTypeManager $nodeTypeManager, ) { @@ -51,8 +50,7 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat // as it can happen that the constraint is only violated in e.g. "AT", but not in "DE". // Then, we only want to remove the single edge. foreach ($nodeAggregate->coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { - $subgraph = $this->contentRepository->getContentGraph()->getSubgraph( - $nodeAggregate->contentStreamId, + $subgraph = $this->projectedNodeIterator->contentGraph->getSubgraph( $coveredDimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); @@ -68,7 +66,7 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat $parentNodeType = $this->nodeTypeManager->getNodeType($parentNode->nodeTypeName); if ($parentNodeType) { $allowedByParent = $parentNodeType->allowsChildNodeType($nodeType) - || $nodeAggregate->nodeName && $parentNodeType->hasTetheredNode($nodeAggregate->nodeName); + || ($nodeAggregate->nodeName && $parentNodeType->hasTetheredNode($nodeAggregate->nodeName)); } } diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/ProjectedNodeIterator.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/ProjectedNodeIterator.php index e7548998909..69e50487315 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/ProjectedNodeIterator.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/ProjectedNodeIterator.php @@ -8,8 +8,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** * Low-Level helper service, iterating over the "real" Nodes in the Live workspace; that is, the nodes, @@ -22,13 +20,10 @@ */ class ProjectedNodeIterator { - protected WorkspaceFinder $workspaceFinder; - protected ContentGraphInterface $contentGraph; - - public function __construct(WorkspaceFinder $workspaceFinder, ContentGraphInterface $contentGraph) + public function __construct( + public readonly ContentGraphInterface $contentGraph + ) { - $this->workspaceFinder = $workspaceFinder; - $this->contentGraph = $contentGraph; } /** @@ -37,18 +32,9 @@ public function __construct(WorkspaceFinder $workspaceFinder, ContentGraphInterf */ public function nodeAggregatesOfType(NodeTypeName $nodeTypeName): iterable { - $contentStreamId = $this->findLiveContentStream(); - $nodeAggregates = $this->contentGraph->findNodeAggregatesByType($contentStreamId, $nodeTypeName); + $nodeAggregates = $this->contentGraph->findNodeAggregatesByType($nodeTypeName); foreach ($nodeAggregates as $nodeAggregate) { yield $nodeAggregate; } } - - private function findLiveContentStream(): ContentStreamId - { - $liveWorkspace = $this->workspaceFinder->findOneByName(WorkspaceName::forLive()); - assert($liveWorkspace !== null, 'Live workspace not found'); - - return $liveWorkspace->currentContentStreamId; - } } diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php index fcdc1104af6..73195ff0991 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php @@ -8,25 +8,23 @@ use Neos\ContentRepository\Core\DimensionSpace; 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\NodeVariationInternals; use Neos\ContentRepository\Core\Feature\Common\TetheredNodeInternals; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMappings; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMappings; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; 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\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Model\EventStream\ExpectedVersion; class TetheredNodeAdjustments @@ -36,7 +34,6 @@ class TetheredNodeAdjustments use TetheredNodeInternals; public function __construct( - private readonly ContentRepository $contentRepository, private readonly ProjectedNodeIterator $projectedNodeIterator, private readonly NodeTypeManager $nodeTypeManager, private readonly DimensionSpace\InterDimensionalVariationGraph $interDimensionalVariationGraph, @@ -69,14 +66,12 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat foreach ($expectedTetheredNodes as $tetheredNodeName => $expectedTetheredNodeType) { $tetheredNodeName = NodeName::fromString($tetheredNodeName); - $subgraph = $this->contentRepository->getContentGraph()->getSubgraph( - $nodeAggregate->contentStreamId, + $tetheredNode = $this->projectedNodeIterator->contentGraph->getSubgraph( $originDimensionSpacePoint->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions() - ); - $tetheredNode = $subgraph->findNodeByPath( + )->findNodeByPath( $tetheredNodeName, - $nodeAggregate->nodeAggregateId, + $nodeAggregate->nodeAggregateId ); if ($tetheredNode === null) { $foundMissingOrDisallowedTetheredNodes = true; @@ -90,12 +85,12 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat 'The tethered child node "' . $tetheredNodeName->value . '" is missing.', function () use ($nodeAggregate, $originDimensionSpacePoint, $tetheredNodeName, $expectedTetheredNodeType) { $events = $this->createEventsForMissingTetheredNode( + $this->projectedNodeIterator->contentGraph, $nodeAggregate, $originDimensionSpacePoint, $tetheredNodeName, null, - $expectedTetheredNodeType, - $this->contentRepository + $expectedTetheredNodeType ); $streamName = ContentStreamEventStreamName::fromContentStreamId($nodeAggregate->contentStreamId); @@ -114,8 +109,7 @@ function () use ($nodeAggregate, $originDimensionSpacePoint, $tetheredNodeName, } // find disallowed tethered nodes - $tetheredNodeAggregates = $this->contentRepository->getContentGraph()->findTetheredChildNodeAggregates( - $nodeAggregate->contentStreamId, + $tetheredNodeAggregates = $this->projectedNodeIterator->contentGraph->findTetheredChildNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($tetheredNodeAggregates as $tetheredNodeAggregate) { @@ -137,12 +131,7 @@ function () use ($tetheredNodeAggregate) { // find wrongly ordered tethered nodes if ($foundMissingOrDisallowedTetheredNodes === false) { foreach ($originDimensionSpacePoints as $originDimensionSpacePoint) { - $subgraph = $this->contentRepository->getContentGraph()->getSubgraph( - $nodeAggregate->contentStreamId, - $originDimensionSpacePoint->toDimensionSpacePoint(), - VisibilityConstraints::withoutRestrictions() - ); - $childNodes = $subgraph->findChildNodes($nodeAggregate->nodeAggregateId, FindChildNodesFilter::create()); + $childNodes = $this->projectedNodeIterator->contentGraph->getSubgraph($originDimensionSpacePoint->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions())->findChildNodes($nodeAggregate->nodeAggregateId, FindChildNodesFilter::create()); /** is indexed by node name, and the value is the tethered node itself */ $actualTetheredChildNodes = []; @@ -166,8 +155,7 @@ function () use ($tetheredNodeAggregate) { . implode(', ', array_keys($actualTetheredChildNodes)), fn () => $this->reorderNodes( $nodeAggregate->contentStreamId, - $nodeAggregate->nodeAggregateId, - $originDimensionSpacePoint, + $nodeAggregate->getCoverageByOccupant($originDimensionSpacePoint), $actualTetheredChildNodes, array_keys($expectedTetheredNodes) ) @@ -224,8 +212,7 @@ protected function getPropertyConverter(): PropertyConverter */ private function reorderNodes( ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId, - DimensionSpace\OriginDimensionSpacePoint $originDimensionSpacePoint, + DimensionSpace\DimensionSpacePointSet $coverageByOrigin, array $actualTetheredChildNodes, array $expectedNodeOrdering ): EventsToPublish { @@ -235,33 +222,22 @@ private function reorderNodes( $succeedingSiblingNodeName = array_pop($expectedNodeOrdering); while ($nodeNameToMove = array_pop($expectedNodeOrdering)) { // let's move $nodeToMove before $succeedingNode. - /* @var $nodeToMove Node */ $nodeToMove = $actualTetheredChildNodes[$nodeNameToMove]; - /* @var $succeedingNode Node */ $succeedingNode = $actualTetheredChildNodes[$succeedingSiblingNodeName]; + $succeedingSiblingsForCoverage = []; + foreach ($coverageByOrigin as $coveredDimensionSpacePoint) { + $succeedingSiblingsForCoverage[] = new InterdimensionalSibling( + $coveredDimensionSpacePoint, + $succeedingNode->nodeAggregateId + ); + } + $events[] = new NodeAggregateWasMoved( $contentStreamId, $nodeToMove->nodeAggregateId, - OriginNodeMoveMappings::fromArray([ - new OriginNodeMoveMapping( - $nodeToMove->originDimensionSpacePoint, - CoverageNodeMoveMappings::create( - CoverageNodeMoveMapping::createForNewSucceedingSibling( - // TODO: I am not sure the next line is 100% correct. IMHO this must be the COVERED - // TODO: DimensionSpacePoint (though I am not sure whether we have that one now) - $nodeToMove->originDimensionSpacePoint->toDimensionSpacePoint(), - SucceedingSiblingNodeMoveDestination::create( - $succeedingNode->nodeAggregateId, - $succeedingNode->originDimensionSpacePoint, - // we only change the order, not the parent -> so we can simply use the parent here. - $parentNodeAggregateId, - $originDimensionSpacePoint - ) - ) - ) - ) - ]), + null, + new InterdimensionalSiblings(...$succeedingSiblingsForCoverage), ); // now, go one step left. diff --git a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php index 3f50d52afe7..3c671758d58 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php +++ b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php @@ -12,6 +12,7 @@ use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\StructureAdjustment\Adjustment\DimensionAdjustment; use Neos\ContentRepository\StructureAdjustment\Adjustment\DisallowedChildNodeAdjustment; use Neos\ContentRepository\StructureAdjustment\Adjustment\ProjectedNodeIterator; @@ -33,15 +34,13 @@ public function __construct( private readonly EventPersister $eventPersister, NodeTypeManager $nodeTypeManager, InterDimensionalVariationGraph $interDimensionalVariationGraph, - PropertyConverter $propertyConverter + PropertyConverter $propertyConverter, ) { $projectedNodeIterator = new ProjectedNodeIterator( - $contentRepository->getWorkspaceFinder(), - $contentRepository->getContentGraph(), + $contentRepository->getContentGraph(WorkspaceName::forLive()), ); $this->tetheredNodeAdjustments = new TetheredNodeAdjustments( - $contentRepository, $projectedNodeIterator, $nodeTypeManager, $interDimensionalVariationGraph, @@ -53,7 +52,6 @@ public function __construct( $nodeTypeManager ); $this->disallowedChildNodeAdjustment = new DisallowedChildNodeAdjustment( - $this->contentRepository, $projectedNodeIterator, $nodeTypeManager ); @@ -73,7 +71,7 @@ public function __construct( */ public function findAllAdjustments(): \Generator { - foreach ($this->contentRepository->getContentGraph()->findUsedNodeTypeNames() as $nodeTypeName) { + foreach ($this->contentRepository->getContentGraph(WorkspaceName::forLive())->findUsedNodeTypeNames() as $nodeTypeName) { yield from $this->findAdjustmentsForNodeType($nodeTypeName); } } diff --git a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php index 3363a2ec7fb..b9e75abeaff 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php +++ b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentServiceFactory.php @@ -19,7 +19,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor $serviceFactoryDependencies->eventPersister, $serviceFactoryDependencies->nodeTypeManager, $serviceFactoryDependencies->interDimensionalVariationGraph, - $serviceFactoryDependencies->propertyConverter + $serviceFactoryDependencies->propertyConverter, ); } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php index a19eb219002..c714869f9b7 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteRuntimeVariables.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap; use Neos\ContentRepository\Core\CommandHandler\CommandResult; +use Neos\ContentRepository\Core\ContentGraphFinder; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; @@ -101,15 +102,6 @@ public function iAmInContentStream(string $contentStreamId): void * @Given /^I am in workspace "([^"]*)"$/ */ public function iAmInWorkspace(string $workspaceName): void - { - $this->currentWorkspaceName = WorkspaceName::fromString($workspaceName); - } - - /** - * @Given /^I am in the active content stream of workspace "([^"]*)"$/ - * @throws \Exception - */ - public function iAmInTheActiveContentStreamOfWorkspace(string $workspaceName): void { $workspace = $this->currentContentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($workspaceName)); if ($workspace === null) { @@ -128,21 +120,21 @@ public function iAmInDimensionSpacePoint(string $dimensionSpacePoint): void } /** - * @Given /^I am in content stream "([^"]*)" and dimension space point (.*)$/ + * @Given /^I am in workspace "([^"]*)" and dimension space point (.*)$/ + * @throws \Exception */ - public function iAmInContentStreamAndDimensionSpacePoint(string $contentStreamId, string $dimensionSpacePoint): void + public function iAmInWorkspaceAndDimensionSpacePoint(string $workspaceName, string $dimensionSpacePoint): void { - $this->iAmInContentStream($contentStreamId); + $this->iAmInWorkspace($workspaceName); $this->iAmInDimensionSpacePoint($dimensionSpacePoint); } /** - * @Given /^I am in the active content stream of workspace "([^"]*)" and dimension space point (.*)$/ - * @throws \Exception + * @Given /^I am in content stream "([^"]*)" and dimension space point (.*)$/ */ - public function iAmInTheActiveContentStreamOfWorkspaceAndDimensionSpacePoint(string $workspaceName, string $dimensionSpacePoint): void + public function iAmInContentStreamAndDimensionSpacePoint(string $contentStreamId, string $dimensionSpacePoint): void { - $this->iAmInTheActiveContentStreamOfWorkspace($workspaceName); + $this->iAmInContentStream($contentStreamId); $this->iAmInDimensionSpacePoint($dimensionSpacePoint); } @@ -160,8 +152,14 @@ public function visibilityConstraintsAreSetTo(string $restrictionType): void public function getCurrentSubgraph(): ContentSubgraphInterface { - return $this->currentContentRepository->getContentGraph()->getSubgraph( - $this->currentContentStreamId, + $contentGraphFinder = $this->currentContentRepository->projectionState(ContentGraphFinder::class); + $contentGraphFinder->forgetInstances(); + if (isset($this->currentContentStreamId)) { + // This must still be supported for low level tests, e.g. for content stream forking + return $contentGraphFinder->getByWorkspaceNameAndContentStreamId($this->currentWorkspaceName, $this->currentContentStreamId)->getSubgraph($this->currentDimensionSpacePoint, $this->currentVisibilityConstraints); + } + + return $contentGraphFinder->getByWorkspaceName($this->currentWorkspaceName)->getSubgraph( $this->currentDimensionSpacePoint, $this->currentVisibilityConstraints ); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 832cfcef5fa..fcb2f45fbaa 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -106,6 +106,7 @@ public function beforeEventSourcedScenarioDispatcher(BeforeScenarioScope $scope) $this->currentDimensionSpacePoint = null; $this->currentRootNodeAggregateId = null; $this->currentContentStreamId = null; + $this->currentWorkspaceName = null; $this->currentNodeAggregate = null; $this->currentNode = null; } @@ -123,7 +124,6 @@ protected function readPayloadTable(TableNode $payloadTable): array $propertyOrMethodName = \mb_substr($line['Value'], \mb_strlen('$this->')); $value = match ($propertyOrMethodName) { 'currentNodeAggregateId' => $this->getCurrentNodeAggregateId()->value, - 'contentStreamId' => $this->currentContentStreamId->value, default => method_exists($this, $propertyOrMethodName) ? (string)$this->$propertyOrMethodName() : (string)$this->$propertyOrMethodName, }; } else { @@ -195,7 +195,7 @@ public function workspaceHasStatus(string $rawWorkspaceName, string $status): vo */ public function iExpectTheGraphProjectionToConsistOfExactlyNodes(int $expectedNumberOfNodes): void { - $actualNumberOfNodes = $this->currentContentRepository->getContentGraph()->countNodes(); + $actualNumberOfNodes = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->countNodes(); Assert::assertSame($expectedNumberOfNodes, $actualNumberOfNodes, 'Content graph consists of ' . $actualNumberOfNodes . ' nodes, expected were ' . $expectedNumberOfNodes . '.'); } @@ -262,8 +262,7 @@ protected function getRootNodeAggregateId(): ?NodeAggregateId } try { - return $this->currentContentRepository->getContentGraph()->findRootNodeAggregateByType( - $this->currentContentStreamId, + return $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->findRootNodeAggregateByType( NodeTypeName::fromString('Neos.Neos:Sites') )->nodeAggregateId; } catch (RootNodeAggregateDoesNotExist) { @@ -288,7 +287,13 @@ public function theContentStreamHasState(string $contentStreamId, string $expect */ public function theCurrentContentStreamHasState(string $expectedState): void { - $this->theContentStreamHasState($this->currentContentStreamId->value, $expectedState); + $this->theContentStreamHasState( + $this->currentContentRepository + ->getWorkspaceFinder() + ->findOneByName($this->currentWorkspaceName) + ->currentContentStreamId->value, + $expectedState + ); } /** diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamClosing.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamClosing.php index 8e9589fc490..cf1fdeee18d 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamClosing.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamClosing.php @@ -35,11 +35,7 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; public function theCommandCloseContentStreamIsExecutedWithPayload(TableNode $payloadTable): void { $commandArguments = $this->readPayloadTable($payloadTable); - $contentStreamId = isset($commandArguments['contentStreamId']) - ? ContentStreamId::fromString($commandArguments['contentStreamId']) - : $this->currentContentStreamId; - - $command = CloseContentStream::create($contentStreamId); + $command = CloseContentStream::create(ContentStreamId::fromString($commandArguments['contentStreamId'])); $this->lastCommandOrEventResult = $this->currentContentRepository->handle($command); } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php index c6c887a0770..ce7bae86a26 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php @@ -36,13 +36,9 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; public function theCommandForkContentStreamIsExecutedWithPayload(TableNode $payloadTable): void { $commandArguments = $this->readPayloadTable($payloadTable); - $sourceContentStreamId = isset($commandArguments['sourceContentStreamId']) - ? ContentStreamId::fromString($commandArguments['sourceContentStreamId']) - : $this->currentContentStreamId; - $command = ForkContentStream::create( ContentStreamId::fromString($commandArguments['contentStreamId']), - $sourceContentStreamId, + ContentStreamId::fromString($commandArguments['sourceContentStreamId']), ); $this->lastCommandOrEventResult = $this->currentContentRepository->handle($command); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php index f9c85562631..ae19812dc32 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCopying.php @@ -39,8 +39,7 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; public function theCommandCopyNodesRecursivelyIsExecutedCopyingTheCurrentNodeAggregateWithPayload(TableNode $payloadTable): void { $commandArguments = $this->readPayloadTable($payloadTable); - $subgraph = $this->currentContentRepository->getContentGraph()->getSubgraph( - $this->currentContentStreamId, + $subgraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->getSubgraph( $this->currentDimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php index e9338a046b8..d9fe8b59c92 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeCreation.php @@ -94,9 +94,6 @@ public function theCommandCreateRootNodeAggregateWithNodeIsExecutedWithPayloadAn public function theEventRootNodeAggregateWithNodeWasCreatedWasPublishedToStreamWithPayload(TableNode $payloadTable) { $eventPayload = $this->readPayloadTable($payloadTable); - if (!isset($eventPayload['contentStreamId'])) { - $eventPayload['contentStreamId'] = $this->currentContentStreamId?->value; - } $contentStreamId = ContentStreamId::fromString($eventPayload['contentStreamId']); $nodeAggregateId = NodeAggregateId::fromString($eventPayload['nodeAggregateId']); $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php index fcf7c31be82..df53480a408 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeModification.php @@ -85,9 +85,6 @@ public function theCommandSetPropertiesIsExecutedWithPayloadAndExceptionsAreCaug public function theEventNodePropertiesWereSetWasPublishedWithPayload(TableNode $payloadTable) { $eventPayload = $this->readPayloadTable($payloadTable); - if (!isset($eventPayload['contentStreamId'])) { - $eventPayload['contentStreamId'] = $this->currentContentStreamId->value; - } if (!isset($eventPayload['originDimensionSpacePoint'])) { $eventPayload['originDimensionSpacePoint'] = json_encode($this->currentDimensionSpacePoint); } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php index e965c3e1ca6..fcced8fa130 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeMove.php @@ -97,9 +97,6 @@ public function theCommandMoveNodeIsExecutedWithPayloadAndExceptionsAreCaught(Ta public function theEventNodeAggregateWasMovedWasPublishedWithPayload(TableNode $payloadTable) { $eventPayload = $this->readPayloadTable($payloadTable); - if (!isset($eventPayload['contentStreamId'])) { - $eventPayload['contentStreamId'] = $this->currentContentStreamId->value; - } $contentStreamId = ContentStreamId::fromString($eventPayload['contentStreamId']); $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php index ea391b1d359..bffc305d9b7 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeReferencing.php @@ -100,9 +100,6 @@ public function theCommandSetNodeReferencesIsExecutedWithPayloadAndExceptionsAre public function theEventNodeReferencesWereSetWasPublishedWithPayload(TableNode $payloadTable) { $eventPayload = $this->readPayloadTable($payloadTable); - if (!isset($eventPayload['contentStreamId'])) { - $eventPayload['contentStreamId'] = $this->currentContentStreamId; - } $contentStreamId = ContentStreamId::fromString($eventPayload['contentStreamId']); $streamName = ContentStreamEventStreamName::fromContentStreamId( $contentStreamId diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php index e6e28de0bd6..8a6a0139f71 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeRemoval.php @@ -86,9 +86,6 @@ public function theCommandRemoveNodeAggregateIsExecutedWithPayloadAndExceptionsA public function theEventNodeAggregateWasRemovedWasPublishedWithPayload(TableNode $payloadTable) { $eventPayload = $this->readPayloadTable($payloadTable); - if (!isset($eventPayload['contentStreamId'])) { - $eventPayload['contentStreamId'] = $this->currentContentStreamId->value; - } $contentStreamId = ContentStreamId::fromString($eventPayload['contentStreamId']); $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php index 0eae60a160f..3a4c78dba9b 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php @@ -86,9 +86,7 @@ public function theEventSubtreeWasTaggedWasPublishedWithPayload(TableNode $paylo { $eventPayload = $this->readPayloadTable($payloadTable); $streamName = ContentStreamEventStreamName::fromContentStreamId( - array_key_exists('contentStreamId', $eventPayload) - ? ContentStreamId::fromString($eventPayload['contentStreamId']) - : $this->currentContentStreamId + ContentStreamId::fromString($eventPayload['contentStreamId']) ); $this->publishEvent('SubtreeWasTagged', $streamName->getEventStreamName(), $eventPayload); @@ -104,9 +102,7 @@ public function theEventSubtreeWasUntaggedWasPublishedWithPayload(TableNode $pay { $eventPayload = $this->readPayloadTable($payloadTable); $streamName = ContentStreamEventStreamName::fromContentStreamId( - array_key_exists('contentStreamId', $eventPayload) - ? ContentStreamId::fromString($eventPayload['contentStreamId']) - : $this->currentContentStreamId + ContentStreamId::fromString($eventPayload['contentStreamId']) ); $this->publishEvent('SubtreeWasUntagged', $streamName->getEventStreamName(), $eventPayload); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php index 119ed394de0..098598b9c2a 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php @@ -248,7 +248,11 @@ public function iExecuteTheFindSubtreeQueryIExpectTheFollowingTrees(string $entr $subtree = array_shift($subtreeStack); $tags = []; if ($withTags !== null) { - $tags = [...array_map(static fn(string $tag) => $tag . '*', $subtree->node->tags->withoutInherited()->toStringArray()), ...$subtree->node->tags->onlyInherited()->toStringArray()]; + $explicitTags = $subtree->node->tags->withoutInherited()->toStringArray(); + sort($explicitTags); + $inheritedTags = $subtree->node->tags->onlyInherited()->toStringArray(); + sort($inheritedTags); + $tags = [...array_map(static fn(string $tag) => $tag . '*', $explicitTags), ...$inheritedTags]; } $result[] = str_repeat(' ', $subtree->level) . $subtree->node->nodeAggregateId->value . ($tags !== [] ? ' (' . implode(',', $tags) . ')' : ''); $subtreeStack = [...$subtree->children, ...$subtreeStack]; diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php index b72d26e82c4..6b0c428e5dc 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php @@ -42,15 +42,15 @@ public function iExpectTheNodeAggregateToExist(string $serializedNodeAggregateId { $nodeAggregateId = NodeAggregateId::fromString($serializedNodeAggregateId); $this->initializeCurrentNodeAggregate(function (ContentGraphInterface $contentGraph) use ($nodeAggregateId) { - $currentNodeAggregate = $contentGraph->findNodeAggregateById($this->currentContentStreamId, $nodeAggregateId); - Assert::assertNotNull($currentNodeAggregate, sprintf('Node aggregate "%s" was not found in the current content stream "%s".', $nodeAggregateId->value, $this->currentContentStreamId->value)); + $currentNodeAggregate = $contentGraph->findNodeAggregateById($nodeAggregateId); + Assert::assertNotNull($currentNodeAggregate, sprintf('Node aggregate "%s" was not found in the current workspace "%s".', $nodeAggregateId->value, $this->currentWorkspaceName->value)); return $currentNodeAggregate; }); } protected function initializeCurrentNodeAggregate(callable $query): void { - $this->currentNodeAggregate = $query($this->currentContentRepository->getContentGraph()); + $this->currentNodeAggregate = $query($this->currentContentRepository->getContentGraph($this->currentWorkspaceName)); } /** @@ -159,8 +159,7 @@ public function iExpectThisNodeAggregateToHaveNoParentNodeAggregates(): void { $this->assertOnCurrentNodeAggregate(function (NodeAggregate $nodeAggregate) { Assert::assertEmpty( - iterator_to_array($this->currentContentRepository->getContentGraph()->findParentNodeAggregates( - $nodeAggregate->contentStreamId, + iterator_to_array($this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->findParentNodeAggregates( $nodeAggregate->nodeAggregateId )), 'Did not expect parent node aggregates.' @@ -182,8 +181,7 @@ public function iExpectThisNodeAggregateToHaveTheParentNodeAggregates(string $se fn (NodeAggregate $parentNodeAggregate): string => $parentNodeAggregate->contentStreamId->value . ';' . $parentNodeAggregate->nodeAggregateId->value, iterator_to_array( - $this->currentContentRepository->getContentGraph()->findParentNodeAggregates( - $nodeAggregate->contentStreamId, + $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->findParentNodeAggregates( $nodeAggregate->nodeAggregateId ) ) @@ -203,8 +201,7 @@ public function iExpectThisNodeAggregateToHaveNoChildNodeAggregates(): void { $this->assertOnCurrentNodeAggregate(function (NodeAggregate $nodeAggregate) { Assert::assertEmpty( - iterator_to_array($this->currentContentRepository->getContentGraph()->findChildNodeAggregates( - $nodeAggregate->contentStreamId, + iterator_to_array($this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->findChildNodeAggregates( $nodeAggregate->nodeAggregateId )), 'No child node aggregates were expected.' @@ -227,8 +224,7 @@ public function iExpectThisNodeAggregateToHaveTheChildNodeAggregates(string $ser $actualDiscriminators = array_values(array_map( fn (NodeAggregate $parentNodeAggregate): string => $parentNodeAggregate->contentStreamId->value . ':' . $parentNodeAggregate->nodeAggregateId->value, - iterator_to_array($this->currentContentRepository->getContentGraph()->findChildNodeAggregates( - $nodeAggregate->contentStreamId, + iterator_to_array($this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->findChildNodeAggregates( $nodeAggregate->nodeAggregateId )) )); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php index 0be5327d92b..0822a944d08 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php @@ -16,6 +16,7 @@ use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Psr7\Uri; +use Neos\ContentRepository\Core\ContentGraphFinder; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; @@ -80,23 +81,24 @@ public function iGetTheNodeAtPath(string $serializedNodePath): void public function iExpectANodeIdentifiedByXToExistInTheContentGraph(string $serializedNodeDiscriminator): void { $nodeDiscriminator = NodeDiscriminator::fromShorthand($serializedNodeDiscriminator); - $this->initializeCurrentNodeFromContentGraph(function (ContentGraphInterface $contentGraph) use ($nodeDiscriminator) { - $currentNodeAggregate = $contentGraph->findNodeAggregateById( - $nodeDiscriminator->contentStreamId, - $nodeDiscriminator->nodeAggregateId - ); - Assert::assertTrue( - $currentNodeAggregate?->occupiesDimensionSpacePoint($nodeDiscriminator->originDimensionSpacePoint), - 'Node with aggregate id "' . $nodeDiscriminator->nodeAggregateId->value - . '" and originating in dimension space point "' . $nodeDiscriminator->originDimensionSpacePoint->toJson() - . '" was not found in content stream "' . $nodeDiscriminator->contentStreamId->value . '"' - ); - - return $currentNodeAggregate->getNodeByOccupiedDimensionSpacePoint($nodeDiscriminator->originDimensionSpacePoint); - }); + $contentGraphFinder = $this->currentContentRepository->projectionState(ContentGraphFinder::class); + $contentGraphFinder->forgetInstances(); + $workspaceName = $this->currentContentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId( + $nodeDiscriminator->contentStreamId + )->workspaceName; + $contentGraph = $this->currentContentRepository->getContentGraph($workspaceName); + $currentNodeAggregate = $contentGraph->findNodeAggregateById( + $nodeDiscriminator->nodeAggregateId + ); + Assert::assertTrue( + $currentNodeAggregate?->occupiesDimensionSpacePoint($nodeDiscriminator->originDimensionSpacePoint), + 'Node with aggregate id "' . $nodeDiscriminator->nodeAggregateId->value + . '" and originating in dimension space point "' . $nodeDiscriminator->originDimensionSpacePoint->toJson() + . '" was not found in content stream "' . $nodeDiscriminator->contentStreamId->value . '"' + ); + $this->currentNode = $currentNodeAggregate->getNodeByOccupiedDimensionSpacePoint($nodeDiscriminator->originDimensionSpacePoint); } - /** * @Then /^I expect node aggregate identifier "([^"]*)" to lead to node (.*)$/ */ @@ -108,7 +110,7 @@ public function iExpectNodeAggregateIdToLeadToNode( $expectedDiscriminator = NodeDiscriminator::fromShorthand($serializedNodeDiscriminator); $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodeAggregateId, $expectedDiscriminator) { $currentNode = $subgraph->findNodeById($nodeAggregateId); - Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); + Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentWorkspaceName->value . '"'); $actualDiscriminator = NodeDiscriminator::fromNode($currentNode); Assert::assertTrue($expectedDiscriminator->equals($actualDiscriminator), 'Node discriminators do not match. Expected was ' . json_encode($expectedDiscriminator) . ' , given was ' . json_encode($actualDiscriminator)); return $currentNode; @@ -125,7 +127,7 @@ public function iExpectNodeAggregateIdToLeadToNoNode(string $serializedNodeAggre if (!is_null($nodeByAggregateId)) { Assert::fail( 'A node was found by node aggregate id "' . $nodeAggregateId->value - . '" in content subgraph {' . $this->currentDimensionSpacePoint->toJson() . ',' . $this->currentContentStreamId->value . '}' + . '" in content subgraph {' . $this->currentDimensionSpacePoint->toJson() . ',' . $this->currentWorkspaceName->value . '}' ); } } @@ -145,7 +147,7 @@ public function iExpectPathToLeadToNode(string $serializedNodePath, string $seri $expectedDiscriminator = NodeDiscriminator::fromShorthand($serializedNodeDiscriminator); $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodePath, $expectedDiscriminator) { $currentNode = $subgraph->findNodeByPath($nodePath, $this->getRootNodeAggregateId()); - Assert::assertNotNull($currentNode, 'No node could be found by node path "' . $nodePath->serializeToString() . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); + Assert::assertNotNull($currentNode, 'No node could be found by node path "' . $nodePath->serializeToString() . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentWorkspaceName->value . '"'); $actualDiscriminator = NodeDiscriminator::fromNode($currentNode); Assert::assertTrue($expectedDiscriminator->equals($actualDiscriminator), 'Node discriminators do not match. Expected was ' . json_encode($expectedDiscriminator) . ' , given was ' . json_encode($actualDiscriminator)); return $currentNode; @@ -167,7 +169,7 @@ public function iExpectPathToLeadToNoNode(string $serializedNodePath): void Assert::assertNull( $nodeByPath, 'A node was found by node path "' . $nodePath->serializeToString() - . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"' + . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentWorkspaceName->value . '"' ); } @@ -205,7 +207,7 @@ public function iExpectTheNodeWithAggregateIdentifierToBeExplicitlyTagged(string $expectedTag = SubtreeTag::fromString($serializedTag); $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodeAggregateId, $expectedTag) { $currentNode = $subgraph->findNodeById($nodeAggregateId); - Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); + Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentWorkspaceName->value . '"'); Assert::assertTrue($currentNode->tags->withoutInherited()->contain($expectedTag)); return $currentNode; }); @@ -220,7 +222,7 @@ public function iExpectTheNodeWithAggregateIdentifierToInheritTheTag(string $ser $expectedTag = SubtreeTag::fromString($serializedTag); $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodeAggregateId, $expectedTag) { $currentNode = $subgraph->findNodeById($nodeAggregateId); - Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); + Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentWorkspaceName->value . '"'); Assert::assertTrue($currentNode->tags->onlyInherited()->contain($expectedTag)); return $currentNode; }); @@ -235,15 +237,42 @@ public function iExpectTheNodeWithAggregateIdentifierToNotContainTheTag(string $ $expectedTag = SubtreeTag::fromString($serializedTag); $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodeAggregateId, $expectedTag) { $currentNode = $subgraph->findNodeById($nodeAggregateId); - Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); - Assert::assertFalse($currentNode->tags->contain($expectedTag), sprintf('Node with id "%s" in content subgraph "%s@%s", was not expected to contain the subtree tag "%s" but it does', $nodeAggregateId->value, $this->currentDimensionSpacePoint->toJson(), $this->currentContentStreamId->value, $expectedTag->value)); + Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentWorkspaceName->value . '"'); + Assert::assertFalse($currentNode->tags->contain($expectedTag), sprintf('Node with id "%s" in content subgraph "%s@%s", was not expected to contain the subtree tag "%s" but it does', $nodeAggregateId->value, $this->currentDimensionSpacePoint->toJson(), $this->currentWorkspaceName->value, $expectedTag->value)); return $currentNode; }); } - protected function initializeCurrentNodeFromContentGraph(callable $query): void + /** + * @Then /^I expect this node to be exactly explicitly tagged "(.*)"$/ + * @param string $expectedTagList the comma-separated list of tag names + */ + public function iExpectThisNodeToBeExactlyExplicitlyTagged(string $expectedTagList): void { - $this->currentNode = $query($this->currentContentRepository->getContentGraph()); + $this->assertOnCurrentNode(function (Node $currentNode) use ($expectedTagList) { + $actualTags = $currentNode->tags->withoutInherited()->toStringArray(); + sort($actualTags); + Assert::assertSame( + ($expectedTagList === '') ? [] : explode(',', $expectedTagList), + $actualTags + ); + }); + } + + /** + * @Then /^I expect this node to exactly inherit the tags "(.*)"$/ + * @param string $expectedTagList the comma-separated list of tag names + */ + public function iExpectThisNodeToExactlyInheritTheTags(string $expectedTagList): void + { + $this->assertOnCurrentNode(function (Node $currentNode) use ($expectedTagList) { + $actualTags = $currentNode->tags->onlyInherited()->toStringArray(); + sort($actualTags); + Assert::assertSame( + ($expectedTagList === '') ? [] : explode(',', $expectedTagList), + $actualTags, + ); + }); } protected function initializeCurrentNodeFromContentSubgraph(callable $query): void diff --git a/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php b/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php index 61ce76d10d0..ac74c544c0b 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php +++ b/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php @@ -35,10 +35,12 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; 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; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Serializer; @@ -87,17 +89,13 @@ public function createMinimalNodeOfType( ): Node { $serializedDefaultPropertyValues = SerializedPropertyValues::defaultFromNodeType($nodeType, $this->propertyConverter); return Node::create( - ContentSubgraphIdentity::create( - ContentRepositoryId::fromString('default'), - ContentStreamId::fromString('cs-id'), - DimensionSpacePoint::createWithoutDimensions(), - VisibilityConstraints::withoutRestrictions() - ), + ContentRepositoryId::fromString('default'), + WorkspaceName::forLive(), + DimensionSpacePoint::createWithoutDimensions(), NodeAggregateId::create(), OriginDimensionSpacePoint::createWithoutDimensions(), NodeAggregateClassification::CLASSIFICATION_REGULAR, $nodeType->name, - $nodeType, new PropertyCollection( $propertyValues ? $serializedDefaultPropertyValues->merge($propertyValues) @@ -111,7 +109,10 @@ public function createMinimalNodeOfType( new \DateTimeImmutable(), new \DateTimeImmutable(), new \DateTimeImmutable() - ) + ), + VisibilityConstraints::withoutRestrictions(), + $nodeType, + ContentStreamId::fromString('cs-id'), ); } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/ContentCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/ContentCommandController.php index d4b5c15dc61..aebcfb0dd96 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/ContentCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/ContentCommandController.php @@ -24,6 +24,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -60,8 +61,7 @@ public function refreshRootNodeDimensionsCommand(string $contentRepository = 'de $this->outputLine('Refreshing root node dimensions in workspace %s (content repository %s)', [$workspaceInstance->workspaceName->value, $contentRepositoryId->value]); $this->outputLine('Resolved content stream %s', [$workspaceInstance->currentContentStreamId->value]); - $rootNodeAggregates = $contentRepositoryInstance->getContentGraph()->findRootNodeAggregates( - $workspaceInstance->currentContentStreamId, + $rootNodeAggregates = $contentRepositoryInstance->getContentGraph($workspaceInstance->workspaceName)->findRootNodeAggregates( FindRootNodeAggregatesFilter::create() ); @@ -142,25 +142,24 @@ public function createVariantsRecursivelyCommand(string $source, string $target, $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $sourceSpacePoint = DimensionSpacePoint::fromJsonString($source); $targetSpacePoint = OriginDimensionSpacePoint::fromJsonString($target); + $workspaceName = WorkspaceName::fromString($workspace); $contentRepositoryInstance = $this->contentRepositoryRegistry->get($contentRepositoryId); - $workspaceInstance = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($workspace)); - if ($workspaceInstance === null) { - $this->outputLine('Workspace "%s" does not exist', [$workspace]); + + try { + $sourceSubgraph = $contentRepositoryInstance->getContentGraph($workspaceName)->getSubgraph( + $sourceSpacePoint, + VisibilityConstraints::withoutRestrictions() + ); + } catch (WorkspaceDoesNotExist) { + $this->outputLine('Workspace "%s" does not exist', [$workspaceName->value]); $this->quit(1); } - $this->outputLine('Creating %s to %s in workspace %s (content repository %s)', [$sourceSpacePoint->toJson(), $targetSpacePoint->toJson(), $workspaceInstance->workspaceName->value, $contentRepositoryId->value]); - $this->outputLine('Resolved content stream %s', [$workspaceInstance->currentContentStreamId->value]); - - $sourceSubgraph = $contentRepositoryInstance->getContentGraph()->getSubgraph( - $workspaceInstance->currentContentStreamId, - $sourceSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); + $this->outputLine('Creating %s to %s in workspace %s (content repository %s)', [$sourceSpacePoint->toJson(), $targetSpacePoint->toJson(), $workspaceName->value, $contentRepositoryId->value]); - $rootNodeAggregates = $contentRepositoryInstance->getContentGraph() - ->findRootNodeAggregates($workspaceInstance->currentContentStreamId, FindRootNodeAggregatesFilter::create()); + $rootNodeAggregates = $contentRepositoryInstance->getContentGraph($workspaceName) + ->findRootNodeAggregates(FindRootNodeAggregatesFilter::create()); foreach ($rootNodeAggregates as $rootNodeAggregate) { @@ -170,7 +169,7 @@ public function createVariantsRecursivelyCommand(string $source, string $target, $rootNodeAggregate->nodeAggregateId, $sourceSubgraph, $targetSpacePoint, - $workspaceInstance->workspaceName, + $workspaceName, $contentRepositoryInstance, ) ); diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/ContentGraphIntegrityCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/ContentGraphIntegrityCommandController.php index dd10ce1d5a6..71683ca6422 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/ContentGraphIntegrityCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/ContentGraphIntegrityCommandController.php @@ -11,8 +11,12 @@ * source code. */ -use Neos\ContentRepository\Core\Projection\ContentGraph\ProjectionIntegrityViolationDetectionRunner; +use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory; +use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Error\Messages\Result; +use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; final class ContentGraphIntegrityCommandController extends CommandController @@ -20,20 +24,22 @@ final class ContentGraphIntegrityCommandController extends CommandController private const OUTPUT_MODE_CONSOLE = 'console'; private const OUTPUT_MODE_LOG = 'log'; - private ProjectionIntegrityViolationDetectionRunner $detectionRunner; + #[Flow\Inject()] + protected DbalClientInterface $dbalClient; + #[Flow\Inject()] + protected ContentRepositoryRegistry $contentRepositoryRegistry; - public function __construct(ProjectionIntegrityViolationDetectionRunner $detectionRunner) + public function runViolationDetectionCommand(string $contentRepository = 'default', string $outputMode = null): void { - $this->detectionRunner = $detectionRunner; - parent::__construct(); - } + $detectionRunner = $this->contentRepositoryRegistry->buildService( + ContentRepositoryId::fromString($contentRepository), + new DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory($this->dbalClient) + ); - public function runViolationDetectionCommand(string $outputMode = null): void - { $outputMode = $this->resolveOutputMode($outputMode); /** @var Result $result */ - $result = $this->detectionRunner->run(); + $result = $detectionRunner->run(); switch ($outputMode) { case self::OUTPUT_MODE_CONSOLE: foreach ($result->getErrors() as $error) { diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index d78d6f92d82..b5d98b29e8e 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -13,6 +13,8 @@ use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventStore\StatusType; use Neos\Flow\Cli\CommandController; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\Output; final class CrCommandController extends CommandController @@ -116,6 +118,11 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo */ public function projectionReplayCommand(string $projection, string $contentRepository = 'default', bool $force = false, bool $quiet = false, int $until = 0): void { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + $progressBar = new ProgressBar($this->output->getOutput()); + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); if (!$force && $quiet) { $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); $this->quit(1); @@ -129,19 +136,18 @@ public function projectionReplayCommand(string $projection, string $contentRepos $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); + $options = CatchUpOptions::create(); if (!$quiet) { $this->outputLine('Replaying events for projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); - $this->output->progressStart(); + $progressBar->start(max($until > 0 ? $until : $projectionService->highestSequenceNumber()->value, 1)); + $options->with(progressCallback: fn () => $progressBar->advance()); } - $options = CatchUpOptions::create( - progressCallback: fn () => $this->output->progressAdvance(), - ); if ($until > 0) { $options = $options->with(maximumSequenceNumber: SequenceNumber::fromInteger($until)); } $projectionService->replayProjection($projection, $options); if (!$quiet) { - $this->output->progressFinish(); + $progressBar->finish(); $this->outputLine(); $this->outputLine('Done.'); } @@ -153,9 +159,23 @@ public function projectionReplayCommand(string $projection, string $contentRepos * @param string $contentRepository Identifier of the Content Repository instance to operate on * @param bool $force Replay the projection without confirmation. This may take some time! * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + * @param int $until Until which sequence number should projections be replayed? useful for debugging */ - public function projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false): void + public function projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false, int $until = 0): void { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + $mainSection = ($this->output->getOutput() instanceof ConsoleOutput) ? $this->output->getOutput()->section() : $this->output->getOutput(); + $mainProgressBar = new ProgressBar($mainSection); + $mainProgressBar->setBarCharacter(''); + $mainProgressBar->setEmptyBarCharacter('░'); + $mainProgressBar->setProgressCharacter(''); + $mainProgressBar->setFormat('debug'); + + $subSection = ($this->output->getOutput() instanceof ConsoleOutput) ? $this->output->getOutput()->section() : $this->output->getOutput(); + $progressBar = new ProgressBar($subSection); + $progressBar->setFormat(' %message% - %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); if (!$force && $quiet) { $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); $this->quit(1); @@ -171,9 +191,28 @@ public function projectionReplayAllCommand(string $contentRepository = 'default' if (!$quiet) { $this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]); } - // TODO progress bar with all events? Like projectionReplayCommand? - $projectionService->replayAllProjections(CatchUpOptions::create(), fn (string $projectionAlias) => $this->outputLine(sprintf(' * replaying %s projection', $projectionAlias))); + $options = CatchUpOptions::create(); + if (!$quiet) { + $options = $options->with(progressCallback: fn () => $progressBar->advance()); + } + if ($until > 0) { + $options = $options->with(maximumSequenceNumber: SequenceNumber::fromInteger($until)); + } + $highestSequenceNumber = max($until > 0 ? $until : $projectionService->highestSequenceNumber()->value, 1); + $mainProgressBar->start($projectionService->numberOfProjections()); + $mainProgressCallback = null; + if (!$quiet) { + $mainProgressCallback = static function (string $projectionAlias) use ($mainProgressBar, $progressBar, $highestSequenceNumber) { + $mainProgressBar->advance(); + $progressBar->setMessage($projectionAlias); + $progressBar->start($highestSequenceNumber); + $progressBar->setProgress(0); + }; + } + $projectionService->replayAllProjections($options, $mainProgressCallback); if (!$quiet) { + $mainProgressBar->finish(); + $progressBar->finish(); $this->outputLine('Done.'); } } diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 25daca7cf84..4900eb13451 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -92,11 +92,11 @@ public function resetFactoryInstance(ContentRepositoryId $contentRepositoryId): public function subgraphForNode(Node $node): ContentSubgraphInterface { - $contentRepository = $this->get($node->subgraphIdentity->contentRepositoryId); - return $contentRepository->getContentGraph()->getSubgraph( - $node->subgraphIdentity->contentStreamId, - $node->subgraphIdentity->dimensionSpacePoint, - $node->subgraphIdentity->visibilityConstraints + $contentRepository = $this->get($node->contentRepositoryId); + + return $contentRepository->getContentGraph($node->workspaceName)->getSubgraph( + $node->dimensionSpacePoint, + $node->visibilityConstraints ); } @@ -157,7 +157,7 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content $this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionCatchUpTrigger($contentRepositoryId, $contentRepositorySettings), $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings), - $clock, + $clock ); } catch (\Exception $exception) { throw InvalidConfigurationException::fromException($contentRepositoryId, $exception); diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php index f7184236ed9..c4975d7b9cb 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php @@ -12,7 +12,6 @@ { public function __construct( private ConfigurationManager $configurationManager, - private ObjectManagerBasedNodeLabelGeneratorFactory $nodeLabelGeneratorFactory, private NodeTypeEnrichmentService $nodeTypeEnrichmentService, ) { } @@ -25,8 +24,7 @@ public function build(ContentRepositoryId $contentRepositoryId, array $options): function () { $configuration = $this->configurationManager->getConfiguration('NodeTypes'); return $this->nodeTypeEnrichmentService->enrichNodeTypeLabelsConfiguration($configuration); - }, - $this->nodeLabelGeneratorFactory + } ) ); } diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php b/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php index 2a9e68d90dc..d6b5342d1e9 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php @@ -23,6 +23,7 @@ * We will hopefully get rid of this class at some point; by introducing a NodeAggregate * which will take care of constraint enforcement then. * + * @deprecated remove me https://github.com/neos/neos-development-collection/pull/4988 * @internal */ class CatchUpTriggerWithSynchronousOption implements ProjectionCatchUpTriggerInterface @@ -33,7 +34,10 @@ class CatchUpTriggerWithSynchronousOption implements ProjectionCatchUpTriggerInt */ protected $contentRepositoryRegistry; - private static bool $synchronousEnabled = false; + /** + * Hack by setting to true to be always sync mode: https://github.com/neos/neos-development-collection/pull/4988 + */ + private static bool $synchronousEnabled = true; /** * INTERNAL diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php index 2dcf16f96cc..00a946be01a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php @@ -9,6 +9,9 @@ use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; +use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Model\Event\SequenceNumber; +use Neos\EventStore\Model\EventStream\VirtualStreamName; /** * Content Repository service to perform Projection replays @@ -21,6 +24,7 @@ final class ProjectionReplayService implements ContentRepositoryServiceInterface public function __construct( private readonly Projections $projections, private readonly ContentRepository $contentRepository, + private readonly EventStoreInterface $eventStore, ) { } @@ -49,6 +53,19 @@ public function resetAllProjections(): void } } + public function highestSequenceNumber(): SequenceNumber + { + foreach ($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1) as $eventEnvelope) { + return $eventEnvelope->sequenceNumber; + } + return SequenceNumber::none(); + } + + public function numberOfProjections(): int + { + return count($this->projections); + } + /** * @return class-string> */ diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php index 134d34b4f27..337297d9bb6 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php @@ -24,6 +24,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor return new ProjectionReplayService( $serviceFactoryDependencies->projections, $serviceFactoryDependencies->contentRepository, + $serviceFactoryDependencies->eventStore, ); } } diff --git a/Neos.Fusion/Classes/Core/Cache/FusionContextSerializer.php b/Neos.Fusion/Classes/Core/Cache/FusionContextSerializer.php index 57c06ff3006..60040ff33f0 100644 --- a/Neos.Fusion/Classes/Core/Cache/FusionContextSerializer.php +++ b/Neos.Fusion/Classes/Core/Cache/FusionContextSerializer.php @@ -9,7 +9,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** - * Serializer for Fusion's [at]cache.context values + * Serializer for Fusion's \@cache.context values * * Uses the Flows's property mapper as implementation. * It relies on a converter being available from the context value type to string and reverse. diff --git a/Neos.Fusion/Classes/Core/FusionGlobals.php b/Neos.Fusion/Classes/Core/FusionGlobals.php index 3c1e26ca277..0b81b7f27bb 100644 --- a/Neos.Fusion/Classes/Core/FusionGlobals.php +++ b/Neos.Fusion/Classes/Core/FusionGlobals.php @@ -5,21 +5,31 @@ namespace Neos\Fusion\Core; /** - * Fusion allows to add variable to the context either via - * \@context.foo = "bar" or by leveraging the php api {@see Runtime::pushContext()}. + * Fusion differentiates between dynamic context variables and fixed Fusion globals. * - * Those approaches are highly dynamic and don't guarantee the existence of variables, + * Context variables are allowed to be set via Fusion's \@context.foo = "bar" + * or by leveraging the php api {@see Runtime::pushContext()}. + * + * Context variables are highly dynamic and don't guarantee the existence of a specific variables, * as they have to be explicitly preserved in uncached \@cache segments, * or might accidentally be popped from the stack. * - * The Fusion runtime is instantiated with a set of global variables which contain the EEL helper definitions - * or functions like FlowQuery. Also, variables like "request" are made available via it. + * The Fusion globals are immutable and part of the runtime's constructor. + * A fixed set of global variables which might contain the EEL helper definitions + * or functions like FlowQuery can be passed this way. + * + * Additionally, also special variables like "request" are made available. * - * The "${request}" special case: To make the request available in uncached segments, it would need to be serialized, - * but we don't allow this currently and despite that, it would be absurd to cache a random request. + * The speciality with "request" and similar is that they should be always available but never cached. + * Regular context variables must be serialized to be available in uncached segments, + * but the current request must not be serialized into the cache as it contains user specific information. * This is avoided by always exposing the current action request via the global variable. * * Overriding Fusion globals is disallowed via \@context and {@see Runtime::pushContext()}. + * + * Fusion globals are case-sensitive, though it's not recommend to leverage this behaviour. + * + * @internal The globals will be set inside the FusionView as declared */ final readonly class FusionGlobals { @@ -45,8 +55,13 @@ public static function fromArray(array $variables): self } /** - * You can access the current request like via this getter: - * `$runtime->fusionGlobals->get('request')` + * Access the possible current request or other globals: + * + * $actionRequest = $this->runtime->fusionGlobals->get('request'); + * if (!$actionRequest instanceof ActionRequest) { + * // fallback or error + * } + * */ public function get(string $name): mixed { diff --git a/Neos.Fusion/Classes/Core/IllegalEntryFusionPathValueException.php b/Neos.Fusion/Classes/Core/IllegalEntryFusionPathValueException.php new file mode 100644 index 00000000000..f5f57b8fb1f --- /dev/null +++ b/Neos.Fusion/Classes/Core/IllegalEntryFusionPathValueException.php @@ -0,0 +1,9 @@ +runtime->fusionGlobals->get('request'); + * if (!$actionRequest instanceof ActionRequest) { + * // fallback or error + * } + * + * To get an {@see UriBuilder} proceed with: + * + * $uriBuilder = new UriBuilder(); + * $uriBuilder->setRequest($actionRequest); + * + * WARNING regarding {@see Runtime::getControllerContext()}: + * Invoking this backwards-compatible layer is possibly unsafe, if the rendering was not started + * in {@see self::renderResponse()} or no `request` global is available. This will raise an exception. + * + * @deprecated with Neos 9.0 can be removed with 10 + * @internal + */ +final class LegacyFusionControllerContext +{ + /** + * @Flow\Inject + * @var FlashMessageService + */ + protected $flashMessageService; + + public function __construct( + private readonly ActionRequest $request, + private readonly ActionResponse $legacyActionResponseForCurrentRendering + ) { + } + + /** + * To migrate the use case of fetching the active request, please look into {@see FusionGlobals::get()} instead. + * By convention, an {@see ActionRequest} will be available as `request`: + * + * $actionRequest = $this->runtime->fusionGlobals->get('request'); + * if (!$actionRequest instanceof ActionRequest) { + * // fallback or error + * } + * + * @deprecated with Neos 9.0 can be removed with 10 + */ + public function getRequest(): ActionRequest + { + return $this->request; + } + + /** + * To migrate the use case of getting the UriBuilder please use this instead: + * + * $actionRequest = $this->runtime->fusionGlobals->get('request'); + * if (!$actionRequest instanceof ActionRequest) { + * // fallback or error + * } + * $uriBuilder = new UriBuilder(); + * $uriBuilder->setRequest($actionRequest); + * + * @deprecated with Neos 9.0 can be removed with 10 + */ + public function getUriBuilder(): UriBuilder + { + $uriBuilder = new UriBuilder(); + $uriBuilder->setRequest($this->request); + return $uriBuilder; + } + + /** + * To migrate this use case please use {@see FlashMessageService::getFlashMessageContainerForRequest()} in + * combination with fetching the active request as described here {@see getRequest} instead. + * + * @deprecated with Neos 9.0 can be removed with 10 + */ + public function getFlashMessageContainer(): FlashMessageContainer + { + return $this->flashMessageService->getFlashMessageContainerForRequest($this->request); + } + + /** + * PURELY INTERNAL with partially undefined behaviour!!! + * + * Gives access to the legacy mutable action response simulation {@see Runtime::withSimulatedLegacyControllerContext()} + * + * Initially it was possible to mutate the current response of the active MVC controller through this getter. + * + * While *HIGHLY* internal behaviour and *ONLY* to be used by Neos.Fusion.Form or Neos.Neos:Plugin + * this legacy layer is in place to still allow this functionality. + * + * @deprecated with Neos 9.0 can be removed with 10 + * @internal THIS SHOULD NEVER BE CALLED ON USER-LAND + */ + public function getResponse(): ActionResponse + { + // expose action response to be possibly mutated in neos forms or fusion plugins. + // this behaviour is highly internal and deprecated! + return $this->legacyActionResponseForCurrentRendering; + } + + /** + * The method {@see ControllerContext::getArguments()} was removed without replacement. + */ + // public function getArguments(): Arguments; +} diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 67e121b068b..76f12b0d016 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -11,15 +11,16 @@ * source code. */ +use GuzzleHttp\Psr7\Message; +use GuzzleHttp\Psr7\Response; +use Neos\Http\Factories\StreamFactoryTrait; +use Psr\Http\Message\ResponseInterface; use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; use Neos\Flow\Configuration\Exception\InvalidConfigurationException; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionResponse; -use Neos\Flow\Mvc\Controller\Arguments; -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\Exception\StopActionException; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Security\Exception as SecurityException; use Neos\Fusion\Core\Cache\RuntimeContentCache; @@ -33,6 +34,7 @@ use Neos\Utility\Arrays; use Neos\Utility\ObjectAccess; use Neos\Utility\PositionalArraySorter; +use Psr\Http\Message\StreamInterface; /** * Fusion Runtime @@ -58,6 +60,8 @@ */ class Runtime { + use StreamFactoryTrait; + /** * Internal constants defining how evaluate should work in case of an error */ @@ -116,9 +120,9 @@ class Runtime protected $runtimeConfiguration; /** - * @deprecated + * @deprecated legacy layer {@see self::getControllerContext()} */ - protected ControllerContext $controllerContext; + private ?ActionResponse $legacyActionResponseForCurrentRendering = null; /** * @var array @@ -157,44 +161,6 @@ public function __construct( $this->fusionGlobals = $fusionGlobals; } - /** - * @deprecated {@see self::getControllerContext()} - * @internal - */ - public function setControllerContext(ControllerContext $controllerContext): void - { - $this->controllerContext = $controllerContext; - } - - /** - * Returns the context which has been passed by the currently active MVC Controller - * - * DEPRECATED CONCEPT. We only implement this as backwards-compatible layer. - * - * @deprecated use `Runtime::fusionGlobals->get('request')` instead to get the request. {@see FusionGlobals::get()} - * @internal - */ - public function getControllerContext(): ControllerContext - { - if (isset($this->controllerContext)) { - return $this->controllerContext; - } - - if (!($request = $this->fusionGlobals->get('request')) instanceof ActionRequest) { - throw new Exception(sprintf('Expected Fusion variable "request" to be of type ActionRequest, got value of type "%s".', get_debug_type($request)), 1693558026485); - } - - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return $this->controllerContext = new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - ); - } - /** * Inject settings of this package * @@ -302,6 +268,58 @@ public function getLastEvaluationStatus() return $this->lastEvaluationStatus; } + /** + * Entry point to render a Fusion path with the context. + * + * A ResponseInterface will be returned, if a Neos.Fusion:Http.Message was defined in the entry path, + * or if Neos.Fusion.Form or Neos.Neos:Plugin were used in the path. + * + * In all other simple cases a StreamInterface will be returned. + * + * @param string $entryFusionPath the absolute fusion path to render (without leading slash) + * @param array $contextVariables the context variables that will be available during the rendering. + * @throws IllegalEntryFusionPathValueException The Fusion path rendered to a value that is not a compatible http response body: string|\Stringable|null + */ + public function renderEntryPathWithContext(string $entryFusionPath, array $contextVariables): ResponseInterface|StreamInterface + { + // Like in pushContext, we don't allow to overrule fusion globals + foreach ($contextVariables as $key => $_) { + if ($this->fusionGlobals->has($key)) { + throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1706452063); + } + } + // replace any previously assigned values + $this->pushContextArray($contextVariables); + + return $this->withSimulatedLegacyControllerContext(function () use ($entryFusionPath) { + try { + $output = $this->render($entryFusionPath); + } catch (RuntimeException $exception) { + throw $exception->getWrappedException(); + } finally { + $this->popContext(); + } + + // Parse potential raw http response possibly rendered via "Neos.Fusion:Http.Message" + /** {@see \Neos\Fusion\FusionObjects\HttpResponseImplementation} */ + $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); + if ($outputStringHasHttpPreamble) { + return Message::parseResponse($output); + } + + if ($output instanceof StreamInterface) { + // if someone manages to return a stream *g + return $output; + } + + if (!is_string($output) && !$output instanceof \Stringable && $output !== null) { + throw new IllegalEntryFusionPathValueException(sprintf('Fusion entry path "%s" is expected to render a compatible http response body: string|\Stringable|null. Got %s instead.', $entryFusionPath, get_debug_type($output)), 1706454898); + } + + return $this->createStream((string)$output); + }); + } + /** * Render an absolute Fusion path and return the result. * @@ -629,7 +647,7 @@ protected function prepareContextForFusionObject(AbstractFusionObject $fusionObj $newContextArray ??= $this->currentContext; foreach ($fusionConfiguration['__meta']['context'] as $contextKey => $contextValue) { if ($this->fusionGlobals->has($contextKey)) { - throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1694247627130); + throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1706452069); } $newContextArray[$contextKey] = $this->evaluate($fusionPath . '/__meta/context/' . $contextKey, $fusionObject, self::BEHAVIOR_EXCEPTION); } @@ -933,6 +951,79 @@ protected function throwExceptionForUnrenderablePathIfNeeded($fusionPath, $fusio } } + /** + * Implements the legacy controller context simulation {@see self::getControllerContext()} + * + * Initially it was possible to mutate the current response of the active MVC controller through $response. + * While HIGHLY internal behaviour and ONLY to be used by Neos.Fusion.Form or Neos.Neos:Plugin + * this legacy layer is in place to still allow this functionality. + * + * @param \Closure(): (ResponseInterface|StreamInterface) $renderer + */ + private function withSimulatedLegacyControllerContext(\Closure $renderer): ResponseInterface|StreamInterface + { + if ($this->legacyActionResponseForCurrentRendering !== null) { + throw new Exception('Recursion detected in `Runtime::renderResponse`. This entry point is only allowed to be invoked once per rendering.', 1706993940); + } + $this->legacyActionResponseForCurrentRendering = new ActionResponse(); + + // actual rendering + $httpResponseOrStream = null; + try { + $httpResponseOrStream = $renderer(); + } finally { + $toBeMergedLegacyActionResponse = $this->legacyActionResponseForCurrentRendering; + // reset for next render + $this->legacyActionResponseForCurrentRendering = null; + } + + // transfer possible headers that have been set dynamically + foreach ($toBeMergedLegacyActionResponse->buildHttpResponse()->getHeaders() as $name => $values) { + if ($httpResponseOrStream instanceof StreamInterface) { + $httpResponseOrStream = new Response(body: $httpResponseOrStream); + } + $httpResponseOrStream = $httpResponseOrStream->withAddedHeader($name, $values); + } + // if the status code is 200 we assume it's the default and will not overrule it + if ($toBeMergedLegacyActionResponse->getStatusCode() !== 200) { + if ($httpResponseOrStream instanceof StreamInterface) { + $httpResponseOrStream = new Response(body: $httpResponseOrStream); + } + $httpResponseOrStream = $httpResponseOrStream->withStatus($toBeMergedLegacyActionResponse->getStatusCode()); + } + + return $httpResponseOrStream; + } + + /** + * The concept of the controller context inside Fusion has been deprecated. + * + * For further information and migration strategies, please look into {@see LegacyFusionControllerContext} + * + * WARNING: + * Invoking this backwards-compatible layer is possibly unsafe, if the rendering was not started + * in {@see self::renderEntryPathWithContext()} or no `request` global is available. This will raise an exception. + * + * @deprecated with Neos 9.0 + * @internal + * @throws Exception if unsafe call + */ + public function getControllerContext(): LegacyFusionControllerContext + { + // legacy controller context layer + $actionRequest = $this->fusionGlobals->get('request'); + if ($this->legacyActionResponseForCurrentRendering === null || !$actionRequest instanceof ActionRequest) { + throw new Exception(sprintf('Fusions simulated legacy controller context is only available inside `Runtime::renderResponse` and when the Fusion global "request" is an ActionRequest.'), 1706458355); + } + + return new LegacyFusionControllerContext( + $actionRequest, + // expose action response to be possibly mutated in neos forms or fusion plugins. + // this behaviour is highly internal and deprecated! + $this->legacyActionResponseForCurrentRendering + ); + } + /** * Configures this runtime to override the default exception handler configured in the settings * or via Fusion's \@exceptionHandler {@see AbstractRenderingExceptionHandler}. diff --git a/Neos.Fusion/Classes/Core/RuntimeFactory.php b/Neos.Fusion/Classes/Core/RuntimeFactory.php index 8cd07a696d8..fc5d84bd757 100644 --- a/Neos.Fusion/Classes/Core/RuntimeFactory.php +++ b/Neos.Fusion/Classes/Core/RuntimeFactory.php @@ -15,10 +15,7 @@ use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; -use Neos\Flow\Mvc\Controller\Arguments; use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Flow\Mvc\Routing\UriBuilder; /** * @Flow\Scope("singleton") @@ -45,20 +42,18 @@ class RuntimeFactory */ public function create(array $fusionConfiguration, ControllerContext $controllerContext = null): Runtime { - if ($controllerContext === null) { - $controllerContext = self::createControllerContextFromEnvironment(); - } $defaultContextVariables = EelUtility::getDefaultContextVariables( $this->defaultContextConfiguration ?? [] ); - $runtime = new Runtime( + return new Runtime( FusionConfiguration::fromArray($fusionConfiguration), FusionGlobals::fromArray( - ['request' => $controllerContext->getRequest(), ...$defaultContextVariables] + [ + ...$defaultContextVariables, + 'request' => $controllerContext?->getRequest() ?? ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()), + ] ) ); - $runtime->setControllerContext($controllerContext); - return $runtime; } public function createFromConfiguration( @@ -82,21 +77,4 @@ public function createFromSourceCode( $fusionGlobals ); } - - private static function createControllerContextFromEnvironment(): ControllerContext - { - $httpRequest = ServerRequest::fromGlobals(); - - $request = ActionRequest::fromHttpRequest($httpRequest); - - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - ); - } } diff --git a/Neos.Fusion/Classes/Exception/RuntimeException.php b/Neos.Fusion/Classes/Exception/RuntimeException.php index 8ee31394250..b827edf4b74 100644 --- a/Neos.Fusion/Classes/Exception/RuntimeException.php +++ b/Neos.Fusion/Classes/Exception/RuntimeException.php @@ -32,4 +32,13 @@ public function getFusionPath() { return $this->fusionPath; } + + /** + * Unwrap this Fusion RuntimeException + */ + public function getWrappedException(): \Exception + { + /** @phpstan-ignore-next-line due to overridden construction, we are sure that the previous exists. */ + return $this->getPrevious(); + } } diff --git a/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php b/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php index 06804001fa0..ce4603163db 100644 --- a/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php @@ -116,10 +116,12 @@ public function evaluate() } else { $package = $this->getPackage(); if ($package === null) { - $controllerContext = $this->runtime->getControllerContext(); - /** @var $actionRequest ActionRequest */ - $actionRequest = $controllerContext->getRequest(); - $package = $actionRequest->getControllerPackageKey(); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $package = $possibleRequest->getControllerPackageKey(); + } else { + throw new \RuntimeException('Could not infer package-key from action request. Please render Fusion with request or specify a package-key.', 1706624314); + } } } $localize = $this->isLocalize(); diff --git a/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php b/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php index 2287410a718..edec81b35c9 100644 --- a/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php @@ -79,7 +79,7 @@ public function getPath() */ public function evaluate() { - $actionRequest = $this->runtime->getControllerContext()->getRequest(); + $actionRequest = $this->runtime->fusionGlobals->get('request'); if (!$actionRequest instanceof ActionRequest) { $actionRequest = null; } @@ -140,7 +140,7 @@ public function evaluate() if ($sectionName !== null) { return $fluidTemplate->renderSection($sectionName); } else { - return $fluidTemplate->render(); + return $fluidTemplate->render()->getContents(); } } diff --git a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php index ce3ef08ae15..f59cdde7359 100644 --- a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php @@ -11,6 +11,9 @@ * source code. */ +use GuzzleHttp\Psr7\ServerRequest; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Routing\UriBuilder; /** * A Fusion UriBuilder object @@ -150,8 +153,19 @@ public function isAbsolute() */ public function evaluate() { - $controllerContext = $this->runtime->getControllerContext(); - $uriBuilder = $controllerContext->getUriBuilder()->reset(); + $uriBuilder = new UriBuilder(); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $uriBuilder->setRequest($possibleRequest); + } else { + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without + // even, if the default param merging would not be required + // this will improve with a reformed uri building: + // https://github.com/neos/flow-development-collection/pull/2744 + $uriBuilder->setRequest( + ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) + ); + } $format = $this->getFormat(); if ($format !== null) { diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index bf983346f4f..d6c9c407ac2 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -13,6 +13,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\View\AbstractView; use Neos\Fusion\Core\FusionConfiguration; use Neos\Fusion\Core\FusionGlobals; @@ -21,7 +22,8 @@ use Neos\Fusion\Core\Parser; use Neos\Fusion\Core\Runtime; use Neos\Fusion\Core\RuntimeFactory; -use Neos\Fusion\Exception\RuntimeException; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; /** * View for using Fusion for standard MVC controllers. @@ -43,7 +45,7 @@ class FusionView extends AbstractView protected $supportedOptions = [ 'fusionPathPatterns' => [['resource://@package/Private/Fusion'], 'Fusion files that will be loaded if directories are given the Root.fusion is used.', 'array'], 'fusionPath' => [null, 'The Fusion path which should be rendered; derived from the controller and action names or set by the user.', 'string'], - 'fusionGlobals' => [null, 'Additional global variables; merged together with the "request". Must only be specified at creation.', FusionGlobals::class], + 'fusionGlobals' => [null, 'Additional Fusion global variables. The request must be assigned using `assign`. For regular variables please use `assign` as well.', 'array'], 'packageKey' => [null, 'The package key where the Fusion should be loaded from. If not given, is automatically derived from the current request.', 'string'], 'debugMode' => [false, 'Flag to enable debug mode of the Fusion runtime explicitly (overriding the global setting).', 'boolean'], 'enableContentCache' => [false, 'Flag to enable content caching inside Fusion (overriding the global setting).', 'boolean'] @@ -81,6 +83,12 @@ class FusionView extends AbstractView */ protected $fusionRuntime = null; + /** + * Via {@see assign} request using the "request" key, + * will be available also as Fusion global in the runtime. + */ + protected ?ActionRequest $assignedActionRequest = null; + /** * Reset runtime cache if an option is changed * @@ -90,10 +98,42 @@ class FusionView extends AbstractView */ public function setOption($optionName, $value) { + // todo do we want to allow to set the `fusionPathPatterns` after the first render? $this->fusionPath = null; parent::setOption($optionName, $value); } + /** + * @return $this for chaining + */ + public function assign(string $key, mixed $value): self + { + if ($key === 'request') { + // the request cannot be used as "normal" fusion variable and must be treated as FusionGlobal + // to for example not cache it accidentally + // additionally we need it for special request based handling in the view + $this->assignedActionRequest = $value; + return $this; + } + return parent::assign($key, $value); + } + + /** + * Legacy layer to set the request for this view if not set already. + * + * Please use {@see assign} with "request" instead + * + * $view->assign('request"', $this->request) + * + * @deprecated with Neos 9 + */ + public function setControllerContext(ControllerContext $controllerContext) + { + if (!$this->assignedActionRequest) { + $this->assignedActionRequest = $controllerContext->getRequest(); + } + } + /** * Sets the Fusion path to be rendered to an explicit value; * to be used mostly inside tests. @@ -137,15 +177,19 @@ public function setFusionPathPatterns(array $pathPatterns) } /** - * Render the view + * Render the view to a full response in case a Neos.Fusion:Http.Message was used. + * If the fusion path contains a simple string a stream will be rendered. * - * @return mixed The rendered view + * @return ResponseInterface|StreamInterface * @api */ - public function render() + public function render(): ResponseInterface|StreamInterface { $this->initializeFusionRuntime(); - return $this->renderFusion(); + return $this->fusionRuntime->renderEntryPathWithContext( + $this->getFusionPathForCurrentRequest(), + $this->variables + ); } /** @@ -159,26 +203,16 @@ public function initializeFusionRuntime() { if ($this->fusionRuntime === null) { $this->loadFusion(); - - $fusionGlobals = $this->options['fusionGlobals'] ?? FusionGlobals::empty(); - if (!$fusionGlobals instanceof FusionGlobals) { - throw new \InvalidArgumentException('View option "fusionGlobals" must be of type FusionGlobals', 1694252923947); + $additionalGlobals = FusionGlobals::fromArray($this->options['fusionGlobals'] ?? []); + if ($additionalGlobals->has('request')) { + throw new \RuntimeException(sprintf('The request cannot be set using the additional fusion globals. Please use $view->assign("request", ...) instead.'), 1708854895); } - $fusionGlobals = $fusionGlobals->merge( - FusionGlobals::fromArray( - array_filter([ - 'request' => $this->controllerContext?->getRequest() - ]) - ) - ); - $this->fusionRuntime = $this->runtimeFactory->createFromConfiguration( $this->parsedFusion, - $fusionGlobals + $this->assignedActionRequest + ? $additionalGlobals->merge(FusionGlobals::fromArray(['request' => $this->assignedActionRequest])) + : $additionalGlobals ); - if (isset($this->controllerContext)) { - $this->fusionRuntime->setControllerContext($this->controllerContext); - } } if (isset($this->options['debugMode'])) { $this->fusionRuntime->setDebugMode($this->options['debugMode']); @@ -250,11 +284,10 @@ protected function getPackageKey() if ($packageKey !== null) { return $packageKey; } else { - $request = $this->controllerContext?->getRequest(); - if (!$request) { - throw new \RuntimeException(sprintf('To resolve the @package in all fusionPathPatterns, either packageKey has to be specified, or the current request be available.')); + if (!$this->assignedActionRequest) { + throw new \RuntimeException(sprintf('To resolve the @package in all fusionPathPatterns, either packageKey has to be specified, or the current request be available.'), 1708267874); } - return $request->getControllerPackageKey(); + return $this->assignedActionRequest->getControllerPackageKey(); } } @@ -270,8 +303,10 @@ protected function getFusionPathForCurrentRequest() if ($fusionPath !== null) { $this->fusionPath = $fusionPath; } else { - /** @var $request ActionRequest */ - $request = $this->controllerContext->getRequest(); + $request = $this->assignedActionRequest; + if (!$request) { + throw new \RuntimeException(sprintf('The option `fusionPath` was not set. Could not fallback to the current request.'), 1708267857); + } $fusionPathForCurrentRequest = $request->getControllerObjectName(); $fusionPathForCurrentRequest = str_replace('\\Controller\\', '\\', $fusionPathForCurrentRequest); $fusionPathForCurrentRequest = str_replace('\\', '/', $fusionPathForCurrentRequest); @@ -283,21 +318,4 @@ protected function getFusionPathForCurrentRequest() } return $this->fusionPath; } - - /** - * Render the given Fusion and return the rendered page - * @return mixed - * @throws \Exception - */ - protected function renderFusion() - { - $this->fusionRuntime->pushContextArray($this->variables); - try { - $output = $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); - } catch (RuntimeException $exception) { - throw $exception->getPrevious(); - } - $this->fusionRuntime->popContext(); - return $output; - } } diff --git a/Neos.Fusion/Tests/Functional/FusionObjects/AbstractFusionObjectTest.php b/Neos.Fusion/Tests/Functional/FusionObjects/AbstractFusionObjectTest.php index 810452e8a2b..452b61d6e6e 100644 --- a/Neos.Fusion/Tests/Functional/FusionObjects/AbstractFusionObjectTest.php +++ b/Neos.Fusion/Tests/Functional/FusionObjects/AbstractFusionObjectTest.php @@ -11,14 +11,12 @@ * source code. */ +use GuzzleHttp\Psr7\ServerRequest; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; -use Neos\Flow\Mvc\Controller\Arguments; -use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\Tests\FunctionalTestCase; -use Neos\Fusion\View\FusionView; -use Psr\Http\Message\ServerRequestFactoryInterface; +use Neos\Fusion\Core\FusionGlobals; +use Neos\Fusion\Core\FusionSourceCodeCollection; +use Neos\Fusion\Core\RuntimeFactory; /** * Testcase for the Fusion View @@ -27,39 +25,21 @@ abstract class AbstractFusionObjectTest extends FunctionalTestCase { /** - * @var ControllerContext + * @var ActionRequest */ - protected $controllerContext; + protected $request; - /** - * Helper to build a Fusion view object - * - * @return FusionView - */ - protected function buildView() + protected function buildView(): TestingViewForFusionRuntime { - $view = new FusionView(); + $this->request = ActionRequest::fromHttpRequest(new ServerRequest('GET', 'http://localhost/')); - /** @var ServerRequestFactoryInterface $httpRequestFactory */ - $httpRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class); - $httpRequest = $httpRequestFactory->createServerRequest('GET', 'http://localhost/'); - $request = ActionRequest::fromHttpRequest($httpRequest); - - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - $this->controllerContext = new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder + $runtime = $this->objectManager->get(RuntimeFactory::class)->createFromSourceCode( + FusionSourceCodeCollection::fromFilePath(__DIR__ . '/Fixtures/Fusion/Root.fusion'), + FusionGlobals::fromArray(['request' => $this->request]) ); - $view->setControllerContext($this->controllerContext); - $view->setPackageKey('Neos.Fusion'); - $view->setFusionPathPattern(__DIR__ . '/Fixtures/Fusion'); + $view = new TestingViewForFusionRuntime($runtime); $view->assign('fixtureDirectory', __DIR__ . '/Fixtures/'); - return $view; } diff --git a/Neos.Fusion/Tests/Functional/FusionObjects/ContentCacheTest.php b/Neos.Fusion/Tests/Functional/FusionObjects/ContentCacheTest.php index b98dfc1a503..a6e1c494449 100644 --- a/Neos.Fusion/Tests/Functional/FusionObjects/ContentCacheTest.php +++ b/Neos.Fusion/Tests/Functional/FusionObjects/ContentCacheTest.php @@ -13,7 +13,6 @@ use Neos\Flow\Cache\CacheManager; use Neos\Cache\Frontend\FrontendInterface; -use Neos\Flow\Mvc\ActionRequest; use Neos\Fusion\Core\Cache\ContentCache; use Neos\Fusion\Tests\Functional\FusionObjects\Fixtures\Model\TestModel; @@ -331,8 +330,7 @@ public function conditionsAreAppliedForUncachedSegment() 'object' => $object ]); - /** @var \Neos\Flow\Mvc\ActionRequest $actionRequest */ - $actionRequest = $this->controllerContext->getRequest(); + $actionRequest = $this->request; $actionRequest->setArgument('currentPage', 1); $firstRenderResult = $view->render(); @@ -729,8 +727,7 @@ public function contextIsCorrectlyEvaluated() $view->assign('someContextVariable', 'prettyUnused'); $view->setFusionPath('contentCache/dynamicWithChangingDiscriminator'); - /** @var ActionRequest $actionRequest */ - $actionRequest = $this->controllerContext->getRequest(); + $actionRequest = $this->request; $actionRequest->setArgument('testArgument', '1'); $firstRenderResult = $view->render(); diff --git a/Neos.Fusion/Tests/Functional/FusionObjects/ReservedKeysTest.php b/Neos.Fusion/Tests/Functional/FusionObjects/ReservedKeysTest.php index d0789617ca2..cbf2d3b72e1 100644 --- a/Neos.Fusion/Tests/Functional/FusionObjects/ReservedKeysTest.php +++ b/Neos.Fusion/Tests/Functional/FusionObjects/ReservedKeysTest.php @@ -11,6 +11,9 @@ * source code. */ +use Neos\Fusion\Core\FusionGlobals; +use Neos\Fusion\Core\FusionSourceCodeCollection; +use Neos\Fusion\Core\RuntimeFactory; use Neos\Fusion\Exception; /** @@ -25,9 +28,10 @@ class ReservedKeysTest extends AbstractFusionObjectTest public function usingReservedKeysThrowsException() { $this->expectException(Exception::class); - $view = $this->buildView(); - $view->setFusionPathPattern(__DIR__ . '/Fixtures/ReservedKeysFusion'); - $view->render(); + $this->objectManager->get(RuntimeFactory::class)->createFromSourceCode( + FusionSourceCodeCollection::fromFilePath(__DIR__ . '/Fixtures/ReservedKeysFusion/ReservedKeys.fusion'), + FusionGlobals::empty() + ); } /** diff --git a/Neos.Fusion/Tests/Functional/FusionObjects/TestingViewForFusionRuntime.php b/Neos.Fusion/Tests/Functional/FusionObjects/TestingViewForFusionRuntime.php new file mode 100644 index 00000000000..aa03688ce27 --- /dev/null +++ b/Neos.Fusion/Tests/Functional/FusionObjects/TestingViewForFusionRuntime.php @@ -0,0 +1,67 @@ +fusionPath = $fusionPath; + } + + public function assign($key, $value) + { + $this->runtime->pushContext($key, $value); + } + + public function setOption($key, $value) + { + match ($key) { + 'enableContentCache' => $this->runtime->setEnableContentCache($value), + 'debugMode' => $this->runtime->setDebugMode($value) + }; + } + + public function assignMultiple(array $values) + { + foreach ($values as $key => $value) { + $this->runtime->pushContext($key, $value); + } + } + + public function render() + { + try { + return $this->runtime->render($this->fusionPath); + } catch (RuntimeException $e) { + throw $e->getWrappedException(); + } + } +} diff --git a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/HttpResponse.fusion b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/HttpResponse.fusion new file mode 100644 index 00000000000..d75b5c47d1f --- /dev/null +++ b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/HttpResponse.fusion @@ -0,0 +1,9 @@ + +response = Neos.Fusion:Http.Message { + httpResponseHead { + statusCode = 404 + headers.Content-Type = 'application/json' + } + + body = '{"some":"json"}' +} diff --git a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/IllegalEntryPointValue.fusion b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/IllegalEntryPointValue.fusion new file mode 100644 index 00000000000..03a1c95fa61 --- /dev/null +++ b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/IllegalEntryPointValue.fusion @@ -0,0 +1,4 @@ + +illegalEntryPointValue = Neos.Fusion:DataStructure { + my = 'array' +} diff --git a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion index b190f6bd5a0..fbcf0fc40a3 100644 --- a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion +++ b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion @@ -1 +1,5 @@ include: ./**/*.fusion +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Join.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/DataStructure.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.Message.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.ResponseHead.fusion' diff --git a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php index dc2f06d8ba9..e15f978159a 100644 --- a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php +++ b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php @@ -12,9 +12,11 @@ */ use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Tests\FunctionalTestCase; +use Neos\Fusion\Core\IllegalEntryFusionPathValueException; use Neos\Fusion\View\FusionView; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; /** * Testcase for the Fusion View @@ -23,45 +25,71 @@ class FusionViewTest extends FunctionalTestCase { /** - * @var ControllerContext + * @test */ - protected $mockControllerContext; + public function fusionViewIsUsedForRendering() + { + $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); + self::assertEquals('X', $view->render()->getContents()); + } /** - * Initializer + * @test */ - public function setUp(): void + public function fusionViewUsesGivenPathIfSet() { - $this->mockControllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); + $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); + $view->setFusionPath('foo/bar'); + self::assertEquals('Xfoobar', $view->render()->getContents()); } /** * @test */ - public function fusionViewIsUsedForRendering() + public function fusionViewOutputsVariable() { $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); - self::assertEquals('X', $view->render()); + $view->assign('test', 'Hallo Welt'); + self::assertEquals('XHallo Welt', $view->render()->getContents()); } /** * @test */ - public function fusionViewUsesGivenPathIfSet() + public function fusionViewReturnsStreamInterface() { $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); - $view->setFusionPath('foo/bar'); - self::assertEquals('Xfoobar', $view->render()); + $view->assign('test', 'Hallo Welt'); + $response = $view->render(); + self::assertInstanceOf(StreamInterface::class, $response); + self::assertEquals('XHallo Welt', $response->getContents()); } /** * @test */ - public function fusionViewOutputsVariable() + public function fusionViewReturnsHttpResponseFromHttpMessagePrototype() { $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); - $view->assign('test', 'Hallo Welt'); - self::assertEquals('XHallo Welt', $view->render()); + $view->setFusionPath('response'); + $response = $view->render(); + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertSame('{"some":"json"}', $response->getBody()->getContents()); + self::assertSame(404, $response->getStatusCode()); + self::assertSame('application/json', $response->getHeaderLine('Content-Type')); + } + + /** + * @test + */ + public function fusionViewCannotRenderNonStringableValue() + { + $this->expectException(IllegalEntryFusionPathValueException::class); + $this->expectExceptionMessage('Fusion entry path "illegalEntryPointValue" is expected to render a compatible http response body: string|\Stringable|null. Got array instead.'); + + $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); + $view->setFusionPath('illegalEntryPointValue'); + $view->render(); } /** @@ -76,10 +104,9 @@ protected function buildView($controllerObjectName, $controllerActionName) $request = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock(); $request->expects(self::any())->method('getControllerObjectName')->will(self::returnValue($controllerObjectName)); $request->expects(self::any())->method('getControllerActionName')->will(self::returnValue($controllerActionName)); - $this->mockControllerContext->expects(self::any())->method('getRequest')->will(self::returnValue($request)); $view = new FusionView(); - $view->setControllerContext($this->mockControllerContext); + $view->assign('request', $request); $view->setFusionPathPattern(__DIR__ . '/Fixtures/Fusion'); return $view; diff --git a/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php b/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php index bc9920a0140..17125933372 100644 --- a/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php +++ b/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Message; use Neos\Eel\EelEvaluatorInterface; use Neos\Eel\ProtectedContext; use Neos\Flow\Exception; @@ -19,9 +20,12 @@ use Neos\Fusion\Core\ExceptionHandlers\ThrowingHandler; use Neos\Fusion\Core\FusionConfiguration; use Neos\Fusion\Core\FusionGlobals; +use Neos\Fusion\Core\IllegalEntryFusionPathValueException; use Neos\Fusion\Core\Runtime; use Neos\Fusion\Exception\RuntimeException; use Neos\Fusion\FusionObjects\ValueImplementation; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; class RuntimeTest extends UnitTestCase { @@ -36,9 +40,9 @@ public function renderHandlesExceptionDuringRendering() $runtimeException = new RuntimeException('I am a parent exception', 123, new Exception('I am a previous exception'), 'root'); $runtime = $this->getMockBuilder(Runtime::class)->onlyMethods(['evaluate', 'handleRenderingException'])->disableOriginalConstructor()->getMock(); $runtime->expects(self::any())->method('evaluate')->will(self::throwException($runtimeException)); - $runtime->expects(self::once())->method('handleRenderingException')->with('/foo/bar', $runtimeException)->will(self::returnValue('Exception Message')); + $runtime->expects(self::once())->method('handleRenderingException')->with('foo/bar', $runtimeException)->will(self::returnValue('Exception Message')); - $output = $runtime->render('/foo/bar'); + $output = $runtime->render('foo/bar'); self::assertEquals('Exception Message', $output); } @@ -63,7 +67,7 @@ public function handleRenderingExceptionThrowsException() $objectManager->expects(self::once())->method('isRegistered')->with($exceptionHandlerSetting)->will(self::returnValue(true)); $objectManager->expects(self::once())->method('get')->with($exceptionHandlerSetting)->will(self::returnValue(new ThrowingHandler())); - $runtime->handleRenderingException('/foo/bar', $runtimeException); + $runtime->handleRenderingException('foo/bar', $runtimeException); } /** @@ -119,7 +123,7 @@ public function renderRethrowsSecurityExceptions() $runtime = $this->getMockBuilder(Runtime::class)->onlyMethods(['evaluate', 'handleRenderingException'])->disableOriginalConstructor()->getMock(); $runtime->expects(self::any())->method('evaluate')->will(self::throwException($securityException)); - $runtime->render('/foo/bar'); + $runtime->render('foo/bar'); } /** @@ -207,6 +211,18 @@ public function pushContextIsNotAllowedToOverrideFusionGlobals() $runtime->pushContext('request', 'anything'); } + /** + * @test + */ + public function renderResponseIsNotAllowedToOverrideFusionGlobals() + { + $this->expectException(\Neos\Fusion\Exception::class); + $this->expectExceptionMessage('Overriding Fusion global variable "request" via @context is not allowed.'); + $runtime = new Runtime(FusionConfiguration::fromArray([]), FusionGlobals::fromArray(['request' => 'fixed'])); + + $runtime->renderEntryPathWithContext('foo', ['request' =>'anything']); + } + /** * Legacy compatible layer to possibly override fusion globals like "request". * This functionality is only allowed for internal packages. @@ -222,4 +238,148 @@ public function pushContextArrayIsAllowedToOverrideFusionGlobals() $runtime->pushContextArray(['bing' => 'beer', 'request' => 'anything']); self::assertTrue(true); } + + public static function renderStreamExamples(): iterable + { + yield 'simple string' => [ + 'rawValue' => 'my string', + 'streamContents' => 'my string' + ]; + + yield 'string cast object (\Stringable)' => [ + 'rawValue' => new class implements \Stringable { + public function __toString() + { + return 'my string karsten'; + } + }, + 'streamContents' => 'my string karsten' + ]; + + yield 'empty string' => [ + 'rawValue' => '', + 'streamContents' => '' + ]; + + yield 'null value' => [ + 'rawValue' => null, + 'streamContents' => '' + ]; + } + + public function renderResponseExamples(): iterable + { + yield 'stringified http response string is upcasted' => [ + 'rawValue' => <<<'TEXT' + HTTP/1.1 418 OK + Content-Type: text/html + X-MyCustomHeader: marc + + + + Hello World + TEXT, + 'response' => <<<'TEXT' + HTTP/1.1 418 OK + Content-Type: text/html + X-MyCustomHeader: marc + + + + Hello World + TEXT + ]; + } + + /** + * @test + * @dataProvider renderStreamExamples + */ + public function renderEntryPathStream(mixed $rawValue, string $expectedStreamContents) + { + $runtime = $this->getMockBuilder(Runtime::class) + ->setConstructorArgs([FusionConfiguration::fromArray([]), FusionGlobals::empty()]) + ->onlyMethods(['render']) + ->getMock(); + + $runtime->expects(self::once())->method('render')->willReturn( + $rawValue + ); + + $response = $runtime->renderEntryPathWithContext('path', []); + + self::assertInstanceOf(StreamInterface::class, $response); + self::assertSame($expectedStreamContents, $response->getContents()); + } + + /** + * @test + * @dataProvider renderResponseExamples + */ + public function renderEntryPathResponse(mixed $rawValue, string $expectedHttpResponseString) + { + $runtime = $this->getMockBuilder(Runtime::class) + ->setConstructorArgs([FusionConfiguration::fromArray([]), FusionGlobals::empty()]) + ->onlyMethods(['render']) + ->getMock(); + + $runtime->expects(self::once())->method('render')->willReturn( + is_string($rawValue) ? str_replace("\n", "\r\n", $rawValue) : $rawValue + ); + + $response = $runtime->renderEntryPathWithContext('path', []); + + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertSame(str_replace("\n", "\r\n", $expectedHttpResponseString), Message::toString($response)); + } + + public static function renderResponseIllegalValueExamples(): iterable + { + yield 'array' => [ + 'rawValue' => ['my' => 'array', 'with' => 'values'] + ]; + + yield '\stdClass' => [ + 'rawValue' => (object)[] + ]; + + yield '\JsonSerializable' => [ + 'rawValue' => new class implements \JsonSerializable { + public function jsonSerialize(): mixed + { + return 123; + } + } + ]; + + yield 'any class' => [ + 'rawValue' => new class { + } + ]; + + yield 'boolean' => [ + 'rawValue' => false + ]; + } + + /** + * @dataProvider renderResponseIllegalValueExamples + * @test + */ + public function renderResponseThrowsIfNotStringable(mixed $illegalValue) + { + $this->expectException(IllegalEntryFusionPathValueException::class); + $this->expectExceptionMessage(sprintf('Fusion entry path "path" is expected to render a compatible http response body: string|\Stringable|null. Got %s instead.', get_debug_type($illegalValue))); + + $runtime = $this->getMockBuilder(Runtime::class) + ->setConstructorArgs([FusionConfiguration::fromArray([]), FusionGlobals::empty()]) + ->onlyMethods(['render']) + ->getMock(); + + $runtime->expects(self::once())->method('render')->willReturn( + $illegalValue + ); + + $runtime->renderEntryPathWithContext('path', []); + } } diff --git a/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php b/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php index 742f53d1cc9..a0cc8983cb1 100644 --- a/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php +++ b/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php @@ -13,10 +13,11 @@ use Neos\Flow\I18n\Service; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\ResourceManagement\PersistentResource; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\Tests\UnitTestCase; +use Neos\Fusion\Core\FusionConfiguration; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; use Neos\Fusion\Exception; use Neos\Fusion\FusionObjects\ResourceUriImplementation; @@ -46,11 +47,6 @@ class ResourceUriImplementationTest extends UnitTestCase */ protected $mockI18nService; - /** - * @var ControllerContext - */ - protected $mockControllerContext; - /** * @var ActionRequest */ @@ -58,14 +54,12 @@ class ResourceUriImplementationTest extends UnitTestCase public function setUp(): void { - $this->mockRuntime = $this->getMockBuilder(Runtime::class)->disableOriginalConstructor()->getMock(); - - $this->mockControllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); - $this->mockActionRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock(); - $this->mockControllerContext->expects(self::any())->method('getRequest')->will(self::returnValue($this->mockActionRequest)); - $this->mockRuntime->expects(self::any())->method('getControllerContext')->will(self::returnValue($this->mockControllerContext)); + $this->mockRuntime = $this->getMockBuilder(Runtime::class)->setConstructorArgs([ + FusionConfiguration::fromArray([]), + FusionGlobals::fromArray(['request' => $this->mockActionRequest]) + ])->getMock(); $this->resourceUriImplementation = new ResourceUriImplementation($this->mockRuntime, 'resourceUri/test', 'Neos.Fusion:ResourceUri'); diff --git a/Neos.Media.Browser/Classes/Controller/UsageController.php b/Neos.Media.Browser/Classes/Controller/UsageController.php index e53cfa0df5a..2fd3b3f8ef8 100644 --- a/Neos.Media.Browser/Classes/Controller/UsageController.php +++ b/Neos.Media.Browser/Classes/Controller/UsageController.php @@ -98,7 +98,10 @@ public function relatedNodesAction(AssetInterface $asset) $contentRepository = $this->contentRepositoryRegistry->get($usage->getContentRepositoryId()); - $nodeAggregate = $contentRepository->getContentGraph()->findNodeAggregateById( + $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($usage->getContentStreamId()); + + // FIXME: AssetUsageReference->workspaceName ? + $nodeAggregate = $contentRepository->getContentGraph($workspace->workspaceName)->findNodeAggregateById( $usage->getContentStreamId(), $usage->getNodeAggregateId() ); @@ -107,7 +110,7 @@ public function relatedNodesAction(AssetInterface $asset) } catch (NodeTypeNotFoundException $e) { $nodeType = null; } - $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($usage->getContentStreamId()); + $accessible = $this->domainUserService->currentUserCanReadWorkspace($workspace); $inaccessibleRelation['nodeIdentifier'] = $usage->getNodeAggregateId()->value; @@ -121,7 +124,7 @@ public function relatedNodesAction(AssetInterface $asset) continue; } - $subgraph = $contentRepository->getContentGraph()->getSubgraph( + $subgraph = $contentRepository->getContentGraph($workspace->workspaceName)->getSubgraph( $usage->getContentStreamId(), $usage->getOriginDimensionSpacePoint()->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions() diff --git a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncService.php b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncService.php index 02a77386db9..f73bf9865d5 100644 --- a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncService.php +++ b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncService.php @@ -4,8 +4,8 @@ namespace Neos\Neos\AssetUsage\Service; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Repository\AssetRepository; @@ -26,8 +26,8 @@ class AssetUsageSyncService implements ContentRepositoryServiceInterface private array $existingAssetsById = []; public function __construct( + private readonly ContentRepository $contentRepository, private readonly AssetUsageFinder $assetUsageFinder, - private readonly ContentGraphInterface $contentGraph, private readonly AssetRepository $assetRepository, private readonly AssetUsageRepository $assetUsageRepository, ) { @@ -55,8 +55,12 @@ public function isAssetUsageStillValid(AssetUsage $usage): bool } $dimensionSpacePoint = $usage->originDimensionSpacePoint->toDimensionSpacePoint(); - $subGraph = $this->contentGraph->getSubgraph( - $usage->contentStreamId, + // FIXME: AssetUsage->workspaceName ? + $workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($usage->contentStreamId); + if (is_null($workspace)) { + return false; + } + $subGraph = $this->contentRepository->getContentGraph($workspace->workspaceName)->getSubgraph( $dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); diff --git a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncServiceFactory.php b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncServiceFactory.php index 524ea542c12..2e798ecf25d 100644 --- a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncServiceFactory.php +++ b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageSyncServiceFactory.php @@ -26,8 +26,8 @@ public function build( ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies, ): AssetUsageSyncService { return new AssetUsageSyncService( + $serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->contentRepository->projectionState(AssetUsageFinder::class), - $serviceFactoryDependencies->contentRepository->getContentGraph(), $this->assetRepository, $this->assetUsageRepositoryFactory->build($serviceFactoryDependencies->contentRepositoryId), ); diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 417229aca7f..34d3b9ab037 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -14,7 +14,7 @@ namespace Neos\Neos\Command; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; @@ -98,7 +98,7 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ [$nodeType, NodeTypeNameFactory::NAME_SITE] ); $this->quit(1); - } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyOccupied $exception) { + } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyCovered $exception) { $this->outputLine('A site with siteNodeName "%s" already exists', [$nodeName ?: $name]); $this->quit(1); } diff --git a/Neos.Neos/Classes/Controller/Backend/ContentController.php b/Neos.Neos/Classes/Controller/Backend/ContentController.php index 707cca4d8e9..dd474086c97 100644 --- a/Neos.Neos/Classes/Controller/Backend/ContentController.php +++ b/Neos.Neos/Classes/Controller/Backend/ContentController.php @@ -152,9 +152,8 @@ public function uploadAssetAction(Asset $asset, string $metadata, string $node, $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($nodeAddressString); - $node = $contentRepository->getContentGraph() + $node = $contentRepository->getContentGraph($nodeAddress->workspaceName) ->getSubgraph( - $nodeAddress->contentStreamId, $nodeAddress->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ) diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 4bc115447ee..6f3a607f830 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -132,8 +132,7 @@ public function previewAction(string $node): void $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($node); - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $nodeAddress->contentStreamId, + $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, $visibilityConstraints ); @@ -201,8 +200,7 @@ public function showAction(string $node): void throw new NodeNotFoundException('The requested node isn\'t accessible to the current user', 1430218623); } - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $nodeAddress->contentStreamId, + $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, VisibilityConstraints::frontend() ); diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 24dd502fb69..0eef114fab8 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -18,7 +18,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -185,8 +185,7 @@ public function updateSiteAction(Site $site, $newSiteNodeName) } try { - $sitesNode = $contentRepository->getContentGraph()->findRootNodeAggregateByType( - $liveWorkspace->currentContentStreamId, + $sitesNode = $contentRepository->getContentGraph($liveWorkspace->workspaceName)->findRootNodeAggregateByType( NodeTypeNameFactory::forSites() ); } catch (\Exception $exception) { @@ -205,16 +204,11 @@ public function updateSiteAction(Site $site, $newSiteNodeName) } foreach ($contentRepository->getWorkspaceFinder()->findAll() as $workspace) { - // technically, due to the name being the "identifier", there might be more than one :/ - /** @var NodeAggregate[] $siteNodeAggregates */ - /** @var Workspace $workspace */ - $siteNodeAggregates = $contentRepository->getContentGraph()->findChildNodeAggregatesByName( - $workspace->currentContentStreamId, + $siteNodeAggregate = $contentRepository->getContentGraph($workspace->workspaceName)->findChildNodeAggregateByName( $sitesNode->nodeAggregateId, $site->getNodeName()->toNodeName() ); - - foreach ($siteNodeAggregates as $siteNodeAggregate) { + if ($siteNodeAggregate instanceof NodeAggregate) { $contentRepository->handle(ChangeNodeAggregateName::create( $workspace->workspaceName, $siteNodeAggregate->nodeAggregateId, @@ -416,7 +410,7 @@ public function createSiteNodeAction($packageKey, $siteName, $nodeType) 1412372375 ); $this->redirect('createSiteNode'); - } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyOccupied $exception) { + } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyCovered $exception) { $this->addFlashMessage( $this->getModuleLabel('sites.SiteCreationError.siteWithSiteNodeNameAlreadyExists.body', [$siteName]), $this->getModuleLabel('sites.SiteCreationError.siteWithSiteNodeNameAlreadyExists.title'), diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index 2f6b4a4481c..c1c953efd24 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php @@ -457,8 +457,8 @@ public function rebaseAndRedirectAction(Node $targetNode, Workspace $targetWorks throw new \RuntimeException('No account is authenticated', 1710068880); } $personalWorkspaceName = WorkspaceNameBuilder::fromAccountIdentifier($currentAccount->getAccountIdentifier()); - $personalWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($personalWorkspaceName); /** @var Workspace $personalWorkspace */ + $personalWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($personalWorkspaceName); /** @todo do something else * if ($personalWorkspace !== $targetWorkspace) { @@ -750,8 +750,7 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos ); foreach ($changes as $change) { - $contentStreamId = $change->contentStreamId; - + $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. @@ -759,10 +758,9 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos // // This is safe because the UI basically shows what would be removed once the deletion is published. $baseWorkspace = $this->getBaseWorkspaceWhenSureItExists($selectedWorkspace, $contentRepository); - $contentStreamId = $baseWorkspace->currentContentStreamId; + $workspaceName = $baseWorkspace->workspaceName; } - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, + $subgraph = $contentRepository->getContentGraph($workspaceName)->getSubgraph( $change->originDimensionSpacePoint->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions() ); @@ -872,17 +870,14 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos */ protected function getOriginalNode( Node $modifiedNode, - ContentStreamId $baseContentStreamId, + WorkspaceName $baseWorkspaceName, ContentRepository $contentRepository, ): ?Node { - $baseSubgraph = $contentRepository->getContentGraph()->getSubgraph( - $baseContentStreamId, + $baseSubgraph = $contentRepository->getContentGraph($baseWorkspaceName)->getSubgraph( $modifiedNode->subgraphIdentity->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); - $node = $baseSubgraph->findNodeById($modifiedNode->nodeAggregateId); - - return $node; + return $baseSubgraph->findNodeById($modifiedNode->nodeAggregateId); } /** @@ -902,8 +897,7 @@ protected function renderContentChanges( $originalNode = null; if ($currentWorkspace !== null) { $baseWorkspace = $this->getBaseWorkspaceWhenSureItExists($currentWorkspace, $contentRepository); - $baseContentStreamId = $baseWorkspace->currentContentStreamId; - $originalNode = $this->getOriginalNode($changedNode, $baseContentStreamId, $contentRepository); + $originalNode = $this->getOriginalNode($changedNode, $baseWorkspace->workspaceName, $contentRepository); } diff --git a/Neos.Neos/Classes/Controller/Service/NodesController.php b/Neos.Neos/Classes/Controller/Service/NodesController.php index d8db9a2fcc7..c2905fb04a5 100644 --- a/Neos.Neos/Classes/Controller/Service/NodesController.php +++ b/Neos.Neos/Classes/Controller/Service/NodesController.php @@ -131,23 +131,12 @@ public function indexAction( unset($contextNode); if (is_null($nodeAddress)) { - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName( - WorkspaceName::fromString($workspaceName) - ); - if (is_null($workspace)) { - throw new \InvalidArgumentException( - 'Could not resolve a node address for the given parameters.', - 1645631728 - ); - } - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $workspace->currentContentStreamId, + $subgraph = $contentRepository->getContentGraph(WorkspaceName::fromString($workspaceName))->getSubgraph( DimensionSpacePoint::fromLegacyDimensionArray($dimensions), VisibilityConstraints::withoutRestrictions() // we are in a backend controller. ); } else { - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $nodeAddress->contentStreamId, + $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() // we are in a backend controller. ); @@ -206,14 +195,11 @@ public function showAction(string $identifier, string $workspaceName = 'live', a ->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $workspace = $contentRepository->getWorkspaceFinder() - ->findOneByName(WorkspaceName::fromString($workspaceName)); - assert($workspace instanceof Workspace); + $workspaceName = WorkspaceName::fromString($workspaceName); $dimensionSpacePoint = DimensionSpacePoint::fromLegacyDimensionArray($dimensions); - $subgraph = $contentRepository->getContentGraph() + $subgraph = $contentRepository->getContentGraph($workspaceName) ->getSubgraph( - $workspace->currentContentStreamId, $dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); @@ -223,7 +209,7 @@ public function showAction(string $identifier, string $workspaceName = 'live', a if ($node === null) { $this->addExistingNodeVariantInformationToResponse( $nodeAggregateId, - $workspace->currentContentStreamId, + $workspaceName, $dimensionSpacePoint, $contentRepository ); @@ -276,21 +262,18 @@ public function createAction( ->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $workspace = $contentRepository->getWorkspaceFinder() - ->findOneByName(WorkspaceName::fromString($workspaceName)); - assert($workspace instanceof Workspace); + $workspaceName = WorkspaceName::fromString($workspaceName); - $sourceSubgraph = $contentRepository->getContentGraph() + $contentGraph = $contentRepository->getContentGraph($workspaceName); + $sourceSubgraph = $contentGraph ->getSubgraph( - $workspace->currentContentStreamId, DimensionSpacePoint::fromLegacyDimensionArray($sourceDimensions), VisibilityConstraints::withoutRestrictions() ); $targetDimensionSpacePoint = DimensionSpacePoint::fromLegacyDimensionArray($dimensions); - $targetSubgraph = $contentRepository->getContentGraph() + $targetSubgraph = $contentGraph ->getSubgraph( - $workspace->currentContentStreamId, $targetDimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); @@ -298,7 +281,7 @@ public function createAction( if ($mode === 'adoptFromAnotherDimension' || $mode === 'adoptFromAnotherDimensionAndCopyContent') { CatchUpTriggerWithSynchronousOption::synchronously(fn() => $this->adoptNodeAndParents( - $workspace->workspaceName, + $workspaceName, $nodeAggregateId, $sourceSubgraph, $targetSubgraph, @@ -309,7 +292,7 @@ public function createAction( $this->redirect('show', null, null, [ 'identifier' => $nodeAggregateId->value, - 'workspaceName' => $workspaceName, + 'workspaceName' => $workspaceName->value, 'dimensions' => $dimensions ]); } else { @@ -323,13 +306,13 @@ public function createAction( */ protected function addExistingNodeVariantInformationToResponse( NodeAggregateId $identifier, - ContentStreamId $contentStreamId, + WorkspaceName $workspaceName, DimensionSpacePoint $dimensionSpacePoint, ContentRepository $contentRepository ): void { - $contentGraph = $contentRepository->getContentGraph(); + $contentGraph = $contentRepository->getContentGraph($workspaceName); $nodeTypeManager = $contentRepository->getNodeTypeManager(); - $nodeAggregate = $contentGraph->findNodeAggregateById($contentStreamId, $identifier); + $nodeAggregate = $contentGraph->findNodeAggregateById($identifier); if ($nodeAggregate && $nodeAggregate->coveredDimensionSpacePoints->count() > 0) { $this->response->setHttpHeader('X-Neos-Node-Exists-In-Other-Dimensions', 'true'); @@ -346,7 +329,7 @@ protected function addExistingNodeVariantInformationToResponse( $missingNodesOnRootline = 0; while ( $parentAggregate = self::firstNodeAggregate( - $contentGraph->findParentNodeAggregates($contentStreamId, $identifier) + $contentGraph->findParentNodeAggregates($identifier) ) ) { if (!$parentAggregate->coversDimensionSpacePoint($dimensionSpacePoint)) { @@ -382,6 +365,12 @@ private static function firstNodeAggregate(iterable $nodeAggregates): ?NodeAggre /** * Adopt (translate) the given node and parents that are not yet visible to the given context * + * @param WorkspaceName $workspaceName + * @param NodeAggregateId $nodeAggregateId + * @param ContentSubgraphInterface $sourceSubgraph + * @param ContentSubgraphInterface $targetSubgraph + * @param DimensionSpacePoint $targetDimensionSpacePoint + * @param ContentRepository $contentRepository * @param boolean $copyContent true if the content from the nodes that are translated should be copied * @return void */ diff --git a/Neos.Neos/Classes/Domain/Model/Site.php b/Neos.Neos/Classes/Domain/Model/Site.php index 1d15d90a342..15c278beaa9 100644 --- a/Neos.Neos/Classes/Domain/Model/Site.php +++ b/Neos.Neos/Classes/Domain/Model/Site.php @@ -19,6 +19,7 @@ use Doctrine\ORM\Mapping as ORM; use Neos\Flow\Annotations as Flow; use Neos\Media\Domain\Model\AssetCollection; +use Neos\Utility\Arrays; /** * Domain model of a site @@ -35,12 +36,19 @@ class Site public const STATE_OFFLINE = 2; /** - * @Flow\InjectConfiguration(path="sites") * @var array * @phpstan-var array> */ + #[Flow\InjectConfiguration(path: 'sites')] protected $sitesConfiguration = []; + /** + * @var array + * @phpstan-var array + */ + #[Flow\InjectConfiguration(path: 'sitePresets')] + protected $sitePresetsConfiguration = []; + /** * Name of the site * @@ -372,7 +380,25 @@ public function emitSiteChanged() public function getConfiguration(): SiteConfiguration { - // we DO NOT want recursive merge here - return SiteConfiguration::fromArray($this->sitesConfiguration[$this->nodeName] ?? $this->sitesConfiguration['*']); + if (array_key_exists($this->nodeName, $this->sitesConfiguration)) { + $siteSettingsPath = $this->nodeName; + } else { + if (!array_key_exists('*', $this->sitesConfiguration)) { + throw new \RuntimeException(sprintf('Missing configuration for "Neos.Neos.sites.%s" or fallback "Neos.Neos.sites.*"', $this->nodeName), 1714230658); + } + $siteSettingsPath = '*'; + } + $siteSettings = $this->sitesConfiguration[$siteSettingsPath]; + if (isset($siteSettings['preset'])) { + if (!is_string($siteSettings['preset'])) { + throw new \RuntimeException(sprintf('Invalid "preset" configuration for "Neos.Neos.sites.%s". Expected string, got: %s', $siteSettingsPath, get_debug_type($siteSettings['preset'])), 1699785648); + } + if (!isset($this->sitePresetsConfiguration[$siteSettings['preset']]) || !is_array($this->sitePresetsConfiguration[$siteSettings['preset']])) { + throw new \RuntimeException(sprintf('Site settings "Neos.Neos.sites.%s" refer to a preset "%s", but no corresponding preset is configured', $siteSettingsPath, $siteSettings['preset']), 1699785736); + } + $siteSettings = Arrays::arrayMergeRecursiveOverrule($this->sitePresetsConfiguration[$siteSettings['preset']], $siteSettings); + unset($siteSettings['preset']); + } + return SiteConfiguration::fromArray($siteSettings); } } diff --git a/Neos.Neos/Classes/Domain/NodeLabel/DelegatingNodeLabelRenderer.php b/Neos.Neos/Classes/Domain/NodeLabel/DelegatingNodeLabelRenderer.php new file mode 100644 index 00000000000..57f774c4003 --- /dev/null +++ b/Neos.Neos/Classes/Domain/NodeLabel/DelegatingNodeLabelRenderer.php @@ -0,0 +1,74 @@ +contentRepositoryRegistry->get($node->contentRepositoryId)->getNodeTypeManager(); + $nodeType = $nodeTypeManager->getNodeType($node->nodeTypeName) + ?? $nodeTypeManager->getNodeType(NodeTypeNameFactory::forFallback()); + $generator = $this->getDelegatedGenerator($nodeType); + if ($generator instanceof DelegatingNodeLabelRenderer) { + throw new \RuntimeException( + 'Recursion detected, cannot specify DelegatingNodeLabelRenderer as generatorClass for NodeLabel as this is the default.', + 1715622960 + ); + } + return $generator->getLabel($node); + } + + private function getDelegatedGenerator(?NodeType $nodeType): NodeLabelGeneratorInterface + { + if ($nodeType?->hasConfiguration('label.generatorClass')) { + $nodeLabelGeneratorClassName = $nodeType->getConfiguration('label.generatorClass'); + $nodeLabelGenerator = $this->objectManager->get($nodeLabelGeneratorClassName); + if (!$nodeLabelGenerator instanceof NodeLabelGeneratorInterface) { + throw new \InvalidArgumentException( + 'Configured class "' . $nodeLabelGeneratorClassName . '" does not implement the required ' + . NodeLabelGeneratorInterface::class, + 1682950942 + ); + } + } elseif ($nodeType?->hasConfiguration('label') && is_string($nodeType->getConfiguration('label'))) { + /** @var ExpressionBasedNodeLabelGenerator $nodeLabelGenerator */ + $nodeLabelGenerator = $this->objectManager->get(ExpressionBasedNodeLabelGenerator::class); + $nodeLabelGenerator->setExpression($nodeType->getConfiguration('label')); + } else { + $nodeLabelGenerator = new class implements NodeLabelGeneratorInterface { + public function getLabel(Node $node): string + { + return sprintf( + '%s %s', + $node->nodeTypeName->value, + $node->name + ? sprintf('(%s)', $node->name->value) + : '' + ); + } + }; + } + + return $nodeLabelGenerator; + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/NodeLabel/ExpressionBasedNodeLabelGenerator.php b/Neos.Neos/Classes/Domain/NodeLabel/ExpressionBasedNodeLabelGenerator.php similarity index 71% rename from Neos.ContentRepositoryRegistry/Classes/NodeLabel/ExpressionBasedNodeLabelGenerator.php rename to Neos.Neos/Classes/Domain/NodeLabel/ExpressionBasedNodeLabelGenerator.php index 8ed9333b6a0..8c7010230e4 100644 --- a/Neos.ContentRepositoryRegistry/Classes/NodeLabel/ExpressionBasedNodeLabelGenerator.php +++ b/Neos.Neos/Classes/Domain/NodeLabel/ExpressionBasedNodeLabelGenerator.php @@ -1,5 +1,6 @@ */ protected $defaultContextConfiguration; @@ -37,7 +38,9 @@ class ExpressionBasedNodeLabelGenerator implements NodeLabelGeneratorInterface /** * @var string */ - protected $expression = '${(node.nodeType.label ? node.nodeType.label : node.nodeType.name) + \' (\' + node.name + \')\'}'; + protected $expression = <<<'EEL' + ${(node.nodeType.label ? node.nodeType.label : node.nodeTypeName.value) + (node.nodeName ? ' (' + node.nodeName.value + ')' : '')} + EEL; /** * @return string @@ -61,6 +64,7 @@ public function getLabel(Node $node): string if (Utility::parseEelExpression($this->getExpression()) === null) { return $this->getExpression(); } - return (string)Utility::evaluateEelExpression($this->getExpression(), $this->eelEvaluator, ['node' => $node], $this->defaultContextConfiguration); + $value = Utility::evaluateEelExpression($this->getExpression(), $this->eelEvaluator, ['node' => $node], $this->defaultContextConfiguration); + return (string)$value; } } diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeLabelGeneratorInterface.php b/Neos.Neos/Classes/Domain/NodeLabel/NodeLabelGeneratorInterface.php similarity index 80% rename from Neos.ContentRepository.Core/Classes/NodeType/NodeLabelGeneratorInterface.php rename to Neos.Neos/Classes/Domain/NodeLabel/NodeLabelGeneratorInterface.php index e5893231114..b1f8658b064 100644 --- a/Neos.ContentRepository.Core/Classes/NodeType/NodeLabelGeneratorInterface.php +++ b/Neos.Neos/Classes/Domain/NodeLabel/NodeLabelGeneratorInterface.php @@ -12,14 +12,12 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\NodeType; +namespace Neos\Neos\Domain\NodeLabel; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; /** - * Interface for rendering a node label string based on some strategy - * - * @api + * @api to access the Node's label in PHP, in Fusion one can use ${q(node).label()}. */ interface NodeLabelGeneratorInterface { diff --git a/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php b/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php index a9b503611e5..fb7829123bf 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php +++ b/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php @@ -18,7 +18,7 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Neos\Domain\Model\Site; @@ -41,13 +41,9 @@ public function __construct( * To find the site node for the live workspace in a 0 dimensional content repository use: * * ```php - * $contentRepository = $this->contentRepositoryRegistry->get($site->getConfiguration()->contentRepositoryId); - * $liveWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::forLive()) - * ?? throw new \RuntimeException('Expected live workspace to exist.'); - * * $siteNode = $this->siteNodeUtility->findSiteNodeBySite( * $site, - * $liveWorkspace->currentContentStreamId, + * WorkspaceName::forLive(), * DimensionSpacePoint::createWithoutDimensions(), * VisibilityConstraints::frontend() * ); @@ -57,20 +53,19 @@ public function __construct( */ public function findSiteNodeBySite( Site $site, - ContentStreamId $contentStreamId, + WorkspaceName $workspaceName, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints ): Node { $contentRepository = $this->contentRepositoryRegistry->get($site->getConfiguration()->contentRepositoryId); - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, + $contentGraph = $contentRepository->getContentGraph($workspaceName); + $subgraph = $contentGraph->getSubgraph( $dimensionSpacePoint, $visibilityConstraints, ); - $rootNodeAggregate = $contentRepository->getContentGraph()->findRootNodeAggregateByType( - $contentStreamId, + $rootNodeAggregate = $contentGraph->findRootNodeAggregateByType( NodeTypeNameFactory::forSites() ); $rootNode = $rootNodeAggregate->getNodeByCoveredDimensionSpacePoint($dimensionSpacePoint); diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index 7dbe858a5c5..ae5d75bb1e5 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -39,7 +39,7 @@ public function __construct( private ContentRepository $contentRepository, private InterDimensionalVariationGraph $interDimensionalVariationGraph, - private NodeTypeManager $nodeTypeManager + private NodeTypeManager $nodeTypeManager, ) { } @@ -54,21 +54,17 @@ public function removeSiteNode(SiteNodeName $siteNodeName): void 1651921482 ); } - $contentGraph = $this->contentRepository->getContentGraph(); foreach ($this->contentRepository->getWorkspaceFinder()->findAll() as $workspace) { + $contentGraph = $this->contentRepository->getContentGraph($workspace->workspaceName); $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType( - $workspace->currentContentStreamId, NodeTypeNameFactory::forSites() ); - $siteNodeAggregates = $contentGraph->findChildNodeAggregatesByName( - $workspace->currentContentStreamId, + $siteNodeAggregate = $contentGraph->findChildNodeAggregateByName( $sitesNodeAggregate->nodeAggregateId, $siteNodeName->toNodeName() ); - - foreach ($siteNodeAggregates as $siteNodeAggregate) { - assert($siteNodeAggregate instanceof NodeAggregate); + if ($siteNodeAggregate instanceof NodeAggregate) { $this->contentRepository->handle(RemoveNodeAggregate::create( $workspace->workspaceName, $siteNodeAggregate->nodeAggregateId, @@ -99,12 +95,12 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); } - $siteNodeAggregate = $this->contentRepository->getContentGraph()->findChildNodeAggregatesByName( - $liveWorkspace->currentContentStreamId, - $sitesNodeIdentifier, - $site->getNodeName()->toNodeName(), - ); - foreach ($siteNodeAggregate as $_) { + $siteNodeAggregate = $this->contentRepository->getContentGraph($liveWorkspace->workspaceName) + ->findChildNodeAggregateByName( + $sitesNodeIdentifier, + $site->getNodeName()->toNodeName(), + ); + if ($siteNodeAggregate instanceof NodeAggregate) { // Site node already exists return; } diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php index d47a105c5f1..123b324f754 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternalsFactory.php @@ -27,7 +27,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor return new SiteServiceInternals( $serviceFactoryDependencies->contentRepository, $serviceFactoryDependencies->interDimensionalVariationGraph, - $serviceFactoryDependencies->nodeTypeManager + $serviceFactoryDependencies->nodeTypeManager, ); } } diff --git a/Neos.Neos/Classes/Domain/Workspace/Workspace.php b/Neos.Neos/Classes/Domain/Workspace/Workspace.php index 670a4d9f7f7..814a897f158 100644 --- a/Neos.Neos/Classes/Domain/Workspace/Workspace.php +++ b/Neos.Neos/Classes/Domain/Workspace/Workspace.php @@ -262,8 +262,7 @@ private function requireNodeToBeOfType( NodeAggregateId $nodeAggregateId, NodeTypeName $nodeTypeName, ): void { - $nodeAggregate = $this->contentRepository->getContentGraph()->findNodeAggregateById( - $this->currentContentStreamId, + $nodeAggregate = $this->contentRepository->getContentGraph($this->name)->findNodeAggregateById( $nodeAggregateId, ); if (!$nodeAggregate instanceof NodeAggregate) { @@ -398,8 +397,7 @@ private function isChangePublishableWithinAncestorScope( } } - $subgraph = $this->contentRepository->getContentGraph()->getSubgraph( - $this->currentContentStreamId, + $subgraph = $this->contentRepository->getContentGraph($this->name)->getSubgraph( $change->originDimensionSpacePoint->toDimensionSpacePoint(), VisibilityConstraints::withoutRestrictions() ); diff --git a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php b/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php index 3c82e38109e..3c57bc02c14 100644 --- a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php +++ b/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php @@ -6,7 +6,6 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; @@ -140,21 +139,20 @@ private function onBeforeNodeAggregateWasMoved(NodeAggregateWasMoved $event): vo return; } - foreach ($event->nodeMoveMappings as $moveMapping) { - /* @var \Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping $moveMapping */ - foreach ($moveMapping->newLocations as $newLocation) { - /* @var $newLocation CoverageNodeMoveMapping */ - $node = $this->findDocumentNodeInfoByIdAndDimensionSpacePoint($event->nodeAggregateId, $newLocation->coveredDimensionSpacePoint); - if (!$node) { - // node probably no document node, skip - continue; - } + foreach ($event->succeedingSiblingsForCoverage as $succeedingSiblingForCoverage) { + $node = $this->findDocumentNodeInfoByIdAndDimensionSpacePoint( + $event->nodeAggregateId, + $succeedingSiblingForCoverage->dimensionSpacePoint + ); + if (!$node) { + // node probably no document node, skip + continue; + } - $this->collectTagsToFlush($node); + $this->collectTagsToFlush($node); - $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); - array_map($this->collectTagsToFlush(...), iterator_to_array($descendantsOfNode)); - } + $descendantsOfNode = $this->getState()->getDescendantsOfNode($node); + array_map($this->collectTagsToFlush(...), iterator_to_array($descendantsOfNode)); } } diff --git a/Neos.Neos/Classes/FrontendRouting/NodeAddress.php b/Neos.Neos/Classes/FrontendRouting/NodeAddress.php index 83db25d52c6..1126c00babe 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeAddress.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeAddress.php @@ -31,7 +31,9 @@ * * It is used in Neos Routing to build a URI to a node. * - * @api + * @deprecated will be removed before Final 9.0 + * The NodeAddress was added 6 years ago without the concept of multiple crs + * Its usages will be replaced by the new node attached node address */ #[Flow\Proxy(false)] final readonly class NodeAddress diff --git a/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php index 3a2f28c2a7f..9781840a45a 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php @@ -22,7 +22,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** - * @api + * @deprecated will be removed before Final 9.0 */ class NodeAddressFactory { diff --git a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentNodeInfo.php b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentNodeInfo.php index 0a092055bdb..8caa59173b0 100644 --- a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentNodeInfo.php +++ b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentNodeInfo.php @@ -71,6 +71,15 @@ public function withOriginDimensionSpacePoint(OriginDimensionSpacePoint $originD return new self($source); } + public function withoutSiblings(): self + { + $source = $this->source; + $source['precedingnodeaggregateid'] = null; + $source['succeedingnodeaggregateid'] = null; + + return new self($source); + } + public function getNodeAggregateId(): NodeAggregateId { return NodeAggregateId::fromString($this->source['nodeaggregateid']); diff --git a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathFinder.php b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathFinder.php index 79a0093790f..8c78fc04108 100644 --- a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathFinder.php +++ b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathFinder.php @@ -195,6 +195,28 @@ public function getLastChildNode( ); } + /** + * @throws NodeNotFoundException + * @internal + */ + public function getLastChildNodeNotBeing( + NodeAggregateId $parentNodeAggregateId, + string $dimensionSpacePointHash, + NodeAggregateId $excludedNodeAggregateId + ): DocumentNodeInfo { + return $this->fetchSingle( + 'dimensionSpacePointHash = :dimensionSpacePointHash + AND parentNodeAggregateId = :parentNodeAggregateId + AND nodeAggregateId != :excludedNodeAggregateId + AND succeedingNodeAggregateId IS NULL', + [ + 'dimensionSpacePointHash' => $dimensionSpacePointHash, + 'parentNodeAggregateId' => $parentNodeAggregateId->value, + 'excludedNodeAggregateId' => $excludedNodeAggregateId->value + ] + ); + } + /** * @api */ diff --git a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php index c6e503d57c8..f6269e7cba3 100644 --- a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php +++ b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php @@ -8,16 +8,13 @@ use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Types\Types; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventInterface; +use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionShineThroughWasAdded; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\ParentNodeMoveDestination; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged; @@ -394,7 +391,7 @@ private function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event) $event->nodeAggregateId, $event->sourceOrigin, $event->peerOrigin, - $event->peerSucceedingSiblings->toDimensionSpacePointSet() + $event->peerSucceedingSiblings ); } @@ -407,7 +404,7 @@ private function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVaria $event->nodeAggregateId, $event->sourceOrigin, $event->generalizationOrigin, - $event->variantSucceedingSiblings->toDimensionSpacePointSet() + $event->variantSucceedingSiblings ); } @@ -420,7 +417,7 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria $event->nodeAggregateId, $event->sourceOrigin, $event->specializationOrigin, - $event->specializationSiblings->toDimensionSpacePointSet() + $event->specializationSiblings ); } @@ -428,7 +425,7 @@ private function copyVariants( NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $targetOrigin, - DimensionSpacePointSet $coveredSpacePoints + InterdimensionalSiblings $interdimensionalSiblings, ): void { $sourceNode = $this->tryGetNode(fn () => $this->getState()->getByIdAndDimensionSpacePointHash( $nodeAggregateId, @@ -438,17 +435,18 @@ private function copyVariants( // Probably not a document node return; } - foreach ($coveredSpacePoints as $coveredSpacePoint) { + foreach ($interdimensionalSiblings as $interdimensionalSibling) { // Especially when importing a site it can happen that variants are created in a "non-deterministic" order, // so we need to first make sure a target variant doesn't exist: - $this->deleteNodeByIdAndDimensionSpacePointHash($nodeAggregateId, $coveredSpacePoint->hash); + $this->deleteNodeByIdAndDimensionSpacePointHash($nodeAggregateId, $interdimensionalSibling->dimensionSpacePoint->hash); - $this->insertNode( - $sourceNode - ->withDimensionSpacePoint($coveredSpacePoint) + $targetNode = $sourceNode + ->withDimensionSpacePoint($interdimensionalSibling->dimensionSpacePoint) ->withOriginDimensionSpacePoint($targetOrigin) - ->toArray() - ); + ->withoutSiblings(); + + $this->insertNode($targetNode->toArray()); + $this->connectNodeWithSiblings($targetNode, $targetNode->getParentNodeAggregateId(), $interdimensionalSibling->nodeAggregateId); } } @@ -617,49 +615,36 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void return; } - foreach ($event->nodeMoveMappings as $moveMapping) { - /* @var \Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping $moveMapping */ - foreach ($moveMapping->newLocations as $newLocation) { - /* @var $newLocation CoverageNodeMoveMapping */ - $node = $this->tryGetNode(fn () => $this->getState()->getByIdAndDimensionSpacePointHash( - $event->nodeAggregateId, - $newLocation->coveredDimensionSpacePoint->hash - )); - if (!$node) { - // node probably no document node, skip - continue; - } - - match ($newLocation->destination::class) { - SucceedingSiblingNodeMoveDestination::class => $this->moveNode( - /** @var SucceedingSiblingNodeMoveDestination $newLocation->destination */ - $node, - $newLocation->destination->parentNodeAggregateId, - $newLocation->destination->nodeAggregateId - ), - ParentNodeMoveDestination::class => $this->moveNode( - /** @var ParentNodeMoveDestination $newLocation->destination */ - $node, - $newLocation->destination->nodeAggregateId, - null - ), - }; - - $this->getState()->purgeCacheFor($node); + foreach ($event->succeedingSiblingsForCoverage as $succeedingSiblingForCoverage) { + $node = $this->tryGetNode(fn () => $this->getState()->getByIdAndDimensionSpacePointHash( + $event->nodeAggregateId, + $succeedingSiblingForCoverage->dimensionSpacePoint->hash + )); + if (!$node) { + // node probably no document node, skip + continue; } + + $this->moveNode( + $node, + $event->newParentNodeAggregateId, + $succeedingSiblingForCoverage->nodeAggregateId + ); + + $this->getState()->purgeCacheFor($node); } } private function moveNode( DocumentNodeInfo $node, - NodeAggregateId $newParentNodeAggregateId, + ?NodeAggregateId $newParentNodeAggregateId, ?NodeAggregateId $newSucceedingNodeAggregateId ): void { $this->disconnectNodeFromSiblings($node); - $this->connectNodeWithSiblings($node, $newParentNodeAggregateId, $newSucceedingNodeAggregateId); + $this->connectNodeWithSiblings($node, $newParentNodeAggregateId ?: $node->getParentNodeAggregateId(), $newSucceedingNodeAggregateId); - if ($newParentNodeAggregateId->equals($node->getParentNodeAggregateId())) { + if (!$newParentNodeAggregateId || $newParentNodeAggregateId->equals($node->getParentNodeAggregateId())) { return; } $newParentNode = $this->tryGetNode(fn () => $this->getState()->getByIdAndDimensionSpacePointHash( @@ -911,7 +896,7 @@ private function disconnectNodeFromSiblings(DocumentNodeInfo $node): void private function connectNodeWithSiblings( DocumentNodeInfo $node, NodeAggregateId $parentNodeAggregateId, - ?NodeAggregateId $newSucceedingNodeAggregateId + ?NodeAggregateId $newSucceedingNodeAggregateId, ): void { if ($newSucceedingNodeAggregateId !== null) { $newPrecedingNode = $this->tryGetNode(fn () => $this->getState()->getPrecedingNode( @@ -927,24 +912,34 @@ private function connectNodeWithSiblings( ['precedingNodeAggregateId' => $node->getNodeAggregateId()->value] ); } else { - $newPrecedingNode = $this->tryGetNode(fn () => $this->getState()->getLastChildNode( + $newPrecedingNode = $this->tryGetNode(fn () => $this->getState()->getLastChildNodeNotBeing( $parentNodeAggregateId, - $node->getDimensionSpacePointHash() + $node->getDimensionSpacePointHash(), + $node->getNodeAggregateId() )); } - if ($newPrecedingNode !== null) { + if ( + $newPrecedingNode !== null + && !$newPrecedingNode->getNodeAggregateId()->equals($node->getNodeAggregateId()) + ) { $this->updateNode( $newPrecedingNode, ['succeedingNodeAggregateId' => $node->getNodeAggregateId()->value] ); } - // update node itself - $this->updateNode($node, [ + $updatedNodeData = [ 'parentNodeAggregateId' => $parentNodeAggregateId->value, - 'precedingNodeAggregateId' => $newPrecedingNode?->getNodeAggregateId()->value, 'succeedingNodeAggregateId' => $newSucceedingNodeAggregateId?->value, - ]); + ]; + if ( + !$newPrecedingNode?->getNodeAggregateId()->equals($node->getNodeAggregateId()) + ) { + $updatedNodeData['precedingNodeAggregateId'] = $newPrecedingNode?->getNodeAggregateId()->value; + } + + // update node itself + $this->updateNode($node, $updatedNodeData); } diff --git a/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php index 23f8055dc8a..7595c4f060e 100644 --- a/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/AbstractMenuItemsImplementation.php @@ -24,15 +24,10 @@ * Base class for Menu and DimensionsMenu * * Main Options: - * - renderHiddenInIndex: if TRUE, hidden-in-index nodes will be shown in the menu. FALSE by default. + * - renderHiddenInMenu: if TRUE, nodes with the property ``hiddenInMenu`` will be shown in the menu. FALSE by default. */ abstract class AbstractMenuItemsImplementation extends AbstractFusionObject { - public const STATE_NORMAL = 'normal'; - public const STATE_CURRENT = 'current'; - public const STATE_ACTIVE = 'active'; - public const STATE_ABSENT = 'absent'; - /** * An internal cache for the built menu items array. * @@ -46,11 +41,11 @@ abstract class AbstractMenuItemsImplementation extends AbstractFusionObject protected $currentNode; /** - * Internal cache for the renderHiddenInIndex property. + * Internal cache for the renderHiddenInMenu property. * * @var boolean */ - protected $renderHiddenInIndex; + protected $renderHiddenInMenu; /** * Internal cache for the calculateItemStates property. @@ -76,17 +71,15 @@ public function isCalculateItemStatesEnabled(): bool } /** - * Should nodes that have "hiddenInIndex" set still be visible in this menu. - * - * @return boolean + * Should nodes that have "hiddenInMenu" set still be visible in this menu. */ - public function getRenderHiddenInIndex() + public function getRenderHiddenInMenu(): bool { - if ($this->renderHiddenInIndex === null) { - $this->renderHiddenInIndex = (bool)$this->fusionValue('renderHiddenInIndex'); + if ($this->renderHiddenInMenu === null) { + $this->renderHiddenInMenu = (bool)$this->fusionValue('renderHiddenInMenu'); } - return $this->renderHiddenInIndex; + return $this->renderHiddenInMenu; } /** @@ -139,7 +132,7 @@ abstract protected function buildItems(): array; /** * Return TRUE/FALSE if the node is currently hidden or not in the menu; - * taking the "renderHiddenInIndex" configuration of the Menu Fusion object into account. + * taking the "renderHiddenInMenu" configuration of the Menu Fusion object into account. * * This method needs to be called inside buildItems() in the subclasses. * @@ -148,14 +141,14 @@ abstract protected function buildItems(): array; */ protected function isNodeHidden(Node $node) { - if ($this->getRenderHiddenInIndex() === true) { - // Please show hiddenInIndex nodes + if ($this->getRenderHiddenInMenu() === true) { + // Please show hiddenInMenu nodes // -> node is *never* hidden! return false; } - // Node is hidden depending on the _hiddenInIndex property - return $node->getProperty('_hiddenInIndex'); + // Node is hidden depending on the hiddenInMenu property + return $node->getProperty('hiddenInMenu'); } protected function buildUri(Node $node): string diff --git a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php index 4034943e15c..06546c03641 100644 --- a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php +++ b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php @@ -14,6 +14,7 @@ namespace Neos\Neos\Fusion\Cache; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentGraph; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeName; @@ -22,10 +23,14 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Fusion\Core\Cache\ContentCache; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\AssetVariantInterface; +use Neos\Neos\AssetUsage\Dto\AssetUsageFilter; +use Neos\Neos\AssetUsage\GlobalAssetUsageService; use Psr\Log\LoggerInterface; /** @@ -36,17 +41,24 @@ * This is the relevant case if publishing a workspace * - where we f.e. need to flush the cache for Live. * - * @Flow\Scope("singleton") */ +#[Flow\Scope('singleton')] class ContentCacheFlusher { #[Flow\InjectConfiguration(path: "fusion.contentCacheDebugMode")] protected bool $debugMode; + /** + * @var array + */ + private array $tagsToFlushAfterPersistance = []; public function __construct( - protected ContentCache $contentCache, - protected LoggerInterface $systemLogger, + protected readonly ContentCache $contentCache, + protected readonly LoggerInterface $systemLogger, + protected readonly GlobalAssetUsageService $globalAssetUsageService, + protected readonly ContentRepositoryRegistry $contentRepositoryRegistry, + protected readonly PersistenceManagerInterface $persistenceManager, ) { } @@ -62,33 +74,51 @@ public function flushNodeAggregate( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId ): void { - $tagsToFlush = []; - $tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".'; - $this->registerChangeOnNodeIdentifier($contentRepository->id, $contentStreamId, $nodeAggregateId, $tagsToFlush); - $nodeAggregate = $contentRepository->getContentGraph()->findNodeAggregateById( - $contentStreamId, + $tagsToFlush = array_merge( + $this->collectTagsForChangeOnNodeAggregate($contentRepository, $contentStreamId, $nodeAggregateId), + $tagsToFlush + ); + + $this->flushTags($tagsToFlush); + } + + /** + * @return array + */ + private function collectTagsForChangeOnNodeAggregate( + ContentRepository $contentRepository, + ContentStreamId $contentStreamId, + NodeAggregateId $nodeAggregateId + ): array { + $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($contentStreamId); + if (is_null($workspace)) { + return []; + } + $contentGraph = $contentRepository->getContentGraph($workspace->workspaceName); + + + $nodeAggregate = $contentGraph->findNodeAggregateById( $nodeAggregateId ); if (!$nodeAggregate) { // Node Aggregate was removed in the meantime, so no need to clear caches on this one anymore. - return; + return []; } + $tagsToFlush = $this->collectTagsForChangeOnNodeIdentifier($contentRepository->id, $contentGraph->getContentStreamId(), $nodeAggregateId); - $this->registerChangeOnNodeType( + $tagsToFlush = array_merge($this->collectTagsForChangeOnNodeType( $nodeAggregate->nodeTypeName, $contentRepository->id, - $contentStreamId, + $contentGraph->getContentStreamId(), $nodeAggregateId, - $tagsToFlush, $contentRepository - ); + ), $tagsToFlush); $parentNodeAggregates = []; foreach ( - $contentRepository->getContentGraph()->findParentNodeAggregates( - $contentStreamId, + $contentGraph->findParentNodeAggregates( $nodeAggregateId ) as $parentNodeAggregate ) { @@ -122,32 +152,27 @@ public function flushNodeAggregate( ); foreach ( - $contentRepository->getContentGraph()->findParentNodeAggregates( - $nodeAggregate->contentStreamId, + $contentGraph->findParentNodeAggregates( $nodeAggregate->nodeAggregateId ) as $parentNodeAggregate ) { $parentNodeAggregates[] = $parentNodeAggregate; } } - $this->flushTags($tagsToFlush); + + return $tagsToFlush; } /** - * Please use registerNodeChange() if possible. This method is a low-level api. If you do use this method make sure - * that $cacheIdentifier contains the workspacehash as well as the node identifier: - * $workspaceHash .'_'. $nodeIdentifier - * The workspacehash can be received via $this->getCachingHelper()->renderWorkspaceTagForContextNode($workpsacename) - * - * @param array &$tagsToFlush + * @return array */ - private function registerChangeOnNodeIdentifier( + private function collectTagsForChangeOnNodeIdentifier( ContentRepositoryId $contentRepositoryId, ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, - array &$tagsToFlush - ): void { + ): array { + $tagsToFlush = []; $nodeCacheIdentifier = CacheTag::forNodeAggregate($contentRepositoryId, $contentStreamId, $nodeAggregateId); $tagsToFlush[$nodeCacheIdentifier->value] = sprintf( @@ -155,29 +180,44 @@ private function registerChangeOnNodeIdentifier( $nodeCacheIdentifier->value ); - $descandantOfNodeCacheIdentifier = CacheTag::forDescendantOfNode($contentRepositoryId, $contentStreamId, $nodeAggregateId); - $tagsToFlush[$descandantOfNodeCacheIdentifier->value] = sprintf( + $dynamicNodeCacheIdentifier = CacheTag::forDynamicNodeAggregate($contentRepositoryId, $contentStreamId, $nodeAggregateId); + $tagsToFlush[$dynamicNodeCacheIdentifier->value] = sprintf( + 'which were tagged with "%s" because that identifier has changed.', + $dynamicNodeCacheIdentifier->value + ); + + $descendantOfNodeCacheIdentifier = CacheTag::forDescendantOfNode($contentRepositoryId, $contentStreamId, $nodeAggregateId); + $tagsToFlush[$descendantOfNodeCacheIdentifier->value] = sprintf( 'which were tagged with "%s" because node "%s" has changed.', - $descandantOfNodeCacheIdentifier->value, + $descendantOfNodeCacheIdentifier->value, $nodeCacheIdentifier->value ); + + return $tagsToFlush; } /** - * This is a low-level api. Please use registerNodeChange() if possible. Otherwise make sure that $nodeTypePrefix - * is set up correctly and contains the workspacehash wich can be received via - * $this->getCachingHelper()->renderWorkspaceTagForContextNode($workpsacename) - * - * @param array &$tagsToFlush + * @return array $tagsToFlush */ - private function registerChangeOnNodeType( + private function collectTagsForChangeOnNodeType( NodeTypeName $nodeTypeName, ContentRepositoryId $contentRepositoryId, ContentStreamId $contentStreamId, ?NodeAggregateId $referenceNodeIdentifier, - array &$tagsToFlush, ContentRepository $contentRepository - ): void { + ): array { + $tagsToFlush = []; + + /* @todo ask denny if the below is equivalent to his: + $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName); + if ($nodeType) { + $nodeTypesNamesToFlush = $this->getAllImplementedNodeTypeNames($nodeType); + } else { + // as a fallback, we flush the single NodeType + $nodeTypesNamesToFlush = [$nodeTypeName->value]; + } + */ + $nodeTypeNamesToFlush = $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName)?->superTypes->toArray() ?? []; $nodeTypeNamesToFlush[] = $nodeTypeName; foreach ($nodeTypeNamesToFlush as $nodeTypeNameToFlush) { @@ -189,11 +229,13 @@ private function registerChangeOnNodeType( $nodeTypeName->value ); } + + return $tagsToFlush; } /** - * Flush caches according to the previously registered node changes. + * Flush caches according to the given tags. * * @param array $tagsToFlush */ @@ -216,7 +258,6 @@ protected function flushTags(array $tagsToFlush): void } } - /** * Fetches possible usages of the asset and registers nodes that use the asset as changed. * @@ -226,55 +267,45 @@ public function registerAssetChange(AssetInterface $asset): void { // In Nodes only assets are referenced, never asset variants directly. When an asset // variant is updated, it is passed as $asset, but since it is never "used" by any node - // no flushing of corresponding entries happens. Thus we instead us the original asset + // no flushing of corresponding entries happens. Thus we instead use the original asset // of the variant. if ($asset instanceof AssetVariantInterface) { $asset = $asset->getOriginalAsset(); } - // TODO: re-implement this based on the code below - - /* - if (!$asset->isInUse()) { - return; + $tagsToFlush = []; + $filter = AssetUsageFilter::create() + ->withAsset($this->persistenceManager->getIdentifierByObject($asset)) + ->includeVariantsOfAsset(); + + foreach ($this->globalAssetUsageService->findByFilter($filter) as $contentRepositoryId => $usages) { + foreach ($usages as $usage) { + $contentRepository = $this->contentRepositoryRegistry->get(ContentRepositoryId::fromString($contentRepositoryId)); + $tagsToFlush = array_merge( + $this->collectTagsForChangeOnNodeAggregate( + $contentRepository, + $usage->contentStreamId, + $usage->nodeAggregateId + ), + $tagsToFlush + ); + } } - $cachingHelper = $this->getCachingHelper(); - - foreach ($this->assetService->getUsageReferences($asset) as $reference) { - if (!$reference instanceof AssetUsageInNodeProperties) { - continue; - } + $this->tagsToFlushAfterPersistance = array_merge($tagsToFlush, $this->tagsToFlushAfterPersistance); + } - $workspaceHash = $cachingHelper->renderWorkspaceTagForContextNode($reference->getWorkspaceName()); - $this->securityContext->withoutAuthorizationChecks(function () use ($reference, &$node) { - $node = $this->getContextForReference($reference)->getNodeByIdentifier($reference->getNodeIdentifier()); - }); - - if (!$node instanceof Node) { - $this->systemLogger->warning(sprintf( - 'Found a node reference from node with identifier %s in workspace %s to asset %s,' - . ' but the node could not be fetched.', - $reference->getNodeIdentifier(), - $reference->getWorkspaceName(), - $this->persistenceManager->getIdentifierByObject($asset) - ), LogEnvironment::fromMethodName(__METHOD__)); - continue; - } + /** + * Flush caches according to the previously registered changes. + */ + public function flushCollectedTags(): void + { + $this->flushTags($this->tagsToFlushAfterPersistance); + $this->tagsToFlushAfterPersistance = []; + } - $this->registerNodeChange($node); - - $assetIdentifier = $this->persistenceManager->getIdentifierByObject($asset); - // @see RuntimeContentCache.addTag - $tagName = 'AssetDynamicTag_' . $workspaceHash . '_' . $assetIdentifier; - $this->addTagToFlush( - $tagName, - sprintf( - 'which were tagged with "%s" because asset "%s" has changed.', - $tagName, - $assetIdentifier - ) - ); - }*/ + public function shutdownObject(): void + { + $this->flushCollectedTags(); } } diff --git a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php index e26a5d526e9..76f9a024984 100644 --- a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php +++ b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php @@ -140,13 +140,17 @@ public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $even // cleared, leading to presumably duplicate nodes in the UI. || $eventInstance instanceof NodeAggregateWasMoved ) { - $nodeAggregate = $this->contentRepository->getContentGraph()->findNodeAggregateById( - $eventInstance->getContentStreamId(), + $workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($eventInstance->getContentStreamId()); + if ($workspace === null) { + return; + } + // FIXME: EventInterface->workspaceName + $contentGraph = $this->contentRepository->getContentGraph($workspace->workspaceName); + $nodeAggregate = $contentGraph->findNodeAggregateById( $eventInstance->getNodeAggregateId() ); if ($nodeAggregate) { - $parentNodeAggregates = $this->contentRepository->getContentGraph()->findParentNodeAggregates( - $nodeAggregate->contentStreamId, + $parentNodeAggregates = $contentGraph->findParentNodeAggregates( $nodeAggregate->nodeAggregateId ); foreach ($parentNodeAggregates as $parentNodeAggregate) { @@ -173,8 +177,12 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event !($eventInstance instanceof NodeAggregateWasRemoved) && $eventInstance instanceof EmbedsContentStreamAndNodeAggregateId ) { - $nodeAggregate = $this->contentRepository->getContentGraph()->findNodeAggregateById( - $eventInstance->getContentStreamId(), + $workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($eventInstance->getContentStreamId()); + if ($workspace === null) { + return; + } + // FIXME: EventInterface->workspaceName + $nodeAggregate = $this->contentRepository->getContentGraph($workspace->workspaceName)->findNodeAggregateById( $eventInstance->getNodeAggregateId() ); diff --git a/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php b/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php index 0e50845b659..ac47b1e3172 100644 --- a/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php +++ b/Neos.Neos/Classes/Fusion/Cache/NeosFusionContextSerializer.php @@ -4,30 +4,26 @@ namespace Neos\Neos\Fusion\Cache; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; -use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Fusion\Core\Cache\FusionContextSerializer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** - * Serializer for Fusion's [at]cache.context values + * Serializer for Fusion's \@cache.context values * * Implements special handing for serializing {@see Node} objects in fusions cache context: * - * ``` - * [at]cache { - * mode = 'uncached' - * context { - * 1 = 'node' - * } - * } - * ``` + * \@cache { + * mode = 'uncached' + * context { + * 1 = 'node' + * } + * } * * The property mapper cannot be relied upon to serialize nodes, as this is willingly not implemented. * @@ -49,6 +45,7 @@ public function __construct( public function denormalize(mixed $data, string $type, string $format = null, array $context = []) { if ($type === Node::class) { + /** @var $data array */ return $this->tryDeserializeNode($data); } return $this->fusionContextSerializer->denormalize($data, $type, $format, $context); @@ -67,31 +64,29 @@ public function normalize(mixed $object, string $format = null, array $context = } /** - * @param array $serializedNode + * @param array $serializedNode */ private function tryDeserializeNode(array $serializedNode): ?Node { - $contentRepositoryId = ContentRepositoryId::fromString($serializedNode['contentRepositoryId']); - - $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($serializedNode['workspaceName'])); - if (!$workspace) { + $nodeAddress = NodeAddress::fromArray($serializedNode); + + $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); + + try { + $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( + $nodeAddress->dimensionSpacePoint, + $nodeAddress->workspaceName->isLive() + ? VisibilityConstraints::frontend() + : VisibilityConstraints::withoutRestrictions() + ); + } catch (WorkspaceDoesNotExist $exception) { // in case the workspace was deleted the rendering should probably not come to this very point // still if it does we fail silently // this is also the behaviour for when the property mapper is used return null; } - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $workspace->currentContentStreamId, - DimensionSpacePoint::fromArray($serializedNode['dimensionSpacePoint']), - $workspace->isPublicWorkspace() - ? VisibilityConstraints::frontend() - : VisibilityConstraints::withoutRestrictions() - ); - - $node = $subgraph->findNodeById(NodeAggregateId::fromString($serializedNode['nodeAggregateId'])); + $node = $subgraph->findNodeById($nodeAddress->aggregateId); if (!$node) { // instead of crashing the whole rendering, by silently returning null we will most likely just break // rendering of the sub part here that needs the node @@ -106,22 +101,7 @@ private function tryDeserializeNode(array $serializedNode): ?Node */ private function serializeNode(Node $source): array { - $contentRepository = $this->contentRepositoryRegistry->get( - $source->subgraphIdentity->contentRepositoryId - ); - - $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($source->subgraphIdentity->contentStreamId); - - if (!$workspace) { - throw new \RuntimeException(sprintf('Could not fetch workspace for node (%s) in content stream (%s).', $source->nodeAggregateId->value, $source->subgraphIdentity->contentStreamId->value), 1699780153); - } - - return [ - 'contentRepositoryId' => $source->subgraphIdentity->contentRepositoryId->value, - 'workspaceName' => $workspace->workspaceName->value, - 'dimensionSpacePoint' => $source->subgraphIdentity->dimensionSpacePoint->jsonSerialize(), - 'nodeAggregateId' => $source->nodeAggregateId->value - ]; + return NodeAddress::fromNode($source)->jsonSerialize(); } public function supportsDenormalization(mixed $data, string $type, string $format = null) diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index 0254272e55d..99cc98edb44 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -14,11 +14,13 @@ namespace Neos\Neos\Fusion; +use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\Utility\LogEnvironment; +use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\ResourceManagement\ResourceManager; @@ -103,7 +105,6 @@ class ConvertUrisImplementation extends AbstractFusionObject */ public function evaluate() { - $text = $this->fusionValue('value'); if ($text === '' || $text === null) { @@ -149,12 +150,23 @@ public function evaluate() NodeAggregateId::fromString($matches[2]) ); $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($this->runtime->getControllerContext()->getRequest()); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $uriBuilder->setRequest($possibleRequest); + } else { + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without + // even, if the default param merging would not be required + // this will improve with a reformed uri building: + // https://github.com/neos/flow-development-collection/pull/2744 + $uriBuilder->setRequest( + ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) + ); + } $uriBuilder->setCreateAbsoluteUri($absolute); try { $resolvedUri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); } catch (NoMatchingRouteException) { - $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri. Arguments: %s', $matches[0], json_encode($uriBuilder->getLastArguments())), LogEnvironment::fromMethodName(__METHOD__)); + $this->systemLogger->info(sprintf('Could not resolve "%s" to a live node uri. Arguments: %s', $matches[0], json_encode($uriBuilder->getLastArguments())), LogEnvironment::fromMethodName(__METHOD__)); } $this->runtime->addCacheTag( CacheTag::forDynamicNodeAggregate($contentRepository->id, $nodeAddress->contentStreamId, NodeAggregateId::fromString($matches[2]))->value @@ -205,8 +217,12 @@ protected function replaceLinkTargets($processedContent) $setExternal = $this->fusionValue('setExternal'); $externalLinkTarget = \trim((string)$this->fusionValue('externalLinkTarget')); $resourceLinkTarget = \trim((string)$this->fusionValue('resourceLinkTarget')); - $controllerContext = $this->runtime->getControllerContext(); - $host = $controllerContext->getRequest()->getHttpRequest()->getUri()->getHost(); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $host = $possibleRequest->getHttpRequest()->getUri()->getHost(); + } else { + $host = null; + } $processedContent = \preg_replace_callback( '~~i', static function ($matches) use ($externalLinkTarget, $resourceLinkTarget, $host, $setNoOpener, $setExternal) { diff --git a/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php index 5f2e128a252..11fa3dd6074 100644 --- a/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/DimensionsMenuItemsImplementation.php @@ -7,8 +7,12 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Dimension\ContentDimensionId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\Flow\Annotations as Flow; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; /** * Fusion implementation for a dimensions menu. @@ -27,6 +31,9 @@ */ class DimensionsMenuItemsImplementation extends AbstractMenuItemsImplementation { + #[Flow\Inject()] + protected NodeLabelGeneratorInterface $nodeLabelGenerator; + /** * @return array */ @@ -58,15 +65,20 @@ protected function buildItems(): array $interDimensionalVariationGraph = $dimensionMenuItemsImplementationInternals->interDimensionalVariationGraph; $currentDimensionSpacePoint = $currentNode->subgraphIdentity->dimensionSpacePoint; $contentDimensionIdentifierToLimitTo = $this->getContentDimensionIdentifierToLimitTo(); + try { + $contentGraph = $contentRepository->getContentGraph($currentNode->workspaceName); + } catch (WorkspaceDoesNotExist) { + return $menuItems; + } + foreach ($interDimensionalVariationGraph->getDimensionSpacePoints() as $dimensionSpacePoint) { $variant = null; if ($this->isDimensionSpacePointRelevant($dimensionSpacePoint)) { if ($dimensionSpacePoint->equals($currentDimensionSpacePoint)) { $variant = $currentNode; } else { - $variant = $contentRepository->getContentGraph() + $variant = $contentGraph ->getSubgraph( - $currentNode->subgraphIdentity->contentStreamId, $dimensionSpacePoint, $currentNode->subgraphIdentity->visibilityConstraints, ) @@ -79,7 +91,7 @@ protected function buildItems(): array $contentDimensionIdentifierToLimitTo, $currentNode->nodeAggregateId, $dimensionMenuItemsImplementationInternals, - $contentRepository + $contentGraph ); } @@ -147,7 +159,7 @@ protected function findClosestGeneralizationMatchingDimensionValue( ContentDimensionId $contentDimensionIdentifier, NodeAggregateId $nodeAggregateId, DimensionsMenuItemsImplementationInternals $dimensionMenuItemsImplementationInternals, - ContentRepository $contentRepository + ContentGraphInterface $contentGraph ): ?Node { $generalizations = $dimensionMenuItemsImplementationInternals->interDimensionalVariationGraph ->getWeightedGeneralizations($dimensionSpacePoint); @@ -157,11 +169,10 @@ protected function findClosestGeneralizationMatchingDimensionValue( $generalization->getCoordinate($contentDimensionIdentifier) === $dimensionSpacePoint->getCoordinate($contentDimensionIdentifier) ) { - $variant = $contentRepository->getContentGraph() + $variant = $contentGraph ->getSubgraph( - $this->currentNode->subgraphIdentity->contentStreamId, $generalization, - $this->currentNode->subgraphIdentity->visibilityConstraints, + $this->getCurrentNode()->subgraphIdentity->visibilityConstraints, ) ->findNodeById($nodeAggregateId); if ($variant) { @@ -209,7 +220,7 @@ protected function determineLabel(?Node $variant = null, array $metadata = []): if ($this->getContentDimensionIdentifierToLimitTo()) { return $metadata[$this->getContentDimensionIdentifierToLimitTo()->value]['label'] ?: ''; } elseif ($variant) { - return $variant->getLabel() ?: ''; + return $this->nodeLabelGenerator->getLabel($variant) ?: ''; } else { return array_reduce($metadata, function ($carry, $item) { return $carry . (empty($carry) ? '' : '-') . $item['label']; diff --git a/Neos.Neos/Classes/Fusion/ExceptionHandlers/PageHandler.php b/Neos.Neos/Classes/Fusion/ExceptionHandlers/PageHandler.php index 56ff4bed0a0..bf47fbeea60 100644 --- a/Neos.Neos/Classes/Fusion/ExceptionHandlers/PageHandler.php +++ b/Neos.Neos/Classes/Fusion/ExceptionHandlers/PageHandler.php @@ -19,6 +19,8 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Exception as FlowException; +use Neos\Flow\Mvc\Controller\Arguments; +use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Security\Authorization\PrivilegeManagerInterface; use Neos\Flow\Utility\Environment; use Neos\FluidAdaptor\View\StandaloneView; @@ -103,7 +105,7 @@ protected function handle($fusionPath, \Exception $exception, $referenceCode) 'node' => $node ]); - return $this->wrapHttpResponse($exception, $fluidView->render()); + return $this->wrapHttpResponse($exception, $fluidView->render()->getContents()); } /** @@ -129,7 +131,14 @@ protected function wrapHttpResponse(\Exception $exception, string $bodyContent): protected function prepareFluidView() { $fluidView = new StandaloneView(); - $fluidView->setControllerContext($this->runtime->getControllerContext()); + $fluidView->setControllerContext( + new ControllerContext( + $this->runtime->getControllerContext()->getRequest(), + $this->runtime->getControllerContext()->getResponse(), + new Arguments(), + $this->runtime->getControllerContext()->getUriBuilder() + ) + ); $fluidView->setFormat('html'); $fluidView->setTemplatePathAndFilename('resource://Neos.Neos/Private/Templates/Error/NeosBackendMessage.html'); $fluidView->setLayoutRootPath('resource://Neos.Neos/Private/Layouts/'); diff --git a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php index 17cffd3c6a8..6da6b577a39 100644 --- a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php @@ -135,17 +135,15 @@ public function getWorkspaceChain(?Node $node): array } $contentRepository = $this->contentRepositoryRegistry->get( - $node->subgraphIdentity->contentRepositoryId + $node->contentRepositoryId ); - - /** @var Workspace $currentWorkspace */ - $currentWorkspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId( - $node->subgraphIdentity->contentStreamId + $currentWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName( + $node->workspaceName ); $workspaceChain = []; // TODO: Maybe write CTE here - while ($currentWorkspace instanceof Workspace) { + while ($currentWorkspace !== null) { $workspaceChain[$currentWorkspace->workspaceName->value] = $currentWorkspace; $currentWorkspace = $currentWorkspace->baseWorkspaceName ? $contentRepository->getWorkspaceFinder()->findOneByName($currentWorkspace->baseWorkspaceName) diff --git a/Neos.Neos/Classes/Fusion/Helper/DimensionHelper.php b/Neos.Neos/Classes/Fusion/Helper/DimensionHelper.php index 15bafd94ac1..de4c5b0b691 100644 --- a/Neos.Neos/Classes/Fusion/Helper/DimensionHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/DimensionHelper.php @@ -20,6 +20,7 @@ use Neos\ContentRepository\Core\Dimension\ContentDimensionValues; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; @@ -142,15 +143,18 @@ public function findVariantInDimension(Node $node, ContentDimensionId|string $di { $contentDimensionId = is_string($dimensionName) ? new ContentDimensionId($dimensionName) : $dimensionName; $contentDimensionValue = is_string($dimensionValue) ? new ContentDimensionValue($dimensionValue) : $dimensionValue; - $contentRepository = $this->contentRepositoryRegistry->get($node->subgraphIdentity->contentRepositoryId); - - return $contentRepository - ->getContentGraph() - ->getSubgraph( - $node->subgraphIdentity->contentStreamId, - $node->subgraphIdentity->dimensionSpacePoint->vary($contentDimensionId, $contentDimensionValue->value), - $node->subgraphIdentity->visibilityConstraints - )->findNodeById($node->nodeAggregateId); + $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); + + try { + return $contentRepository + ->getContentGraph($node->workspaceName) + ->getSubgraph( + $node->dimensionSpacePoint->vary($contentDimensionId, $contentDimensionValue->value), + $node->visibilityConstraints + )->findNodeById($node->aggregateId); + } catch (WorkspaceDoesNotExist) { + return null; + } } public function allowsCallOfMethod($methodName): bool diff --git a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php index 28de5534bb7..f79add7ba04 100644 --- a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindAncestorNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; +use Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateFinder; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; @@ -96,7 +97,11 @@ public function nearestContentCollection(Node $node, string $nodePath): Node } /** - * Generate a label for a node with a chaining mechanism. To be used in nodetype definitions. + * Generate a label for a node with a chaining mechanism. To be used in NodeType definition: + * + * 'Vendor.Site:MyContent': + * label: "${Neos.Node.labelForNode(node).prefix('foo')}" + * */ public function labelForNode(Node $node): NodeLabelToken { @@ -142,6 +147,12 @@ public function getNodeType(Node $node): NodeType return $this->getNodeTypeInternal($node); } + public function isNodeTypeExistent(Node $node): bool + { + $contentRepository = $this->contentRepositoryRegistry->get($node->subgraphIdentity->contentRepositoryId); + return $contentRepository->getNodeTypeManager()->hasNodeType($node->nodeTypeName); + } + public function serializedNodeAddress(Node $node): string { $contentRepository = $this->contentRepositoryRegistry->get( diff --git a/Neos.Neos/Classes/Fusion/ImageUriImplementation.php b/Neos.Neos/Classes/Fusion/ImageUriImplementation.php index 506fb8296f4..263861658a5 100644 --- a/Neos.Neos/Classes/Fusion/ImageUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/ImageUriImplementation.php @@ -15,6 +15,7 @@ namespace Neos\Neos\Fusion; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\ActionRequest; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\ThumbnailConfiguration; use Neos\Media\Domain\Service\AssetService; @@ -181,7 +182,9 @@ public function evaluate() $this->getFormat() ); } - $request = $this->getRuntime()->getControllerContext()->getRequest(); + + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + $request = $possibleRequest instanceof ActionRequest ? $possibleRequest : null; $thumbnailData = $this->assetService->getThumbnailUriAndSizeForAsset($asset, $thumbnailConfiguration, $request); if ($thumbnailData === null) { return ''; diff --git a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php index 4bee1c0ec52..9cf4abc075a 100644 --- a/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php +++ b/Neos.Neos/Classes/Fusion/MenuItemsImplementation.php @@ -24,7 +24,9 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; +use Neos\Flow\Annotations as Flow; use Neos\Fusion\Exception as FusionException; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; use Neos\Neos\Domain\Service\NodeTypeNameFactory; /** @@ -62,6 +64,9 @@ class MenuItemsImplementation extends AbstractMenuItemsImplementation */ protected ?NodeTypeCriteria $nodeTypeCriteria = null; + #[Flow\Inject()] + protected NodeLabelGeneratorInterface $nodeLabelGenerator; + /** * The last navigation level which should be rendered. * @@ -215,7 +220,7 @@ protected function buildMenuItemFromNode(Node $node): MenuItem return new MenuItem( $node, $this->isCalculateItemStatesEnabled() ? $this->calculateItemState($node) : null, - $node->getLabel(), + $this->nodeLabelGenerator->getLabel($node), 0, [], $this->buildUri($node) @@ -239,7 +244,7 @@ protected function buildMenuItemFromSubtree(Subtree $subtree, int $startLevel = return new MenuItem( $node, $this->isCalculateItemStatesEnabled() ? $this->calculateItemState($node) : null, - $node->getLabel(), + $this->nodeLabelGenerator->getLabel($node), $subtree->level + $startLevel, $children, $this->buildUri($node) diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 7845fdbcaed..9e1da3c7d83 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -14,14 +14,16 @@ namespace Neos\Neos\Fusion; +use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Fusion\FusionObjects\AbstractFusionObject; -use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\Neos\FrontendRouting\NodeUriBuilder; use Psr\Log\LoggerInterface; @@ -159,7 +161,18 @@ public function evaluate() );*/ $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($this->runtime->getControllerContext()->getRequest()); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $uriBuilder->setRequest($possibleRequest); + } else { + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without + // even, if the default param merging would not be required + // this will improve with a reformed uri building: + // https://github.com/neos/flow-development-collection/pull/2744 + $uriBuilder->setRequest( + ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) + ); + } $uriBuilder ->setFormat($this->getFormat()) ->setCreateAbsoluteUri($this->isAbsolute()) diff --git a/Neos.Neos/Classes/Fusion/PluginImplementation.php b/Neos.Neos/Classes/Fusion/PluginImplementation.php index c623542afd9..36ba957cc32 100644 --- a/Neos.Neos/Classes/Fusion/PluginImplementation.php +++ b/Neos.Neos/Classes/Fusion/PluginImplementation.php @@ -90,7 +90,10 @@ public function getArgumentNamespace() */ protected function buildPluginRequest(): ActionRequest { - $parentRequest = $this->runtime->getControllerContext()->getRequest(); + $parentRequest = $this->runtime->fusionGlobals->get('request'); + if (!$parentRequest instanceof ActionRequest) { + throw new \RuntimeException('Fusion Plugins must be rendered with an ActionRequest set as fusion-global.', 1706624581); + } $pluginRequest = $parentRequest->createSubRequest(); $pluginRequest->setArgumentNamespace('--' . $this->getPluginNamespace()); $this->passArgumentsToPluginRequest($pluginRequest); diff --git a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php index fd67227a3de..a4568f635bf 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php +++ b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php @@ -21,6 +21,7 @@ use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved; @@ -232,16 +233,21 @@ private function whenRootWorkspaceWasCreated(RootWorkspaceWasCreated $event): vo private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void { - // WORKAROUND: we simply use the first MoveNodeMapping here to find the dimension space point - // @todo properly handle this - /* @var \Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping[] $mapping */ - $mapping = iterator_to_array($event->nodeMoveMappings); - - $this->markAsMoved( - $event->getContentStreamId(), - $event->getNodeAggregateId(), - $mapping[0]->movedNodeOrigin - ); + $affectedDimensionSpacePoints = iterator_to_array($event->succeedingSiblingsForCoverage->toDimensionSpacePointSet()); + $arbitraryDimensionSpacePoint = reset($affectedDimensionSpacePoints); + if ($arbitraryDimensionSpacePoint instanceof DimensionSpacePoint) { + // always the case due to constraint enforcement (at least one DSP is selected and must have a succeeding sibling or null) + + // WORKAROUND: we simply use the event's first DSP here as the origin dimension space point. + // But this DSP is not necessarily occupied. + // @todo properly handle this by storing the necessary information in the projection + + $this->markAsMoved( + $event->getContentStreamId(), + $event->getNodeAggregateId(), + OriginDimensionSpacePoint::fromDimensionSpacePoint($arbitraryDimensionSpacePoint) + ); + } } private function whenNodePropertiesWereSet(NodePropertiesWereSet $event): void diff --git a/Neos.Neos/Classes/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index 445c1e62b63..e9553c778d9 100644 --- a/Neos.Neos/Classes/Service/LinkingService.php +++ b/Neos.Neos/Classes/Service/LinkingService.php @@ -314,8 +314,7 @@ public function createNodeUri( $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($node); $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($nodeAddress->workspaceName); - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $nodeAddress->contentStreamId, + $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, $workspace && !$workspace->isPublicWorkspace() ? VisibilityConstraints::withoutRestrictions() @@ -351,10 +350,10 @@ public function createNodeUri( $this->lastLinkedNode = $node; $contentRepository = $this->contentRepositoryRegistry->get( - $node->subgraphIdentity->contentRepositoryId + $node->contentRepositoryId ); - $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId( - $node->subgraphIdentity->contentStreamId + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName( + $node->workspaceName ); $request = $controllerContext->getRequest()->getMainRequest(); $uriBuilder = clone $controllerContext->getUriBuilder(); diff --git a/Neos.Neos/Classes/TypeConverter/HackyNodeAddressToNodeConverter.php b/Neos.Neos/Classes/TypeConverter/HackyNodeAddressToNodeConverter.php index 50069aed402..9786c418fc9 100644 --- a/Neos.Neos/Classes/TypeConverter/HackyNodeAddressToNodeConverter.php +++ b/Neos.Neos/Classes/TypeConverter/HackyNodeAddressToNodeConverter.php @@ -80,9 +80,8 @@ public function convertFrom( $nodeAddressFactory = NodeAddressFactory::create($contentRepository); $nodeAddress = $nodeAddressFactory->createFromUriString($source); - $subgraph = $contentRepository->getContentGraph() + $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName) ->getSubgraph( - $nodeAddress->contentStreamId, $nodeAddress->dimensionSpacePoint, $nodeAddress->isInLiveWorkspace() ? VisibilityConstraints::frontend() diff --git a/Neos.Neos/Classes/Utility/NodeUriPathSegmentGenerator.php b/Neos.Neos/Classes/Utility/NodeUriPathSegmentGenerator.php index 33a24f941f1..e80e4bc947c 100644 --- a/Neos.Neos/Classes/Utility/NodeUriPathSegmentGenerator.php +++ b/Neos.Neos/Classes/Utility/NodeUriPathSegmentGenerator.php @@ -18,6 +18,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\Flow\Annotations as Flow; use Neos\Flow\I18n\Locale; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; use Neos\Neos\Exception; use Neos\Neos\Service\TransliterationService; @@ -30,6 +31,9 @@ class NodeUriPathSegmentGenerator #[Flow\Inject] protected TransliterationService $transliterationService; + #[Flow\Inject] + protected NodeLabelGeneratorInterface $nodeLabelGenerator; + /** * Generates a URI path segment for a given node taking its language dimension value into account * @@ -40,7 +44,7 @@ public function generateUriPathSegment(?Node $node = null, ?string $text = null) { $language = null; if ($node) { - $text = $text ?: $node->getLabel() ?: ($node->nodeName?->value ?? ''); + $text = $text ?: $this->nodeLabelGenerator->getLabel($node) ?: ($node->nodeName?->value ?? ''); $languageDimensionValue = $node->originDimensionSpacePoint->coordinates['language'] ?? null; if (!is_null($languageDimensionValue)) { $locale = new Locale($languageDimensionValue); diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index bf4807b0e65..fd6814acf20 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -16,15 +16,13 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Http\RequestHandler as HttpRequestHandler; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; -use Neos\Flow\Mvc\Controller\Arguments; -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\Mvc\View\AbstractView; use Neos\Flow\ObjectManagement\ObjectManagerInterface; @@ -32,7 +30,6 @@ use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime as FusionRuntime; use Neos\Fusion\Core\RuntimeFactory; -use Neos\Fusion\Exception\RuntimeException; use Neos\Neos\Domain\Model\RenderingMode; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; @@ -40,6 +37,8 @@ use Neos\Neos\Domain\Service\SiteNodeUtility; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionFailedException; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; class FusionExceptionView extends AbstractView { @@ -91,14 +90,7 @@ class FusionExceptionView extends AbstractView #[Flow\Inject] protected DomainRepository $domainRepository; - /** - * @return mixed - * @throws \Neos\Flow\I18n\Exception\InvalidLocaleIdentifierException - * @throws \Neos\Fusion\Exception - * @throws \Neos\Neos\Domain\Exception - * @throws \Neos\Flow\Security\Exception - */ - public function render() + public function render(): ResponseInterface|StreamInterface { $requestHandler = $this->bootstrap->getActiveRequestHandler(); @@ -114,27 +106,26 @@ public function render() return $this->renderErrorWelcomeScreen(); } - $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); $fusionExceptionViewInternals = $this->contentRepositoryRegistry->buildService( $siteDetectionResult->contentRepositoryId, new FusionExceptionViewInternalsFactory() ); $dimensionSpacePoint = $fusionExceptionViewInternals->getArbitraryDimensionSpacePoint(); - $liveWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::forLive()); - - $currentSiteNode = null; $site = $this->siteRepository->findOneByNodeName($siteDetectionResult->siteNodeName); - if ($liveWorkspace && $site) { + + if (!$site) { + return $this->renderErrorWelcomeScreen(); + } + + try { $currentSiteNode = $this->siteNodeUtility->findSiteNodeBySite( $site, - $liveWorkspace->currentContentStreamId, + WorkspaceName::forLive(), $dimensionSpacePoint, VisibilityConstraints::frontend() ); - } - - if (!$currentSiteNode) { + } catch (WorkspaceDoesNotExist | \RuntimeException) { return $this->renderErrorWelcomeScreen(); } @@ -143,22 +134,16 @@ public function render() $request->setFormat('html'); $uriBuilder = new UriBuilder(); $uriBuilder->setRequest($request); - $controllerContext = new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - ); /** @var SecurityContext $securityContext */ $securityContext = $this->objectManager->get(SecurityContext::class); $securityContext->setRequest($request); - $fusionRuntime = $this->getFusionRuntime($currentSiteNode, $controllerContext); + $fusionRuntime = $this->getFusionRuntime($currentSiteNode, $request); $this->setFallbackRuleFromDimension($dimensionSpacePoint); - $fusionRuntime->pushContextArray(array_merge( + return $fusionRuntime->renderEntryPathWithContext('error', array_merge( $this->variables, [ 'node' => $currentSiteNode, @@ -166,42 +151,11 @@ public function render() 'site' => $currentSiteNode ] )); - - try { - $output = $fusionRuntime->render('error'); - return $this->extractBodyFromOutput($output); - } catch (RuntimeException $exception) { - throw $exception->getPrevious() ?: $exception; - } finally { - $fusionRuntime->popContext(); - } } - /** - * @param string $output - * @return string The message body without the message head - */ - protected function extractBodyFromOutput(string $output): string - { - if (substr($output, 0, 5) === 'HTTP/') { - $endOfHeader = strpos($output, "\r\n\r\n"); - if ($endOfHeader !== false) { - $output = substr($output, $endOfHeader + 4); - } - } - return $output; - } - - /** - * @param Node $currentSiteNode - * @param ControllerContext $controllerContext - * @return FusionRuntime - * @throws \Neos\Fusion\Exception - * @throws \Neos\Neos\Domain\Exception - */ protected function getFusionRuntime( Node $currentSiteNode, - ControllerContext $controllerContext + ActionRequest $actionRequest ): FusionRuntime { if ($this->fusionRuntime === null) { $site = $this->siteRepository->findSiteBySiteNode($currentSiteNode); @@ -209,14 +163,13 @@ protected function getFusionRuntime( $fusionConfiguration = $this->fusionService->createFusionConfigurationFromSite($site); $fusionGlobals = FusionGlobals::fromArray([ - 'request' => $controllerContext->getRequest(), + 'request' => $actionRequest, 'renderingMode' => RenderingMode::createFrontend() ]); $this->fusionRuntime = $this->runtimeFactory->createFromConfiguration( $fusionConfiguration, $fusionGlobals ); - $this->fusionRuntime->setControllerContext($controllerContext); if (isset($this->options['enableContentCache']) && $this->options['enableContentCache'] !== null) { $this->fusionRuntime->setEnableContentCache($this->options['enableContentCache']); @@ -225,11 +178,12 @@ protected function getFusionRuntime( return $this->fusionRuntime; } - private function renderErrorWelcomeScreen(): mixed + private function renderErrorWelcomeScreen(): ResponseInterface|StreamInterface { // in case no neos site being there or no site node we cannot continue with the fusion exception view, // as we wouldn't know the site and cannot get the site's root.fusion // instead we render the welcome screen directly + /** @var \Neos\Fusion\View\FusionView $view */ $view = \Neos\Fusion\View\FusionView::createWithOptions([ 'fusionPath' => 'Neos/Fusion/NotFoundExceptions', 'fusionPathPatterns' => ['resource://Neos.Neos/Private/Fusion/Error/Root.fusion'], diff --git a/Neos.Neos/Classes/View/FusionView.php b/Neos.Neos/Classes/View/FusionView.php index 2f95ba9010f..0783275afb1 100644 --- a/Neos.Neos/Classes/View/FusionView.php +++ b/Neos.Neos/Classes/View/FusionView.php @@ -14,17 +14,17 @@ namespace Neos\Neos\View; -use GuzzleHttp\Psr7\Message; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\View\AbstractView; use Neos\Flow\Security\Context; use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; use Neos\Fusion\Core\RuntimeFactory; -use Neos\Fusion\Exception\RuntimeException; use Neos\Neos\Domain\Model\RenderingMode; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\FusionService; @@ -33,6 +33,7 @@ use Neos\Neos\Exception; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; /** * A Fusion view for Neos @@ -55,13 +56,19 @@ class FusionView extends AbstractView protected RenderingModeService $renderingModeService; /** - * Renders the view + * Via {@see assign} request using the "request" key, + * will be available also as Fusion global in the runtime. + */ + protected ?ActionRequest $assignedActionRequest = null; + + /** + * Render the view to a full response in case a Neos.Fusion:Http.Message was used. + * If the fusion path contains a simple string a stream will be rendered. * - * @return string|ResponseInterface The rendered view * @throws \Exception if no node is given * @api */ - public function render(): string|ResponseInterface + public function render(): ResponseInterface|StreamInterface { $currentNode = $this->getCurrentNode(); @@ -76,20 +83,11 @@ public function render(): string|ResponseInterface $this->setFallbackRuleFromDimension($currentNode->subgraphIdentity->dimensionSpacePoint); - $fusionRuntime->pushContextArray([ + return $fusionRuntime->renderEntryPathWithContext($this->fusionPath, [ 'node' => $currentNode, 'documentNode' => $this->getClosestDocumentNode($currentNode) ?: $currentNode, 'site' => $currentSiteNode ]); - try { - $output = $fusionRuntime->render($this->fusionPath); - $output = $this->parsePotentialRawHttpResponse($output); - } catch (RuntimeException $exception) { - throw $exception->getPrevious() ?: $exception; - } - $fusionRuntime->popContext(); - - return $output; } /** @@ -131,35 +129,6 @@ public function render(): string|ResponseInterface */ protected $securityContext; - /** - * @param string $output - * @return string|ResponseInterface If output is a string with a HTTP preamble a ResponseInterface - * otherwise the original output. - */ - protected function parsePotentialRawHttpResponse($output) - { - if ($this->isRawHttpResponse($output)) { - return Message::parseResponse($output); - } - - return $output; - } - - /** - * Checks if the mixed input looks like a raw HTTTP response. - * - * @param mixed $value - * @return bool - */ - protected function isRawHttpResponse($value): bool - { - if (is_string($value) && strpos($value, 'HTTP/') === 0) { - return true; - } - - return false; - } - /** * Is it possible to render $node with $his->fusionPath? * @@ -238,15 +207,14 @@ protected function getFusionRuntime(Node $currentSiteNode) $renderingMode = $this->renderingModeService->findByName($this->getOption('renderingModeName')); - $fusionGlobals = FusionGlobals::fromArray([ - 'request' => $this->controllerContext->getRequest(), + $fusionGlobals = FusionGlobals::fromArray(array_filter([ + 'request' => $this->assignedActionRequest, 'renderingMode' => $renderingMode - ]); + ])); $this->fusionRuntime = $this->runtimeFactory->createFromConfiguration( $fusionConfiguration, $fusionGlobals ); - $this->fusionRuntime->setControllerContext($this->controllerContext); if (isset($this->options['enableContentCache']) && $this->options['enableContentCache'] !== null) { $this->fusionRuntime->setEnableContentCache($this->options['enableContentCache']); @@ -261,9 +229,32 @@ protected function getFusionRuntime(Node $currentSiteNode) * @param string $key * @param mixed $value */ - public function assign($key, $value): AbstractView + public function assign(string $key, mixed $value): self { + if ($key === 'request') { + // the request cannot be used as "normal" fusion variable and must be treated as FusionGlobal + // to for example not cache it accidentally + // additionally we need it for special request based handling in the view + $this->assignedActionRequest = $value; + return $this; + } $this->fusionRuntime = null; return parent::assign($key, $value); } + + /** + * Legacy layer to set the request for this view if not set already. + * + * Please use {@see assign} with "request" instead + * + * $view->assign('request"', $this->request) + * + * @deprecated with Neos 9 + */ + public function setControllerContext(ControllerContext $controllerContext) + { + if (!$this->assignedActionRequest) { + $this->assignedActionRequest = $controllerContext->getRequest(); + } + } } diff --git a/Neos.Neos/Classes/View/Service/AssetJsonView.php b/Neos.Neos/Classes/View/Service/AssetJsonView.php index bb15b6306e5..c56c0c596ec 100644 --- a/Neos.Neos/Classes/View/Service/AssetJsonView.php +++ b/Neos.Neos/Classes/View/Service/AssetJsonView.php @@ -16,12 +16,15 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\View\JsonView; +use Psr\Http\Message\ResponseInterface; /** * A view specialised on a JSON representation of Assets. * * This view is used by the service controllers in Neos\Neos\Controller\Service\ * + * @deprecated with Neos 9, the JsonView should not be used + * @internal only to be used internally * @Flow\Scope("prototype") */ class AssetJsonView extends JsonView @@ -29,10 +32,8 @@ class AssetJsonView extends JsonView /** * Configures rendering according to the set variable(s) and calls * render on the parent. - * - * @return string */ - public function render() + public function render(): ResponseInterface { if (isset($this->variables['assets'])) { $this->setConfiguration( diff --git a/Neos.Neos/Classes/View/Service/NodeJsonView.php b/Neos.Neos/Classes/View/Service/NodeJsonView.php index 24ce05ea747..b4a10ca6ff8 100644 --- a/Neos.Neos/Classes/View/Service/NodeJsonView.php +++ b/Neos.Neos/Classes/View/Service/NodeJsonView.php @@ -16,12 +16,15 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\View\JsonView; +use Psr\Http\Message\ResponseInterface; /** * A view specialised on a JSON representation of Nodes. * * This view is used by the service controllers in Neos\Neos\Controller\Service\ * + * @deprecated with Neos 9, the JsonView should not be used + * @internal only to be used internally * @Flow\Scope("prototype") */ class NodeJsonView extends JsonView @@ -29,10 +32,8 @@ class NodeJsonView extends JsonView /** * Configures rendering according to the set variable(s) and calls * render on the parent. - * - * @return string */ - public function render() + public function render(): ResponseInterface { if (isset($this->variables['nodes'])) { $this->setConfiguration( diff --git a/Neos.Neos/Classes/View/Service/WorkspaceJsonView.php b/Neos.Neos/Classes/View/Service/WorkspaceJsonView.php index 52c8b1e261e..04dd5467ad8 100644 --- a/Neos.Neos/Classes/View/Service/WorkspaceJsonView.php +++ b/Neos.Neos/Classes/View/Service/WorkspaceJsonView.php @@ -16,12 +16,15 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\View\JsonView; +use Psr\Http\Message\ResponseInterface; /** * A view specialised on a JSON representation of Workspaces. * * This view is used by the service controllers in Neos\Neos\Controller\Service\ * + * @deprecated with Neos 9, the JsonView should not be used + * @internal only to be used internally * @Flow\Scope("prototype") */ class WorkspaceJsonView extends JsonView @@ -29,10 +32,8 @@ class WorkspaceJsonView extends JsonView /** * Configures rendering according to the set variable(s) and calls * render on the parent. - * - * @return string */ - public function render() + public function render(): ResponseInterface { if (isset($this->variables['workspaces'])) { $this->setConfiguration( diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index f75ef066aa9..a466b777065 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -20,6 +20,7 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\FrontendRouting\NodeAddress; use Neos\Neos\FrontendRouting\NodeAddressFactory; @@ -149,6 +150,12 @@ class NodeViewHelper extends AbstractTagBasedViewHelper */ protected $throwableStorage; + /** + * @Flow\Inject + * @var NodeLabelGeneratorInterface + */ + protected $nodeLabelGenerator; + /** * Initialize arguments * @@ -277,9 +284,8 @@ public function render(): string } - $subgraph = $contentRepository->getContentGraph() + $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName) ->getSubgraph( - $nodeAddress->contentStreamId, $nodeAddress->dimensionSpacePoint, $node->subgraphIdentity->visibilityConstraints ); @@ -341,7 +347,7 @@ public function render(): string $this->templateVariableContainer->remove($this->arguments['nodeVariableName']); if ($content === null && $resolvedNode !== null) { - $content = $resolvedNode->getLabel(); + $content = $this->nodeLabelGenerator->getLabel($resolvedNode); } $this->tag->setContent($content); @@ -370,8 +376,7 @@ private function resolveNodeAddressFromString( NodeAggregateId::fromString(\mb_substr($path, 7)) ); } - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $documentNodeAddress->contentStreamId, + $subgraph = $contentRepository->getContentGraph($documentNodeAddress->workspaceName)->getSubgraph( $documentNodeAddress->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); diff --git a/Neos.Neos/Classes/ViewHelpers/StandaloneViewViewHelper.php b/Neos.Neos/Classes/ViewHelpers/StandaloneViewViewHelper.php index 8408f9a5cb6..4851341cf56 100644 --- a/Neos.Neos/Classes/ViewHelpers/StandaloneViewViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/StandaloneViewViewHelper.php @@ -74,6 +74,6 @@ public function render(): string { $standaloneView = new StandaloneView($this->controllerContext->getRequest()); $standaloneView->setTemplatePathAndFilename($this->arguments['templatePathAndFilename']); - return $standaloneView->assignMultiple($this->arguments['arguments'])->render(); + return $standaloneView->assignMultiple($this->arguments['arguments'])->render()->getContents(); } } diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 67438d4afb5..1b3ef235890 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -273,8 +273,7 @@ private function resolveNodeAddressFromString(string $path): ?NodeAddress NodeAggregateId::fromString(\mb_substr($path, 7)) ); } - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $documentNodeAddress->contentStreamId, + $subgraph = $contentRepository->getContentGraph($documentNodeAddress->workspaceName)->getSubgraph( $documentNodeAddress->dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); diff --git a/Neos.Neos/Configuration/Objects.yaml b/Neos.Neos/Configuration/Objects.yaml index 56b7ac6e23e..be5cd60dd91 100644 --- a/Neos.Neos/Configuration/Objects.yaml +++ b/Neos.Neos/Configuration/Objects.yaml @@ -80,3 +80,6 @@ Neos\Neos\AssetUsage\GlobalAssetUsageService: arguments: 3: setting: Neos.Neos.assetUsage.contentRepositories + +Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface: + className: Neos\Neos\Domain\NodeLabel\DelegatingNodeLabelRenderer diff --git a/Neos.Neos/Configuration/Settings.Sites.yaml b/Neos.Neos/Configuration/Settings.Sites.yaml new file mode 100755 index 00000000000..3c62589e72e --- /dev/null +++ b/Neos.Neos/Configuration/Settings.Sites.yaml @@ -0,0 +1,18 @@ +# # +# Site specific settings # +# # +# This file contains settings specific to Neos Sites # + +Neos: + + Neos: + sitePresets: + 'default': + uriPathSuffix: '.html' + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\AutoUriPathResolverFactory + sites: + '*': + preset: 'default' diff --git a/Neos.Neos/Configuration/Settings.yaml b/Neos.Neos/Configuration/Settings.yaml index 11ca11829ba..336f42b2aa2 100755 --- a/Neos.Neos/Configuration/Settings.yaml +++ b/Neos.Neos/Configuration/Settings.yaml @@ -11,9 +11,6 @@ Neos: Neos: - contentDimensions: - resolution: - uriPathSegmentDelimiter: '_' fusion: # if set to true, Fusion is cached on a per-site basis. @@ -54,19 +51,6 @@ Neos: More information and contribution opportunities at https://www.neos.io --> - routing: - # Setting this to true allows to use an empty uriSegment for default dimensions. - # The only limitation is that all segments must be unique across all dimenions. - supportEmptySegmentForDimensions: true - - sites: - '*': - uriPathSuffix: '.html' - contentRepository: default - contentDimensions: - resolver: - factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\AutoUriPathResolverFactory - nodeTypes: groups: general: @@ -217,6 +201,33 @@ Neos: features: upload: true mediaBrowser: true + Neos\Media\Domain\Model\Audio: + typeConverter: Neos\Neos\TypeConverter\EntityToIdentityConverter + editor: Neos.Neos/Inspector/Editors/AssetEditor + editorOptions: + features: + upload: true + mediaBrowser: true + constraints: + mediaTypes: + - 'audio/*' + Neos\Media\Domain\Model\Document: + typeConverter: Neos\Neos\TypeConverter\EntityToIdentityConverter + editor: Neos.Neos/Inspector/Editors/AssetEditor + editorOptions: + features: + upload: true + mediaBrowser: true + Neos\Media\Domain\Model\Video: + typeConverter: Neos\Neos\TypeConverter\EntityToIdentityConverter + editor: Neos.Neos/Inspector/Editors/AssetEditor + editorOptions: + features: + upload: true + mediaBrowser: true + constraints: + mediaTypes: + - 'video/*' array: typeConverter: Neos\Flow\Property\TypeConverter\TypedArrayConverter editor: Neos.Neos/Inspector/Editors/AssetEditor @@ -406,6 +417,20 @@ Neos: ö: oe ü: ue + # Configures defaults for node label generation + labelGenerator: + eel: + defaultContext: + String: Neos\Eel\Helper\StringHelper + Array: Neos\Eel\Helper\ArrayHelper + Date: Neos\Eel\Helper\DateHelper + Configuration: Neos\Eel\Helper\ConfigurationHelper + Math: Neos\Eel\Helper\MathHelper + Json: Neos\Eel\Helper\JsonHelper + I18n: Neos\Flow\I18n\EelHelper\TranslationHelper + q: Neos\Eel\FlowQuery\FlowQuery::q + Neos.Node: Neos\Neos\Fusion\Helper\NodeHelper + Flow: aop: globalObjects: @@ -487,42 +512,6 @@ Neos: neos-site: Sites neos-plugin: Plugins - ContentRepository: - # Definition of available content dimensions. Additional content dimensions may be defined in third-party packages - # or via global settings. - # - #contentDimensions: - # - # # Content dimension "language" serves for translation of content into different languages. Its value specifies - # # the language or language variant by means of a locale. - # 'language': - # # The default dimension that is applied when creating nodes without specifying a dimension - # default: 'mul_ZZ' - # # The default preset to use if no URI segment was given when resolving languages in the router - # defaultPreset: 'all' - # label: 'Language' - # icon: 'icon-language' - # presets: - # 'all': - # label: 'All languages' - # values: ['mul_ZZ'] - # uriSegment: 'all' - # # Example for additional languages: - # - # 'en_GB': - # label: 'English (Great Britain)' - # values: ['en_GB', 'en_ZZ', 'mul_ZZ'] - # uriSegment: 'gb' - # 'de': - # label: 'German (Germany)' - # values: ['de_DE', 'de_ZZ', 'mul_ZZ'] - # uriSegment: 'de' - - labelGenerator: - eel: - defaultContext: - Neos.Node: Neos\Neos\Fusion\Helper\NodeHelper - Fusion: rendering: exceptionHandler: Neos\Fusion\Core\ExceptionHandlers\ThrowingHandler diff --git a/Neos.Neos/Documentation/References/CommandReference.rst b/Neos.Neos/Documentation/References/CommandReference.rst index 19492ad687a..1e3b24895d5 100644 --- a/Neos.Neos/Documentation/References/CommandReference.rst +++ b/Neos.Neos/Documentation/References/CommandReference.rst @@ -19,7 +19,7 @@ commands that may be available, use:: ./flow help -The following reference was automatically generated from code on 2024-04-20 +The following reference was automatically generated from code on 2024-05-14 .. _`Neos Command Reference: NEOS.FLOW`: diff --git a/Neos.Neos/Documentation/References/EelHelpersReference.rst b/Neos.Neos/Documentation/References/EelHelpersReference.rst index 63eb4b33ff6..12d85c4806c 100644 --- a/Neos.Neos/Documentation/References/EelHelpersReference.rst +++ b/Neos.Neos/Documentation/References/EelHelpersReference.rst @@ -3,7 +3,7 @@ Eel Helpers Reference ===================== -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Eel Helpers Reference: Array`: diff --git a/Neos.Neos/Documentation/References/FlowQueryOperationReference.rst b/Neos.Neos/Documentation/References/FlowQueryOperationReference.rst index fad1ac8b3af..564b9748f58 100644 --- a/Neos.Neos/Documentation/References/FlowQueryOperationReference.rst +++ b/Neos.Neos/Documentation/References/FlowQueryOperationReference.rst @@ -3,7 +3,7 @@ FlowQuery Operation Reference ============================= -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`FlowQuery Operation Reference: add`: diff --git a/Neos.Neos/Documentation/References/NeosFusionReference.rst b/Neos.Neos/Documentation/References/NeosFusionReference.rst index 113099e9115..bc26a54be82 100644 --- a/Neos.Neos/Documentation/References/NeosFusionReference.rst +++ b/Neos.Neos/Documentation/References/NeosFusionReference.rst @@ -927,7 +927,7 @@ The following properties are passed over to :ref:`Neos_Neos__MenuItems` internal :maximumLevels: (integer) Restrict the maximum depth of items in the menu (relative to ``entryLevel``) :startingPoint: (optional, Node) The node where the menu hierarchy starts. If not specified explicitly the startingPoint is calculated from (``node`` and ``entryLevel``), defaults to ``null`` :filter: (string) Filter items by node type (e.g. ``'!My.Site:News,Neos.Neos:Document'``), defaults to ``'Neos.Neos:Document'``. The filter is only used for fetching subItems and is ignored for determining the ``startingPoint`` -:renderHiddenInIndex: (boolean) Whether nodes with ``hiddenInIndex`` should be rendered, defaults to ``false`` +:renderHiddenInMenu: (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered, defaults to ``false`` :calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false``. :itemCollection: (optional, array of Nodes) Explicitly set the Node items for the menu (taking precedence over ``startingPoints`` and ``entryLevel`` and ``lastLevel``). The children for each ``Node`` will be fetched taking the ``maximumLevels`` property into account. @@ -957,7 +957,7 @@ The following properties are passed over to :ref:`Neos_Neos__BreadcrumbMenuItems :node: (Node) The current node to render the menu for. Defaults to ``documentNode`` from the fusion context :maximumLevels: (integer) Restrict the maximum depth of items in the menu, defaults to ``0`` -:renderHiddenInIndex: (boolean) Whether nodes with ``hiddenInIndex`` should be rendered (the current documentNode is always included), defaults to ``false``. +:renderHiddenInMenu: (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered (the current documentNode is always included), defaults to ``false``. :calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false`` Example:: @@ -986,7 +986,7 @@ The following fusion properties are passed over to :ref:`Neos_Neos__DimensionsMe :dimension: (optional, string): name of the dimension which this menu should be based on. Example: "language". :presets: (optional, array): If set, the presets rendered will be taken from this list of preset identifiers :includeAllPresets: (boolean, default **false**) If TRUE, include all presets, not only allowed combinations -:renderHiddenInIndex: (boolean, default **true**) If TRUE, render nodes which are marked as "hidded-in-index" +:renderHiddenInMenu: (boolean, default **true**) Whether nodes with the property ``hiddenInMenu`` should be rendered :calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false`` .. note:: The ``items`` of the ``DimensionsMenu`` are internally calculated with the prototype :ref:`Neos_Neos__DimensionsMenuItems` which @@ -1017,7 +1017,7 @@ Create a list of menu-items items for nodes. :maximumLevels: (integer) Restrict the maximum depth of items in the menu (relative to ``entryLevel``) :startingPoint: (optional, Node) The node where the menu hierarchy starts. If not specified explicitly the startingPoint is calculated from (``node`` and ``entryLevel``), defaults to ``null`` :filter: (string) Filter items by node type (e.g. ``'!My.Site:News,Neos.Neos:Document'``), defaults to ``'Neos.Neos:Document'``. The filter is only used for fetching subItems and is ignored for determining the ``startingPoint`` -:renderHiddenInIndex: (boolean) Whether nodes with ``hiddenInIndex`` should be rendered, defaults to ``false`` +:renderHiddenInMenu: (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered, defaults to ``false`` :calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false``. :itemCollection: (optional, array of Nodes) Explicitly set the Node items for the menu (taking precedence over ``startingPoints`` and ``entryLevel`` and ``lastLevel``). The children for each ``Node`` will be fetched taking the ``maximumLevels`` property into account. @@ -1084,7 +1084,7 @@ Create a list of of menu-items for the breadcrumb (ancestor documents). :node: (Node) The current node to render the menu for. Defaults to ``documentNode`` from the fusion context :maximumLevels: (integer) Restrict the maximum depth of items in the menu, defaults to ``0`` -:renderHiddenInIndex: (boolean) Whether nodes with ``hiddenInIndex`` should be rendered (the current documentNode is always included), defaults to ``false``. +:renderHiddenInMenu: (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered (the current documentNode is always included), defaults to ``false``. :calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false`` Example:: @@ -1107,7 +1107,7 @@ If no node variant exists for the preset combination, a ``NULL`` node will be in :dimension: (optional, string): name of the dimension which this menu should be based on. Example: "language". :presets: (optional, array): If set, the presets rendered will be taken from this list of preset identifiers :includeAllPresets: (boolean, default **false**) If TRUE, include all presets, not only allowed combinations -:renderHiddenInIndex: (boolean, default **true**) If TRUE, render nodes which are marked as "hidded-in-index" +:renderHiddenInMenu: (boolean, default **true**) If TRUE, render nodes which are marked as "hidded-in-menu" :calculateItemStates: (boolean) activate the *expensive* calculation of item states defaults to ``false`` Each ``item`` has the following properties: diff --git a/Neos.Neos/Documentation/References/NodeTypeDefinition.rst b/Neos.Neos/Documentation/References/NodeTypeDefinition.rst index a28157ef5d7..0af58d7631d 100644 --- a/Neos.Neos/Documentation/References/NodeTypeDefinition.rst +++ b/Neos.Neos/Documentation/References/NodeTypeDefinition.rst @@ -93,7 +93,7 @@ The following options are allowed for defining a NodeType: ``generatorClass`` Alternatively the class of a node label generator implementing - ``Neos\ContentRepository\Domain\Model\NodeLabelGeneratorInterface`` can be specified as a nested option. + ``Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface`` can be specified as a nested option. ``options`` Options for third party-code, the Content-Repository ignores those options but Neos or Packages may use this to adjust diff --git a/Neos.Neos/Documentation/References/Signals/Flow.rst b/Neos.Neos/Documentation/References/Signals/Flow.rst index bbb1c895b74..4e5fa123c49 100644 --- a/Neos.Neos/Documentation/References/Signals/Flow.rst +++ b/Neos.Neos/Documentation/References/Signals/Flow.rst @@ -3,7 +3,7 @@ Flow Signals Reference ====================== -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Flow Signals Reference: AbstractAdvice (``Neos\Flow\Aop\Advice\AbstractAdvice``)`: diff --git a/Neos.Neos/Documentation/References/Signals/Media.rst b/Neos.Neos/Documentation/References/Signals/Media.rst index 16047d16bba..6fe5901efa5 100644 --- a/Neos.Neos/Documentation/References/Signals/Media.rst +++ b/Neos.Neos/Documentation/References/Signals/Media.rst @@ -3,7 +3,7 @@ Media Signals Reference ======================= -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Media Signals Reference: AssetCollectionController (``Neos\Media\Browser\Controller\AssetCollectionController``)`: diff --git a/Neos.Neos/Documentation/References/Signals/Neos.rst b/Neos.Neos/Documentation/References/Signals/Neos.rst index c77dfd5872a..aa058c3ccf6 100644 --- a/Neos.Neos/Documentation/References/Signals/Neos.rst +++ b/Neos.Neos/Documentation/References/Signals/Neos.rst @@ -3,7 +3,7 @@ Neos Signals Reference ====================== -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Neos Signals Reference: AbstractModuleController (``Neos\Neos\Controller\Module\AbstractModuleController``)`: diff --git a/Neos.Neos/Documentation/References/Validators/Flow.rst b/Neos.Neos/Documentation/References/Validators/Flow.rst index d6b8a6efac7..8f9a200fb41 100644 --- a/Neos.Neos/Documentation/References/Validators/Flow.rst +++ b/Neos.Neos/Documentation/References/Validators/Flow.rst @@ -3,7 +3,7 @@ Flow Validator Reference ======================== -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Flow Validator Reference: AggregateBoundaryValidator`: diff --git a/Neos.Neos/Documentation/References/Validators/Media.rst b/Neos.Neos/Documentation/References/Validators/Media.rst index 6fac21f0bde..ceef5c00666 100644 --- a/Neos.Neos/Documentation/References/Validators/Media.rst +++ b/Neos.Neos/Documentation/References/Validators/Media.rst @@ -3,7 +3,7 @@ Media Validator Reference ========================= -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Media Validator Reference: ImageOrientationValidator`: diff --git a/Neos.Neos/Documentation/References/Validators/Party.rst b/Neos.Neos/Documentation/References/Validators/Party.rst index 5a17caaa024..063fb4adebd 100644 --- a/Neos.Neos/Documentation/References/Validators/Party.rst +++ b/Neos.Neos/Documentation/References/Validators/Party.rst @@ -3,7 +3,7 @@ Party Validator Reference ========================= -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Party Validator Reference: AimAddressValidator`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst index fbbccbbc411..291f60020d1 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/FluidAdaptor.rst @@ -3,7 +3,7 @@ FluidAdaptor ViewHelper Reference ################################# -This reference was automatically generated from code on 2024-04-20 +This reference was automatically generated from code on 2024-05-14 .. _`FluidAdaptor ViewHelper Reference: f:debug`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst index c89cc375d34..c691f540bb9 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Form.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Form.rst @@ -3,7 +3,7 @@ Form ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-04-20 +This reference was automatically generated from code on 2024-05-14 .. _`Form ViewHelper Reference: neos.form:form`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Fusion.rst b/Neos.Neos/Documentation/References/ViewHelpers/Fusion.rst index 9fc00a35312..548020a520d 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Fusion.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Fusion.rst @@ -3,7 +3,7 @@ Fusion ViewHelper Reference ########################### -This reference was automatically generated from code on 2024-04-19 +This reference was automatically generated from code on 2024-05-14 .. _`Fusion ViewHelper Reference: fusion:render`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst index 2585b8ae34d..15f0ec2cee4 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Media.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Media.rst @@ -3,7 +3,7 @@ Media ViewHelper Reference ########################## -This reference was automatically generated from code on 2024-04-20 +This reference was automatically generated from code on 2024-05-14 .. _`Media ViewHelper Reference: neos.media:fileTypeIcon`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst index feed90c2fba..0809cd7250a 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/Neos.rst @@ -3,7 +3,7 @@ Neos ViewHelper Reference ######################### -This reference was automatically generated from code on 2024-04-20 +This reference was automatically generated from code on 2024-05-14 .. _`Neos ViewHelper Reference: neos:backend.authenticationProviderLabel`: diff --git a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst index 169589fd0b3..400adbd9f86 100644 --- a/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst +++ b/Neos.Neos/Documentation/References/ViewHelpers/TYPO3Fluid.rst @@ -3,7 +3,7 @@ TYPO3 Fluid ViewHelper Reference ################################ -This reference was automatically generated from code on 2024-04-20 +This reference was automatically generated from code on 2024-05-14 .. _`TYPO3 Fluid ViewHelper Reference: f:alias`: diff --git a/Neos.Neos/Migrations/Code/Version20230801154834.php b/Neos.Neos/Migrations/Code/Version20230801154834.php index 68118a7648c..f31c5610840 100644 --- a/Neos.Neos/Migrations/Code/Version20230801154834.php +++ b/Neos.Neos/Migrations/Code/Version20230801154834.php @@ -4,7 +4,7 @@ /** - * Replace defaultUriSuffix configuration with uriPathSuffix in default site configuration + * Replace defaultUriSuffix configuration with uriPathSuffix in default site preset configuration */ class Version20230801154834 extends AbstractMigration { @@ -16,6 +16,6 @@ public function getIdentifier(): string public function up(): void { - $this->moveSettingsPaths(['Neos', 'Flow', 'mvc', 'routes', 'Neos.Neos', 'variables', 'defaultUriSuffix'], ['Neos', 'Neos', 'sites', '*', 'uriPathSuffix']); + $this->moveSettingsPaths(['Neos', 'Flow', 'mvc', 'routes', 'Neos.Neos', 'variables', 'defaultUriSuffix'], ['Neos', 'Neos', 'sitePresets', 'default', 'uriPathSuffix']); } } diff --git a/Neos.Neos/NodeTypes/Mixin/Document.yaml b/Neos.Neos/NodeTypes/Mixin/Document.yaml index 6fdfbaf2eda..c3277bf4344 100644 --- a/Neos.Neos/NodeTypes/Mixin/Document.yaml +++ b/Neos.Neos/NodeTypes/Mixin/Document.yaml @@ -59,7 +59,7 @@ _hidden: ui: reloadPageIfChanged: true - _hiddenInIndex: + hiddenInMenu: type: boolean ui: label: i18n diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion index 0da25707ac1..2c0be2b2649 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenu.fusion @@ -11,8 +11,8 @@ prototype(Neos.Neos:BreadcrumbMenu) < prototype(Neos.Fusion:Component) { # (integer) Restrict the maximum depth of items in the menu, defaults to ``0`` maximumLevels = 0 - # (boolean) Whether nodes with ``hiddenInIndex`` should be rendered (the current documentNode is always included), defaults to ``false``. - renderHiddenInIndex = true + # (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered (the current documentNode is always included), defaults to ``false``. + renderHiddenInMenu = true # (boolean) activate the *expensive* calculation of item states defaults to ``false`` calculateItemStates = false @@ -21,7 +21,7 @@ prototype(Neos.Neos:BreadcrumbMenu) < prototype(Neos.Fusion:Component) { items = Neos.Neos:BreadcrumbMenuItems { node = ${props.node} maximumLevels = ${props.maximumLevels} - renderHiddenInIndex = ${props.renderHiddenInIndex} + renderHiddenInMenu = ${props.renderHiddenInMenu} calculateItemStates = ${props.calculateItemStates} } } diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion index 8b128ba4eb8..1fe593d93d5 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/BreadcrumbMenuItems.fusion @@ -6,8 +6,8 @@ prototype(Neos.Neos:BreadcrumbMenuItems) < prototype(Neos.Fusion:Component) { # (integer) Restrict the maximum depth of items in the menu, defaults to ``0`` maximumLevels = 0 - # (boolean) Whether nodes with ``hiddenInIndex`` should be rendered (the current documentNode is always included), defaults to ``false``. - renderHiddenInIndex = true + # (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered (the current documentNode is always included), defaults to ``false``. + renderHiddenInMenu = true # (boolean) activate the *expensive* calculation of item states defaults to ``false`` calculateItemStates = false @@ -16,7 +16,7 @@ prototype(Neos.Neos:BreadcrumbMenuItems) < prototype(Neos.Fusion:Component) { parentItems = Neos.Neos:MenuItems { node = ${props.node} calculateItemStates = ${props.calculateItemStates} - renderHiddenInIndex = ${props.renderHiddenInIndex} + renderHiddenInMenu = ${props.renderHiddenInMenu} maximumLevels = ${props.maximumLevels} itemCollection = ${Array.reverse(q(documentNode).parents('[instanceof Neos.Neos:Document]').get())} } @@ -24,7 +24,7 @@ prototype(Neos.Neos:BreadcrumbMenuItems) < prototype(Neos.Fusion:Component) { currentItem = Neos.Neos:MenuItems { node = ${props.node} calculateItemStates = ${props.calculateItemStates} - renderHiddenInIndex = true + renderHiddenInMenu = true maximumLevels = ${props.maximumLevels} itemCollection = ${[documentNode]} } diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/ContentCase.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/ContentCase.fusion index fde834c17cc..951e396dfa6 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/ContentCase.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/ContentCase.fusion @@ -6,7 +6,6 @@ prototype(Neos.Neos:ContentCase) < prototype(Neos.Fusion:Case) { default { @position = 'end' condition = true - # this eel helper also handles the Neos.Neos:FallbackNode node type mechanism - type = ${Neos.Node.getNodeType(node).name.value} + type = ${Neos.Node.isNodeTypeExistent(node) ? q(node).nodeTypeName() : 'Neos.Neos:FallbackNode'} } } diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion index d5a75fb9ff6..599746ba283 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenu.fusion @@ -6,8 +6,8 @@ prototype(Neos.Neos:DimensionsMenu) < prototype(Neos.Fusion:Component) { # html attributes for the rendered list attributes = Neos.Fusion:DataStructure - # (boolean, default **true**) If TRUE, render nodes which are marked as "hidded-in-index" - renderHiddenInIndex = true + # (boolean, default **true**) Whether nodes with the property ``hiddenInMenu`` should be rendered + renderHiddenInMenu = true # (optional, string): name of the dimension which this menu should be based on. Example: "language". dimension = null @@ -24,7 +24,7 @@ prototype(Neos.Neos:DimensionsMenu) < prototype(Neos.Fusion:Component) { @private { items = Neos.Neos:DimensionsMenuItems { node = ${props.node} - renderHiddenInIndex = ${props.renderHiddenInIndex} + renderHiddenInMenu = ${props.renderHiddenInMenu} dimension = ${props.dimension} presets = ${props.presets} includeAllPresets = ${props.includeAllPresets} diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenuItems.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenuItems.fusion index 55629907ada..d45f3d09083 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenuItems.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/DimensionsMenuItems.fusion @@ -4,8 +4,8 @@ prototype(Neos.Neos:DimensionsMenuItems) { # (Node) The current node. Defaults to ``node`` from the fusion context node = ${documentNode} - # (boolean, default **true**) If TRUE, render nodes which are marked as "hidded-in-index" - renderHiddenInIndex = true + # (boolean, default **true**) Whether nodes with the property ``hiddenInMenu`` should be rendered + renderHiddenInMenu = true # (optional, string): name of the dimension which this menu should be based on. Example: "language". dimension = null diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion index c435eb45e3c..4c0e00360da 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/Menu.fusion @@ -22,8 +22,8 @@ prototype(Neos.Neos:Menu) < prototype(Neos.Fusion:Component) { # (string) Filter items by node type (e.g. ``'!My.Site:News,Neos.Neos:Document'``), defaults to ``'Neos.Neos:Document'``. The filter is only used for fetching subItems and is ignored for determining the ``startingPoint`` filter = 'Neos.Neos:Document' - # (boolean) Whether nodes with ``hiddenInIndex`` should be rendered, defaults to ``false`` - renderHiddenInIndex = false + # (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered, defaults to ``false`` + renderHiddenInMenu = false # (boolean) activate the *expensive* calculation of item states defaults to ``false``. calculateItemStates = false @@ -40,7 +40,7 @@ prototype(Neos.Neos:Menu) < prototype(Neos.Fusion:Component) { maximumLevels = ${props.maximumLevels} startingPoint = ${props.startingPoint} filter = ${props.filter} - renderHiddenInIndex = ${props.renderHiddenInIndex} + renderHiddenInMenu = ${props.renderHiddenInMenu} calculateItemStates = ${props.calculateItemStates} itemCollection = ${props.itemCollection} } diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion index edf6bc37c60..c11b9762844 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/MenuItems.fusion @@ -19,8 +19,8 @@ prototype(Neos.Neos:MenuItems) { # (string) Filter items by node type (e.g. ``'!My.Site:News,Neos.Neos:Document'``), defaults to ``'Neos.Neos:Document'``. The filter is only used for fetching subItems and is ignored for determining the ``startingPoint`` filter = 'Neos.Neos:Document' - # (boolean) Whether nodes with ``hiddenInIndex`` should be rendered, defaults to ``false`` - renderHiddenInIndex = false + # (boolean) Whether nodes with the property ``hiddenInMenu`` should be rendered, defaults to ``false`` + renderHiddenInMenu = false # (boolean) activate the *expensive* calculation of item states defaults to ``false``. calculateItemStates = false diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/NodeLink.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/NodeLink.fusion index d64b2cccef0..c064f1131f5 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/NodeLink.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/NodeLink.fusion @@ -33,5 +33,5 @@ prototype(Neos.Neos:NodeLink) < prototype(Neos.Fusion:Tag) { } } - content = ${node.label} + content = ${q(node).label()} } diff --git a/Neos.Neos/Resources/Private/Fusion/RawContent/NodeHeader.fusion b/Neos.Neos/Resources/Private/Fusion/RawContent/NodeHeader.fusion index aa3b1b01d9e..741e0b095f1 100644 --- a/Neos.Neos/Resources/Private/Fusion/RawContent/NodeHeader.fusion +++ b/Neos.Neos/Resources/Private/Fusion/RawContent/NodeHeader.fusion @@ -11,7 +11,7 @@ prototype(Neos.Neos:RawContent.NodeHeader) < prototype(Neos.Fusion:Component) { renderer = afx`
  - {(node.tethered || q(node).is('[instanceof Neos.Neos:Document]')) ? node.label : Translation.translate(props.labelParts[2], node.nodeType.label, [], props.labelParts[1], props.labelParts[0])} + {(node.tethered || q(node).is('[instanceof Neos.Neos:Document]')) ? q(node).label() : Translation.translate(props.labelParts[2], node.nodeType.label, [], props.labelParts[1], props.labelParts[0])} - {props.visibilityInformations}
` diff --git a/Neos.Neos/Resources/Private/Partials/Module/Shared/EditAccount.html b/Neos.Neos/Resources/Private/Partials/Module/Shared/EditAccount.html index 2c3c9c0a45e..2c690c78077 100644 --- a/Neos.Neos/Resources/Private/Partials/Module/Shared/EditAccount.html +++ b/Neos.Neos/Resources/Private/Partials/Module/Shared/EditAccount.html @@ -13,14 +13,14 @@
- +
- +
diff --git a/Neos.Neos/Resources/Private/Styles/_Tree.scss b/Neos.Neos/Resources/Private/Styles/_Tree.scss index bdf01001691..ee25d121a8b 100644 --- a/Neos.Neos/Resources/Private/Styles/_Tree.scss +++ b/Neos.Neos/Resources/Private/Styles/_Tree.scss @@ -54,6 +54,7 @@ ul.neos-tree-container { } } + /* todo legacy, should now be named hiddenInMenu */ &.neos-hiddenInIndex { span + span { opacity: 0.5; diff --git a/Neos.Neos/Resources/Private/Templates/Module/Administration/Users/New.html b/Neos.Neos/Resources/Private/Templates/Module/Administration/Users/New.html index b0dfdf9e473..f7572a1977b 100644 --- a/Neos.Neos/Resources/Private/Templates/Module/Administration/Users/New.html +++ b/Neos.Neos/Resources/Private/Templates/Module/Administration/Users/New.html @@ -14,7 +14,7 @@

{neos:backend.translate(id: 'user.new.subtitle', value: 'Create a new user',
- +
@@ -22,14 +22,14 @@

{neos:backend.translate(id: 'user.new.subtitle', value: 'Create a new user',
- +
- +
diff --git a/Neos.Neos/Resources/Private/Translations/ar/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/ar/NodeTypes/Document.xlf index 74a8a479afe..4c493915247 100644 --- a/Neos.Neos/Resources/Private/Translations/ar/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/ar/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment جزء من مسار URL - - Hide in menus - إخفاء في القوائم + + Hide in menus + إخفاء في القوائم diff --git a/Neos.Neos/Resources/Private/Translations/cs/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/cs/NodeTypes/Document.xlf index 4c0c4b4d23e..c05f25c505a 100644 --- a/Neos.Neos/Resources/Private/Translations/cs/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/cs/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment Segment cesty adresy URL - - Hide in menus - Skrýt v menu + + Hide in menus + Skrýt v menu diff --git a/Neos.Neos/Resources/Private/Translations/da/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/da/NodeTypes/Document.xlf index 88eb66041b2..21977d7fe28 100644 --- a/Neos.Neos/Resources/Private/Translations/da/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/da/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL adressesegment - - Hide in menus - Skjul i menuer + + Hide in menus + Skjul i menuer diff --git a/Neos.Neos/Resources/Private/Translations/de/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/de/NodeTypes/Document.xlf index 33f56f2d736..39d815b8867 100644 --- a/Neos.Neos/Resources/Private/Translations/de/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/de/NodeTypes/Document.xlf @@ -11,7 +11,7 @@ URL path segment URL Pfadsegment - + Hide in menus In Menüs verbergen diff --git a/Neos.Neos/Resources/Private/Translations/el/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/el/NodeTypes/Document.xlf index f84c70cb9e9..01ae4993918 100644 --- a/Neos.Neos/Resources/Private/Translations/el/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/el/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL path segment - - Hide in menus - Κρυφό στα μενού + + Hide in menus + Κρυφό στα μενού diff --git a/Neos.Neos/Resources/Private/Translations/en/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/en/NodeTypes/Document.xlf index 00d11fc2e6e..8469025151e 100644 --- a/Neos.Neos/Resources/Private/Translations/en/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/en/NodeTypes/Document.xlf @@ -11,7 +11,7 @@ URL path segment - + Hide in menus diff --git a/Neos.Neos/Resources/Private/Translations/es/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/es/NodeTypes/Document.xlf index 6e5c98088c3..d2ec8a13d3a 100644 --- a/Neos.Neos/Resources/Private/Translations/es/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/es/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment Segmento de ruta de URL - - Hide in menus - Ocultar en los menús + + Hide in menus + Ocultar en los menús diff --git a/Neos.Neos/Resources/Private/Translations/fi/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/fi/NodeTypes/Document.xlf index 37391b8c509..84e328e588f 100644 --- a/Neos.Neos/Resources/Private/Translations/fi/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/fi/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL-polun osa - - Hide in menus - Piilota valikoissa + + Hide in menus + Piilota valikoissa diff --git a/Neos.Neos/Resources/Private/Translations/fr/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/fr/NodeTypes/Document.xlf index 1b983a0be8d..025219dae0d 100644 --- a/Neos.Neos/Resources/Private/Translations/fr/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/fr/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment Segment d'URL - - Hide in menus - Cacher dans les menus + + Hide in menus + Cacher dans les menus diff --git a/Neos.Neos/Resources/Private/Translations/hu/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/hu/NodeTypes/Document.xlf index 5b45270dfd8..01f8e1ed7f0 100644 --- a/Neos.Neos/Resources/Private/Translations/hu/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/hu/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL szegmens - - Hide in menus - Elrejtés a menüben + + Hide in menus + Elrejtés a menüben diff --git a/Neos.Neos/Resources/Private/Translations/id_ID/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/id_ID/NodeTypes/Document.xlf index 339798d9413..b11c6c53c5d 100644 --- a/Neos.Neos/Resources/Private/Translations/id_ID/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/id_ID/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment Segmen path URL - - Hide in menus - Sembunyikan di Menu + + Hide in menus + Sembunyikan di Menu diff --git a/Neos.Neos/Resources/Private/Translations/it/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/it/NodeTypes/Document.xlf index 4887f24b0f0..72c877f40c5 100644 --- a/Neos.Neos/Resources/Private/Translations/it/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/it/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment Segmento di percorso URL - - Hide in menus - Nascondi nei menu + + Hide in menus + Nascondi nei menu diff --git a/Neos.Neos/Resources/Private/Translations/ja/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/ja/NodeTypes/Document.xlf index cb9aaef87e7..bc1b54c0219 100644 --- a/Neos.Neos/Resources/Private/Translations/ja/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/ja/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL パスセグメント - - Hide in menus - メニュー非表示 + + Hide in menus + メニュー非表示 diff --git a/Neos.Neos/Resources/Private/Translations/km/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/km/NodeTypes/Document.xlf index cbf91093ce3..86eacfb29c4 100644 --- a/Neos.Neos/Resources/Private/Translations/km/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/km/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment បំណែកផ្លូវនៃតំណ - - Hide in menus - មិនបង្ហាុញក្នុងមីនុយ + + Hide in menus + មិនបង្ហាុញក្នុងមីនុយ diff --git a/Neos.Neos/Resources/Private/Translations/lv/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/lv/NodeTypes/Document.xlf index 1633085c3fa..c8b29397511 100644 --- a/Neos.Neos/Resources/Private/Translations/lv/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/lv/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL segments - - Hide in menus - Paslēpt izvēlnē + + Hide in menus + Paslēpt izvēlnē diff --git a/Neos.Neos/Resources/Private/Translations/nl/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/nl/NodeTypes/Document.xlf index 6be62d17124..ea6b753d8e5 100644 --- a/Neos.Neos/Resources/Private/Translations/nl/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/nl/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL pad segment - - Hide in menus - Verberg in menu's + + Hide in menus + Verberg in menu's diff --git a/Neos.Neos/Resources/Private/Translations/no/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/no/NodeTypes/Document.xlf index 8205579ac5c..07e25137155 100644 --- a/Neos.Neos/Resources/Private/Translations/no/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/no/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment URL-banesegment - - Hide in menus - Skjul i menyer + + Hide in menus + Skjul i menyer diff --git a/Neos.Neos/Resources/Private/Translations/pl/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/pl/NodeTypes/Document.xlf index fc17daa0889..221041f7295 100644 --- a/Neos.Neos/Resources/Private/Translations/pl/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/pl/NodeTypes/Document.xlf @@ -14,9 +14,9 @@ URL path segment Segment ścieżki URL - - Hide in menus - Ukryj w menu + + Hide in menus + Ukryj w menu diff --git a/Neos.Neos/Resources/Private/Translations/pt/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/pt/NodeTypes/Document.xlf index 310986d4a13..c69b2e09a2f 100644 --- a/Neos.Neos/Resources/Private/Translations/pt/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/pt/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment Segmento de caminho de URL - + Hide in menus Ocultar em menus diff --git a/Neos.Neos/Resources/Private/Translations/pt_BR/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/pt_BR/NodeTypes/Document.xlf index af3854dfa6f..3d6166a6427 100644 --- a/Neos.Neos/Resources/Private/Translations/pt_BR/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/pt_BR/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment Segmento de URL - + Hide in menus Esconder em menus diff --git a/Neos.Neos/Resources/Private/Translations/ru/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/ru/NodeTypes/Document.xlf index d8e771abbe4..b3b476c4c8f 100644 --- a/Neos.Neos/Resources/Private/Translations/ru/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/ru/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment Сегмент пути URL-адреса - + Hide in menus Скрыть в меню diff --git a/Neos.Neos/Resources/Private/Translations/sr/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/sr/NodeTypes/Document.xlf index 4561f58bf69..6d0fac4e492 100644 --- a/Neos.Neos/Resources/Private/Translations/sr/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/sr/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment Сегмент URL путање - + Hide in menus Сакривено у менију diff --git a/Neos.Neos/Resources/Private/Translations/sv/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/sv/NodeTypes/Document.xlf index 8c710d59a77..65b1c3d9085 100644 --- a/Neos.Neos/Resources/Private/Translations/sv/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/sv/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment URL-sökvägssegment - + Hide in menus Dölj i menyer diff --git a/Neos.Neos/Resources/Private/Translations/tl_PH/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/tl_PH/NodeTypes/Document.xlf index 3b426ba0275..a552fa23dad 100644 --- a/Neos.Neos/Resources/Private/Translations/tl_PH/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/tl_PH/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment URL path segment - + Hide in menus Hide in menus diff --git a/Neos.Neos/Resources/Private/Translations/tr/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/tr/NodeTypes/Document.xlf index 10be0efa791..4f72b7b545d 100644 --- a/Neos.Neos/Resources/Private/Translations/tr/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/tr/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment URL yolu segmenti - + Hide in menus Menülerde gizle diff --git a/Neos.Neos/Resources/Private/Translations/uk/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/uk/NodeTypes/Document.xlf index a2918f802ac..e056f2084b8 100644 --- a/Neos.Neos/Resources/Private/Translations/uk/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/uk/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment Сегмент адреси URL - + Hide in menus Сховати до меню diff --git a/Neos.Neos/Resources/Private/Translations/vi/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/vi/NodeTypes/Document.xlf index 4700ec83d27..deb1ba9a6af 100644 --- a/Neos.Neos/Resources/Private/Translations/vi/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/vi/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment Phân đoạn đường dẫn URL - + Hide in menus Ẩn trong menu diff --git a/Neos.Neos/Resources/Private/Translations/zh/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/zh/NodeTypes/Document.xlf index debc5db04b4..582aea1487d 100644 --- a/Neos.Neos/Resources/Private/Translations/zh/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/zh/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment URL 路径段 - + Hide in menus 在菜单中隐藏 diff --git a/Neos.Neos/Resources/Private/Translations/zh_TW/NodeTypes/Document.xlf b/Neos.Neos/Resources/Private/Translations/zh_TW/NodeTypes/Document.xlf index 42ff61d4db0..b4a2f55d143 100644 --- a/Neos.Neos/Resources/Private/Translations/zh_TW/NodeTypes/Document.xlf +++ b/Neos.Neos/Resources/Private/Translations/zh_TW/NodeTypes/Document.xlf @@ -14,7 +14,7 @@ URL path segment 網址路徑 - + Hide in menus 於選單中隱藏 diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetTrait.php index 8998c61678c..ed9dfa5aa43 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetTrait.php @@ -20,8 +20,8 @@ /** * Step implementations for tests inside Neos.Neos * -* @internal only for behat tests within the Neos.Neos package -*/ + * @internal only for behat tests within the Neos.Neos package + */ trait AssetTrait { /** @@ -46,4 +46,22 @@ public function anAssetExistsWithId(string $assetId): void $this->getObject(AssetRepository::class)->add($asset); $this->getObject(PersistenceManagerInterface::class)->persistAll(); } + + /** + * @Given the asset :assetId has the title :title + * @Given the asset :assetId has the title :title and caption :caption + * @Given the asset :assetId has the title :title and caption :caption and copyright notice :copyrightNotice + */ + public function theAssetHasTheTitleAndCaptionAndCopyrightNotice($assetId, $title, $caption = null, $copyrightNotice = null): void + { + $repository = $this->getObject(AssetRepository::class); + $asset = $repository->findByIdentifier($assetId); + + $asset->setTitle($title); + $caption && $asset->setCaption($caption); + $copyrightNotice && $asset->setCopyrightNotice($copyrightNotice); + + $repository->update($asset); + $this->getObject(PersistenceManagerInterface::class)->persistAll(); + } } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/BrowserTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/BrowserTrait.php index 030bde9ea19..a5454d0352e 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/BrowserTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/BrowserTrait.php @@ -113,7 +113,7 @@ public function getCurrentNodeAddresses(): array */ public function iGetTheNodeAddressForNodeAggregate(string $rawNodeAggregateId, $alias = 'DEFAULT') { - $subgraph = $this->currentContentRepository->getContentGraph()->getSubgraph($this->currentContentStreamId, $this->currentDimensionSpacePoint, $this->currentVisibilityConstraints); + $subgraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->getSubgraph($this->currentDimensionSpacePoint, $this->currentVisibilityConstraints); $nodeAggregateId = NodeAggregateId::fromString($rawNodeAggregateId); $node = $subgraph->findNodeById($nodeAggregateId); Assert::assertNotNull($node, 'Did not find a node with aggregate id "' . $nodeAggregateId->value . '"'); @@ -122,9 +122,7 @@ public function iGetTheNodeAddressForNodeAggregate(string $rawNodeAggregateId, $ $this->currentContentStreamId, $this->currentDimensionSpacePoint, $nodeAggregateId, - $this->currentContentRepository->getWorkspaceFinder() - ->findOneByCurrentContentStreamId($this->currentContentStreamId) - ->workspaceName + $this->currentWorkspaceName, ); } @@ -136,7 +134,7 @@ public function iGetTheNodeAddressForNodeAggregate(string $rawNodeAggregateId, $ */ public function iGetTheNodeAddressForTheNodeAtPath(string $serializedNodePath, $alias = 'DEFAULT') { - $subgraph = $this->currentContentRepository->getContentGraph()->getSubgraph($this->currentContentStreamId, $this->currentDimensionSpacePoint, $this->currentVisibilityConstraints); + $subgraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->getSubgraph($this->currentDimensionSpacePoint, $this->currentVisibilityConstraints); if (!$this->getRootNodeAggregateId()) { throw new \Exception('ERROR: rootNodeAggregateId needed for running this step. You need to use "the event RootNodeAggregateWithNodeWasCreated was published with payload" to create a root node..'); } @@ -147,9 +145,7 @@ public function iGetTheNodeAddressForTheNodeAtPath(string $serializedNodePath, $ $this->currentContentStreamId, $this->currentDimensionSpacePoint, $node->nodeAggregateId, - $this->currentContentRepository->getWorkspaceFinder() - ->findOneByCurrentContentStreamId($this->currentContentStreamId) - ->workspaceName + $this->currentWorkspaceName, ); } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentCacheTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentCacheTrait.php new file mode 100644 index 00000000000..193accfbf5b --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentCacheTrait.php @@ -0,0 +1,39 @@ + $className + * + * @return T + */ + abstract private function getObject(string $className): object; + + + /** + * @Given the ContentCacheFlusher flushes all collected tags + */ + public function theContentCacheFlusherFlushesAllCollectedTags(): void + { + $this->getObject(ContentCacheFlusher::class)->flushCollectedTags(); + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 340bff03d86..631f0469d7f 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -24,6 +24,8 @@ use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\MigrationsTrait; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Utility\Environment; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; +use Neos\Flow\Persistence\PersistenceManagerInterface; class FeatureContext implements BehatContext { @@ -31,23 +33,28 @@ class FeatureContext implements BehatContext use FlowEntitiesTrait; use BrowserTrait; - use CRTestSuiteTrait; + use CRTestSuiteTrait { + deserializeProperties as deserializePropertiesCrTestSuiteTrait; + } use CRBehavioralTestsSubjectProvider; use RoutingTrait; use MigrationsTrait; use FusionTrait; + use ContentCacheTrait; use AssetTrait; protected Environment $environment; protected ContentRepositoryRegistry $contentRepositoryRegistry; + protected PersistenceManagerInterface $persistenceManager; public function __construct() { self::bootstrapFlow(); $this->environment = $this->getObject(Environment::class); $this->contentRepositoryRegistry = $this->getObject(ContentRepositoryRegistry::class); + $this->persistenceManager = $this->getObject(PersistenceManagerInterface::class); $this->setupCRTestSuiteTrait(); } @@ -102,4 +109,28 @@ protected function createContentRepository( return $contentRepository; } + + protected function deserializeProperties(array $properties): PropertyValuesToWrite + { + $properties = array_map( + $this->loadObjectsRecursive(...), + $properties + ); + + return $this->deserializePropertiesCrTestSuiteTrait($properties); + } + + private function loadObjectsRecursive(mixed $value): mixed + { + if (is_string($value) && str_starts_with($value, 'Asset:')) { + $assetIdentier = substr($value, strlen('Asset:')); + return $this->persistenceManager->getObjectByIdentifier($assetIdentier, 'Neos\\Media\\Domain\\Model\\Asset', true); + } elseif (is_array($value)) { + return array_map( + $this->loadObjectsRecursive(...), + $value + ); + } + return $value; + } } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FusionTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FusionTrait.php index 21b8f5ba725..b1ebbbd35a5 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FusionTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FusionTrait.php @@ -28,6 +28,7 @@ use Neos\Neos\Domain\Service\RenderingModeService; use PHPUnit\Framework\Assert; use Psr\Http\Message\ServerRequestFactoryInterface; +use Neos\Fusion\Core\Cache\ContentCache; /** * @internal only for behat tests within the Neos.Neos package @@ -47,6 +48,8 @@ trait FusionTrait private ?\Throwable $lastRenderingException = null; + private $contentCacheEnabled = false; + /** * @template T of object * @param class-string $className @@ -63,6 +66,7 @@ public function setupFusionContext(): void $this->fusionGlobalContext = []; $this->fusionContext = []; $this->fusionCode = null; + $this->contentCacheEnabled = false; $this->renderingResult = null; } @@ -114,6 +118,14 @@ public function iHaveTheFollowingFusionSetup(PyStringNode $fusionCode): void $this->fusionCode = $fusionCode->getRaw(); } + /** + * @When I have Fusion content cache enabled + */ + public function iHaveFusionContentCacheEnabled(): void + { + $this->contentCacheEnabled = true; + } + /** * @When I execute the following Fusion code: * @When I execute the following Fusion code on path :path: @@ -131,6 +143,7 @@ public function iExecuteTheFollowingFusionCode(PyStringNode $fusionCode, string $fusionGlobals = FusionGlobals::fromArray($this->fusionGlobalContext); $fusionRuntime = (new RuntimeFactory())->createFromConfiguration($fusionAst, $fusionGlobals); + $fusionRuntime->setEnableContentCache($this->contentCacheEnabled); $fusionRuntime->overrideExceptionHandler($this->getObject(ThrowingHandler::class)); $fusionRuntime->pushContextArray($this->fusionContext); try { @@ -192,4 +205,12 @@ public function throwExceptionIfLastRenderingLedToAnError(): void } } + /** + * @BeforeScenario + */ + public function clearFusionCaches() + { + $this->getObject(ContentCache::class)->flush(); + } + } diff --git a/Neos.Neos/Tests/Behavior/Features/ContentCache/Assets.feature b/Neos.Neos/Tests/Behavior/Features/ContentCache/Assets.feature new file mode 100644 index 00000000000..2d4a8a34fd7 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentCache/Assets.feature @@ -0,0 +1,322 @@ +@flowEntities +Feature: Tests for the ContentCacheFlusher and cache flushing on asset changes + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + title: + type: string + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + 'Neos.Neos:Test.DocumentType1': + superTypes: + 'Neos.Neos:Document': true + properties: + asset: + type: Neos\Media\Domain\Model\Asset + assets: + type: array + 'Neos.Neos:Test.DocumentType2': + superTypes: + 'Neos.Neos:Document': true + properties: + text: + type: string + + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + + When an asset exists with id "an-asset-to-change" + And the asset "an-asset-to-change" has the title "First asset" and caption "This is an asset" and copyright notice "Copyright Neos 2024" + When an asset exists with id "some-other-asset" + And the asset "some-other-asset" has the title "Some other asset" and caption "This is some other asset" and copyright notice "Copyright Neos 2024" + And the ContentCacheFlusher flushes all collected tags + + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | a | root | Neos.Neos:Site | {} | site | + | a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1", "asset": "Asset:an-asset-to-change"} | a1 | + | a1-1 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1", "title": "Node a1-1", "assets": ["Asset:an-asset-to-change"]} | a1-1 | + | a1-2 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-2", "title": "Node a1-2", "asset": "Asset:some-other-asset"} | a1-2 | + | a2 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2", "text": "Link to asset://an-asset-to-change."} | a2 | + | a3 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2", "text": "Link to asset://some-other-asset."} | a3 | + When the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + And A site exists for node name "a" and domain "http://localhost" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + And the Fusion context node is "a1" + And the Fusion context request URI is "http://localhost" + And I have the following Fusion setup: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + prototype(Neos.Neos:Test.DocumentType1) < prototype(Neos.Fusion:Component) { + + cacheVerifier = ${null} + assetTitle = ${q(node).property("asset").title} + assetTitleOfArray = ${q(node).property("assets")[0].title} + + renderer = afx` + cacheVerifier={props.cacheVerifier}, + assetTitle={props.assetTitle}, + assetTitleOfArray={props.assetTitleOfArray} + ` + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(node)} + } + entryTags { + 1 = ${Neos.Caching.nodeTag(node)} + 2 = ${Neos.Caching.descendantOfTag(node)} + } + } + } + + prototype(Neos.Neos:Test.DocumentType2) < prototype(Neos.Fusion:Component) { + + cacheVerifier = ${null} + text = ${q(node).property('text')} + + renderer = afx` + cacheVerifier={props.cacheVerifier}, + text={props.text} + ` + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(node)} + } + entryTags { + 1 = ${Neos.Caching.nodeTag(node)} + 2 = ${Neos.Caching.descendantOfTag(node)} + } + } + } + + """ + + Scenario: ContentCache gets flushed when an referenced asset in a property has changed + Given I have Fusion content cache enabled + And the Fusion context node is "a1" + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, assetTitle=First asset, assetTitleOfArray= + """ + + Then the asset "an-asset-to-change" has the title "First changed asset" + And the ContentCacheFlusher flushes all collected tags + + Then the Fusion context node is "a1" + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, assetTitle=First changed asset, assetTitleOfArray= + """ + + Scenario: ContentCache gets flushed when an referenced asset in a property array has changed + Given I have Fusion content cache enabled + And the Fusion context node is "a1-1" + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, assetTitle=, assetTitleOfArray=First asset + """ + + Then the asset "an-asset-to-change" has the title "First changed asset" + And the ContentCacheFlusher flushes all collected tags + + Then the Fusion context node is "a1-1" + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, assetTitle=, assetTitleOfArray=First changed asset + """ + + + Scenario: ContentCache doesn't get flushed when another asset than the referenced asset in a property has changed + Given I have Fusion content cache enabled + And the Fusion context node is "a1-2" + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, assetTitle=Some other asset, assetTitleOfArray= + """ + + Then the asset "an-asset-to-change" has the title "First changed asset" + And the ContentCacheFlusher flushes all collected tags + + Then the Fusion context node is "a1-2" + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, assetTitle=Some other asset, assetTitleOfArray= + """ + + Scenario: ContentCache gets flushed for live workspace when a referenced asset in a property text has changed + Given I have Fusion content cache enabled + And the Fusion context node is a2 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, text=Link to asset://an-asset-to-change. + """ + + Then the asset "an-asset-to-change" has the title "First changed asset" + And the ContentCacheFlusher flushes all collected tags + + Then the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, text=Link to asset://an-asset-to-change. + """ + + Scenario: ContentCache gets flushed for user workspace when a referenced asset in a property text has changed + Given I have Fusion content cache enabled + And I am in workspace "user-test" and dimension space point {} + And the Fusion context node is a2 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, text=Link to asset://an-asset-to-change. + """ + + And I am in workspace "live" and dimension space point {} + Then the asset "an-asset-to-change" has the title "First changed asset" + And the ContentCacheFlusher flushes all collected tags + + And I am in workspace "user-test" and dimension space point {} + Then the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, text=Link to asset://an-asset-to-change. + """ + + Scenario: ContentCache doesn't get flushed when a non-referenced asset in a property text has changed + Given I have Fusion content cache enabled + And the Fusion context node is a3 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, text=Link to asset://some-other-asset. + """ + + Then the asset "an-asset-to-change" has the title "First changed asset" + And the ContentCacheFlusher flushes all collected tags + + Then the Fusion context node is a3 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, text=Link to asset://some-other-asset. + """ diff --git a/Neos.Neos/Tests/Behavior/Features/ContentCache/ConvertUris.feature b/Neos.Neos/Tests/Behavior/Features/ContentCache/ConvertUris.feature new file mode 100644 index 00000000000..fad01fcfe6c --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentCache/ConvertUris.feature @@ -0,0 +1,172 @@ +@flowEntities +Feature: Tests for the ContentCacheFlusher and cache flushing on DynamicNodeTag tags + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + title: + type: string + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + 'Neos.Neos:Test.DocumentType1': + superTypes: + 'Neos.Neos:Document': true + + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | a | root | Neos.Neos:Site | {} | a | + | a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1"} | a1 | + | a2 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a2", "title": "Node a2"} | a2 | + When the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + And A site exists for node name "a" and domain "http://localhost" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + And the Fusion context node is "a1" + And the Fusion context request URI is "http://localhost" + And the Fusion renderingMode is "frontend" + And I have the following Fusion setup: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + prototype(Neos.Neos:Test.DocumentType1) < prototype(Neos.Fusion:Component) { + + cacheVerifier = ${null} + title = ${q(node).property('title')} + link = Neos.Neos:ConvertUris { + value = ${"Some value with node URI: node://a1."} + } + + renderer = afx` + cacheVerifier={props.cacheVerifier}, + title={props.title}, + link={props.link} + ` + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(node)} + } + entryTags { + 1 = ${Neos.Caching.nodeTag(node)} + } + } + } + + """ + + + Scenario: ContentCache gets flushed when target node changes + Given I have Fusion content cache enabled + And the Fusion context node is a2 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a2, link=Some value with node URI: /a1. + """ + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | propertyValues | {"uriPathSegment": "a1-new"} | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + And the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, title=Node a2, link=Some value with node URI: /a1-new. + """ + + Scenario: ContentCache doesn't get flushed when target node changes in different workspace + Given I have Fusion content cache enabled + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a2 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a2, link=Some value with node URI: /a1. + """ + + And I am in workspace "user-test" and dimension space point {} + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | propertyValues | {"uriPathSegment": "a1-new"} | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a2, link=Some value with node URI: /a1. + """ diff --git a/Neos.Neos/Tests/Behavior/Features/ContentCache/Nodes.feature b/Neos.Neos/Tests/Behavior/Features/ContentCache/Nodes.feature new file mode 100644 index 00000000000..840166ccdf4 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentCache/Nodes.feature @@ -0,0 +1,246 @@ +@flowEntities +Feature: Tests for the ContentCacheFlusher and cache flushing on node and nodetype specific tags + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + title: + type: string + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + 'Neos.Neos:Test.DocumentType1': + superTypes: + 'Neos.Neos:Document': true + 'Neos.Neos:Test.DocumentType2': + superTypes: + 'Neos.Neos:Document': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | a | root | Neos.Neos:Site | {} | site | + | a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1"} | a1 | + | a1-1 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1", "title": "Node a1-1"} | a1-1 | + | a2 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2"} | a2 | + And A site exists for node name "a" and domain "http://localhost" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + And the Fusion context node is "a1" + And the Fusion context request URI is "http://localhost" + And I have the following Fusion setup: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + prototype(Neos.Neos:Test.DocumentType1) < prototype(Neos.Fusion:Component) { + + cacheVerifier = ${null} + title = ${q(node).property('title')} + + renderer = afx` + cacheVerifier={props.cacheVerifier}, + title={props.title} + ` + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(node)} + } + entryTags { + 1 = ${Neos.Caching.nodeTag(node)} + 2 = ${Neos.Caching.descendantOfTag(node)} + } + } + } + + prototype(Neos.Neos:Test.DocumentType2) < prototype(Neos.Fusion:Component) { + + cacheVerifier = ${null} + title = ${q(node).property('title')} + + renderer = afx` + cacheVerifier={props.cacheVerifier}, + title={props.title} + ` + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(node)} + } + entryTags { + 1 = ${Neos.Caching.nodeTag(node)} + 2 = ${Neos.Caching.nodeTypeTag('Neos.Neos:Document',node)} + } + } + } + + """ + + + Scenario: ContentCache gets flushed when a property of a node has changed + Given I have Fusion content cache enabled + And the Fusion context node is a1 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | propertyValues | {"title": "Node a1 new"} | + And the graph projection is fully up to date + + And the Fusion context node is a1 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, title=Node a1 new + """ + + Scenario: ContentCache gets not flushed when a property of another node has changed + Given I have Fusion content cache enabled + And the Fusion context node is a1 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a2" | + | propertyValues | {"title": "Node a2 new"} | + And the graph projection is fully up to date + + And the Fusion context node is a1 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + Scenario: ContentCache gets flushed when a property of a node has changed by NodeType name + Given I have Fusion content cache enabled + And the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a2 + """ + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | propertyValues | {"title": "Node a1 new"} | + And the graph projection is fully up to date + + And the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, title=Node a2 + """ + + Scenario: ContentCache gets flushed when a property of a node has changed of a descendant node + Given I have Fusion content cache enabled + And the Fusion context node is "a1" + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1-1" | + | propertyValues | {"title": "Node a1-1 new"} | + And the graph projection is fully up to date + + And the Fusion context node is "a1" + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=second execution, title=Node a1 + """ diff --git a/Neos.Neos/Tests/Behavior/Features/ContentCache/NodesInOtherWorkspace.feature b/Neos.Neos/Tests/Behavior/Features/ContentCache/NodesInOtherWorkspace.feature new file mode 100644 index 00000000000..183dc20ffcc --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/ContentCache/NodesInOtherWorkspace.feature @@ -0,0 +1,265 @@ +@flowEntities +Feature: Tests for the ContentCacheFlusher and cache flushing on node and nodetype specific tags not affecting different workspaces + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + title: + type: string + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + 'Neos.Neos:Test.DocumentType1': + superTypes: + 'Neos.Neos:Document': true + 'Neos.Neos:Test.DocumentType2': + superTypes: + 'Neos.Neos:Document': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | a | root | Neos.Neos:Site | {} | site | + | a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1"} | a1 | + | a1-1 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1", "title": "Node a1-1"} | a1-1 | + | a2 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2"} | a2 | + When the command RebaseWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + And A site exists for node name "a" and domain "http://localhost" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + And the Fusion context node is "a1" + And the Fusion context request URI is "http://localhost" + And I have the following Fusion setup: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + prototype(Neos.Neos:Test.DocumentType1) < prototype(Neos.Fusion:Component) { + + cacheVerifier = ${null} + title = ${q(node).property('title')} + + renderer = afx` + cacheVerifier={props.cacheVerifier}, + title={props.title} + ` + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(node)} + } + entryTags { + 1 = ${Neos.Caching.nodeTag(node)} + 2 = ${Neos.Caching.descendantOfTag(node)} + } + } + } + + prototype(Neos.Neos:Test.DocumentType2) < prototype(Neos.Fusion:Component) { + + cacheVerifier = ${null} + title = ${q(node).property('title')} + + renderer = afx` + cacheVerifier={props.cacheVerifier}, + title={props.title} + ` + + @cache { + mode = 'cached' + entryIdentifier { + documentNode = ${Neos.Caching.entryIdentifierForNode(node)} + } + entryTags { + 1 = ${Neos.Caching.nodeTag(node)} + 2 = ${Neos.Caching.nodeTypeTag('Neos.Neos:Document',node)} + } + } + } + + """ + + + Scenario: ContentCache doesn't get flushed when a property of a node in other workspace has changed + Given I have Fusion content cache enabled + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a1 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + And I am in workspace "user-test" and dimension space point {} + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | propertyValues | {"title": "Node a1 new"} | + And the graph projection is fully up to date + + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a1 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + Scenario: ContentCache gets not flushed when a property of another node has changed in different workspace + Given I have Fusion content cache enabled + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a1 + + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + And I am in workspace "user-test" and dimension space point {} + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a2" | + | propertyValues | {"title": "Node a2 new"} | + And the graph projection is fully up to date + + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a1 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + Scenario: ContentCache doesn't get flushed when a property of a node has changed by NodeType name in different workspace + Given I have Fusion content cache enabled + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a2 + """ + + And I am in workspace "user-test" and dimension space point {} + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | propertyValues | {"title": "Node a1 new"} | + And the graph projection is fully up to date + + And I am in workspace "live" and dimension space point {} + And the Fusion context node is a2 + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType2 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a2 + """ + + Scenario: ContentCache doesn't get flushed when a property of a node has changed of a descendant node in different workspace + Given I have Fusion content cache enabled + And I am in workspace "live" and dimension space point {} + And the Fusion context node is "a1" + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"first execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ + + And I am in workspace "user-test" and dimension space point {} + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1-1" | + | propertyValues | {"title": "Node a1-1 new"} | + And the graph projection is fully up to date + + And I am in workspace "live" and dimension space point {} + And the Fusion context node is "a1" + And I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.DocumentType1 { + cacheVerifier = ${"second execution"} + } + """ + Then I expect the following Fusion rendering result: + """ + cacheVerifier=first execution, title=Node a1 + """ diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature index 42fdfa0c5d4..7c360e6a7f9 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature @@ -31,7 +31,7 @@ Feature: Basic routing functionality (match & resolve document nodes in one dime | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -45,7 +45,7 @@ Feature: Basic routing functionality (match & resolve document nodes in one dime # earl-o-documentbourgh # nody-mc-nodeface # - And I am in content stream "cs-identifier" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | shernode-homes | lady-eleonode-rootford | Neos.Neos:Test.Routing.Page | {"uriPathSegment": "ignore-me"} | node1 | @@ -59,8 +59,9 @@ Feature: Basic routing functionality (match & resolve document nodes in one dime Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: 'default' + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Dimensions.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Dimensions.feature index 4e4fcc69784..6e40d5409db 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Dimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Dimensions.feature @@ -43,7 +43,7 @@ Feature: Routing functionality with multiple content dimensions | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {"market":"DE", "language":"en"} + And I am in workspace "live" and dimension space point {"market":"DE", "language":"en"} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -71,8 +71,9 @@ Feature: Routing functionality with multiple content dimensions Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: defaultDimensionSpacePoint: market: DE @@ -111,8 +112,9 @@ Feature: Routing functionality with multiple content dimensions Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: defaultDimensionSpacePoint: market: DE @@ -170,8 +172,9 @@ Feature: Routing functionality with multiple content dimensions Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory @@ -239,8 +242,9 @@ Feature: Routing functionality with multiple content dimensions Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory @@ -306,8 +310,9 @@ Feature: Routing functionality with multiple content dimensions Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory @@ -367,8 +372,9 @@ Feature: Routing functionality with multiple content dimensions Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature index 786be2fb737..7539f67e1af 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature @@ -30,7 +30,7 @@ Feature: Routing behavior of removed, disabled and re-enabled nodes | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -59,8 +59,9 @@ Feature: Routing behavior of removed, disabled and re-enabled nodes Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Lowlevel_ProjectionTests.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Lowlevel_ProjectionTests.feature index 2ca042c1632..54999dbed75 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Lowlevel_ProjectionTests.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Lowlevel_ProjectionTests.feature @@ -39,7 +39,7 @@ Feature: Low level tests covering the inner behavior of the routing projection | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -236,7 +236,7 @@ Feature: Low level tests covering the inner behavior of the routing projection | "c" | "lady-eleonode-rootford/shernode-homes/c" | "c" | "shernode-homes" | "b" | null | "Neos.Neos:Test.Routing.Page" | Scenario: ab(> b1, b2 > b2a)c => a(> b2 > b2a)b(> b1)c (moving b1 below a) - And I am in content stream "cs-identifier" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | b1 | b | Neos.Neos:Test.Routing.Page | {"uriPathSegment": "b1"} | b1 | @@ -261,7 +261,7 @@ Feature: Low level tests covering the inner behavior of the routing projection | "c" | "lady-eleonode-rootford/shernode-homes/c" | "c" | "shernode-homes" | "b" | null | "Neos.Neos:Test.Routing.Page" | Scenario: ab(> b1, b2 > b2a)c => b(> b1, a, b2 > b2a)c (moving a below b) - And I am in content stream "cs-identifier" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | b1 | b | Neos.Neos:Test.Routing.Page | {"uriPathSegment": "b1"} | b1 | diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/MultiSiteLinking.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/MultiSiteLinking.feature index b054eb1f25d..3d402e4ee54 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/MultiSiteLinking.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/MultiSiteLinking.feature @@ -39,7 +39,7 @@ Feature: Linking between multiple websites | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -65,8 +65,15 @@ Feature: Linking between multiple websites Neos: Neos: sites: - '*': - contentRepository: default + 'site-1': + preset: default + uriPathSuffix: '' + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + 'site-2': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/NodeCreationEdgeCases.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/NodeCreationEdgeCases.feature new file mode 100644 index 00000000000..fe34203f36f --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/NodeCreationEdgeCases.feature @@ -0,0 +1,84 @@ +@flowEntities @contentrepository +Feature: Test cases for node creation edge cases + + Scenario: Delete the succeeding sibling node in a virtual specialization and then create the node + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | source, spec, leafSpec | leafSpec -> spec -> source | + And using the following node types: + """yaml + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example":"source"} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "shernode-homes" | + | nodeTypeName | "Neos.Neos:Site" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | succeedingSiblingNodeAggregateId | nodeTypeName | initialPropertyValues | + # Let's prepare some siblings to check orderings. Also, everything gets better with siblings. + | elder-mc-nodeface | elder-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "elder"} | + | eldest-mc-nodeface | eldest-document | shernode-homes | elder-mc-nodeface | Neos.Neos:Document | {"uriPathSegment": "eldest"} | + | younger-mc-nodeface | younger-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "younger"} | + | youngest-mc-nodeface | youngest-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "youngest"} | + Given the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "younger-mc-nodeface" | + | coveredDimensionSpacePoint | {"example":"spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + When the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | succeedingSiblingNodeAggregateId | nodeTypeName | initialPropertyValues | + | nody-mc-nodeface | document | shernode-homes | younger-mc-nodeface | Neos.Neos:Document | {"uriPathSegment": "nody"} | + + Then I expect the documenturipath table to contain exactly: + # source: 65901ded4f068dac14ad0dce4f459b29 + # spec: 9a723c057afa02982dae9d0b541739be + # leafSpec: c60c44685475d0e2e4f2b964e6158ce2 + | dimensionspacepointhash | uripath | nodeaggregateidpath | nodeaggregateid | parentnodeaggregateid | precedingnodeaggregateid | succeedingnodeaggregateid | nodetypename | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "9a723c057afa02982dae9d0b541739be" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "9a723c057afa02982dae9d0b541739be" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "65901ded4f068dac14ad0dce4f459b29" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | null | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | null | "Neos.Neos:Document" | diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/NodeVariationEdgeCases.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/NodeVariationEdgeCases.feature new file mode 100644 index 00000000000..8265f425db0 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/NodeVariationEdgeCases.feature @@ -0,0 +1,300 @@ +@flowEntities @contentrepository +Feature: Test cases for node variation edge cases + + Scenario: Create peer variant of node to dimension space point with specializations + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | source,peer,peerSpec | peerSpec->peer | + And using the following node types: + """yaml + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example":"source"} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "shernode-homes" | + | nodeTypeName | "Neos.Neos:Site" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + And the graph projection is fully up to date + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "shernode-homes" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"peer"} | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeName | parentNodeAggregateId | succeedingSiblingNodeAggregateId | nodeTypeName | initialPropertyValues | + # Set up our test subject document + | nody-mc-nodeface | {"example":"source"} | document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "nody"} | + # Let's create some siblings, both in source and target, to check ordering + | elder-mc-nodeface | {"example":"source"} | elder-document | shernode-homes | nody-mc-nodeface | Neos.Neos:Document | {"uriPathSegment": "elder"} | + | youngest-mc-nodeface | {"example":"source"} | youngest-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "youngest"} | + | eldest-mc-nodeface | {"example":"peer"} | eldest-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "eldest"} | + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"peer"} | + And the graph projection is fully up to date + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"peer"} | + And the graph projection is fully up to date + # Complete the sibling set with a node in the target DSP between the middle and last node + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "younger-mc-nodeface" | + | nodeTypeName | "Neos.Neos:Document" | + | parentNodeAggregateId | "shernode-homes" | + | originDimensionSpacePoint | {"example":"peer"} | + | nodeName | "younger-document" | + | initialPropertyValues | {"uriPathSegment": "younger"} | + And the graph projection is fully up to date + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"peer"} | + And the graph projection is fully up to date + + Then I expect the documenturipath table to contain exactly: + # source: 65901ded4f068dac14ad0dce4f459b29 + # peer: fbe53ddc3305685fbb4dbf529f283a0e + # peerSpec: 2ca4fae2f65267c94c85602df0cbb728 + | dimensionspacepointhash | uripath | nodeaggregateidpath | nodeaggregateid | parentnodeaggregateid | precedingnodeaggregateid | succeedingnodeaggregateid | nodetypename | + | "2ca4fae2f65267c94c85602df0cbb728" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "fbe53ddc3305685fbb4dbf529f283a0e" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "2ca4fae2f65267c94c85602df0cbb728" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "fbe53ddc3305685fbb4dbf529f283a0e" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "2ca4fae2f65267c94c85602df0cbb728" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | null | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "fbe53ddc3305685fbb4dbf529f283a0e" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "2ca4fae2f65267c94c85602df0cbb728" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "fbe53ddc3305685fbb4dbf529f283a0e" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "2ca4fae2f65267c94c85602df0cbb728" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "fbe53ddc3305685fbb4dbf529f283a0e" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "2ca4fae2f65267c94c85602df0cbb728" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "fbe53ddc3305685fbb4dbf529f283a0e" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "2ca4fae2f65267c94c85602df0cbb728" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | null | "Neos.Neos:Document" | + | "fbe53ddc3305685fbb4dbf529f283a0e" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + + Scenario: Create generalization of node to dimension space point with further generalization and specializations + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | rootGeneral, general, source, specB | source -> general -> rootGeneral, specB -> general -> rootGeneral | + And using the following node types: + """yaml + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example":"source"} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "shernode-homes" | + | nodeTypeName | "Neos.Neos:Site" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"rootGeneral"} | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | originDimensionSpacePoint | nodeName | parentNodeAggregateId | succeedingSiblingNodeAggregateId | nodeTypeName | initialPropertyValues | + # Let's create some siblings, both in source and target, to check ordering + | eldest-mc-nodeface | {"example":"general"} | eldest-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "eldest"} | + | nody-mc-nodeface | {"example":"source"} | document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "nody"} | + | elder-mc-nodeface | {"example":"source"} | elder-document | shernode-homes | nody-mc-nodeface | Neos.Neos:Document | {"uriPathSegment": "elder"} | + | younger-mc-nodeface | {"example":"general"} | younger-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "younger"} | + | youngest-mc-nodeface | {"example":"source"} | youngest-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "youngest"} | + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"general"} | + And the graph projection is fully up to date + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "elder-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"general"} | + And the graph projection is fully up to date + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "youngest-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"general"} | + And the graph projection is fully up to date + + Then I expect the documenturipath table to contain exactly: + # general: 033e5de7b423f45bb4f5a09f73af839e + # source: 65901ded4f068dac14ad0dce4f459b29 + # sourceB: 9447118dcac98e2912f66a3387f057a0 + # rootGeneral: f02657442189da118ab86d745842894e + | dimensionspacepointhash | uripath | nodeaggregateidpath | nodeaggregateid | parentnodeaggregateid | precedingnodeaggregateid | succeedingnodeaggregateid | nodetypename | + | "033e5de7b423f45bb4f5a09f73af839e" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "9447118dcac98e2912f66a3387f057a0" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "f02657442189da118ab86d745842894e" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "033e5de7b423f45bb4f5a09f73af839e" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "9447118dcac98e2912f66a3387f057a0" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "f02657442189da118ab86d745842894e" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "033e5de7b423f45bb4f5a09f73af839e" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "9447118dcac98e2912f66a3387f057a0" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "033e5de7b423f45bb4f5a09f73af839e" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "9447118dcac98e2912f66a3387f057a0" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "033e5de7b423f45bb4f5a09f73af839e" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "9447118dcac98e2912f66a3387f057a0" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "033e5de7b423f45bb4f5a09f73af839e" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "9447118dcac98e2912f66a3387f057a0" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "033e5de7b423f45bb4f5a09f73af839e" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + | "9447118dcac98e2912f66a3387f057a0" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + + Scenario: Delete the node in a virtual specialization and then create the node in that specialization, forcing the edges to be recreated + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | example | source, spec, leafSpec | leafSpec -> spec -> source | + And using the following node types: + """yaml + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in workspace "live" and dimension space point {"example":"source"} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "shernode-homes" | + | nodeTypeName | "Neos.Neos:Site" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeName | parentNodeAggregateId | succeedingSiblingNodeAggregateId | nodeTypeName | initialPropertyValues | + # Let's create our test subject... + | nody-mc-nodeface | document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "nody"} | + # ...and add some siblings to check orderings. Also, everything gets better with siblings. + | elder-mc-nodeface | elder-document | shernode-homes | nody-mc-nodeface | Neos.Neos:Document | {"uriPathSegment": "elder"} | + | eldest-mc-nodeface | eldest-document | shernode-homes | elder-mc-nodeface | Neos.Neos:Document | {"uriPathSegment": "eldest"} | + | younger-mc-nodeface | younger-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "younger"} | + | youngest-mc-nodeface | youngest-document | shernode-homes | | Neos.Neos:Document | {"uriPathSegment": "youngest"} | + And the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | coveredDimensionSpacePoint | {"example":"spec"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nody-mc-nodeface" | + | sourceOrigin | {"example":"source"} | + | targetOrigin | {"example":"spec"} | + And the graph projection is fully up to date + + Then I expect the documenturipath table to contain exactly: + # source: 65901ded4f068dac14ad0dce4f459b29 + # spec: 9a723c057afa02982dae9d0b541739be + # leafSpec: c60c44685475d0e2e4f2b964e6158ce2 + | dimensionspacepointhash | uripath | nodeaggregateidpath | nodeaggregateid | parentnodeaggregateid | precedingnodeaggregateid | succeedingnodeaggregateid | nodetypename | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "9a723c057afa02982dae9d0b541739be" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "" | "lady-eleonode-rootford" | "lady-eleonode-rootford" | null | null | null | "Neos.Neos:Sites" | + | "65901ded4f068dac14ad0dce4f459b29" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "9a723c057afa02982dae9d0b541739be" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "" | "lady-eleonode-rootford/shernode-homes" | "shernode-homes" | "lady-eleonode-rootford" | null | null | "Neos.Neos:Site" | + | "65901ded4f068dac14ad0dce4f459b29" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "elder" | "lady-eleonode-rootford/shernode-homes/elder-mc-nodeface" | "elder-mc-nodeface" | "shernode-homes" | "eldest-mc-nodeface" | "nody-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "eldest" | "lady-eleonode-rootford/shernode-homes/eldest-mc-nodeface" | "eldest-mc-nodeface" | "shernode-homes" | null | "elder-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "nody" | "lady-eleonode-rootford/shernode-homes/nody-mc-nodeface" | "nody-mc-nodeface" | "shernode-homes" | "elder-mc-nodeface" | "younger-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "younger" | "lady-eleonode-rootford/shernode-homes/younger-mc-nodeface" | "younger-mc-nodeface" | "shernode-homes" | "nody-mc-nodeface" | "youngest-mc-nodeface" | "Neos.Neos:Document" | + | "65901ded4f068dac14ad0dce4f459b29" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + | "9a723c057afa02982dae9d0b541739be" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | + | "c60c44685475d0e2e4f2b964e6158ce2" | "youngest" | "lady-eleonode-rootford/shernode-homes/youngest-mc-nodeface" | "youngest-mc-nodeface" | "shernode-homes" | "younger-mc-nodeface" | null | "Neos.Neos:Document" | diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/RouteCache.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/RouteCache.feature index 046c965289e..6a39474e497 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/RouteCache.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/RouteCache.feature @@ -30,7 +30,7 @@ Feature: Route cache invalidation | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -44,7 +44,7 @@ Feature: Route cache invalidation # earl-o-documentbourgh # nody-mc-nodeface # - And I am in content stream "cs-identifier" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | shernode-homes | lady-eleonode-rootford | Neos.Neos:Test.Routing.Page | {"uriPathSegment": "ignore-me"} | node1 | @@ -58,8 +58,9 @@ Feature: Route cache invalidation Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Shortcuts.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Shortcuts.feature index 8b315048502..d937e5607a7 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Shortcuts.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Shortcuts.feature @@ -47,7 +47,7 @@ Feature: Routing behavior of shortcut nodes | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -87,8 +87,9 @@ Feature: Routing behavior of shortcut nodes Neos: Neos: sites: - '*': - contentRepository: default + 'node1': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory @@ -243,7 +244,7 @@ Feature: Routing behavior of shortcut nodes Then the node "shortcut-first-child-node" in content stream "cs-identifier" and dimension "{}" should resolve to URL "http://www.neos.io/" Scenario: Change shortcut targetMode from "parentNode" to "firstChildNode" - And I am in content stream "cs-identifier" and dimension space point {} + And I am in workspace "live" and dimension space point {} When the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | new-child-node | shortcut-parent-node | Neos.Neos:Test.Routing.Page | {"uriPathSegment": "new-child"} | new | @@ -332,7 +333,7 @@ Feature: Routing behavior of shortcut nodes Then The node "invalid-shortcut-selected-node" in content stream "cs-identifier" and dimension "{}" should not resolve to an URL Scenario: Recursive shortcuts - And I am in content stream "cs-identifier" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | level-1 | shortcuts | Neos.Neos:Shortcut | {"uriPathSegment": "level1", "targetMode": "selectedTarget", "target": "node://level-2"} | level1 | @@ -343,7 +344,7 @@ Feature: Routing behavior of shortcut nodes Then the node "level-2" in content stream "cs-identifier" and dimension "{}" should resolve to URL "/david-nodenborough/shortcuts/shortcut-first-child/first-child-node" Scenario: Unlimited recursive shortcuts - And I am in content stream "cs-identifier" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | node-a | shortcuts | Neos.Neos:Shortcut | {"uriPathSegment": "a", "targetMode": "selectedTarget", "target": "node://node-b"} | node-a | diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/TetheredSiteChildDocuments.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/TetheredSiteChildDocuments.feature index df5b10e9ccc..d3ae4a3ee7d 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/TetheredSiteChildDocuments.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/TetheredSiteChildDocuments.feature @@ -30,7 +30,7 @@ Feature: Tests for site node child documents. These are special in that they hav | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | @@ -46,8 +46,9 @@ Feature: Tests for site node child documents. These are special in that they hav Neos: Neos: sites: - '*': - contentRepository: default + 'site': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/UnknownNodeTypes.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/UnknownNodeTypes.feature index 7923dac5f9e..fee65d02083 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/UnknownNodeTypes.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/UnknownNodeTypes.feature @@ -24,7 +24,7 @@ Feature: Basic routing functionality (match & resolve nodes with unknown types) | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" | diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCache.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCache.feature new file mode 100644 index 00000000000..bc0c4c68455 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCache.feature @@ -0,0 +1,66 @@ +@flowEntities @contentrepository +Feature: Tests for Fusion ContentCache + Background: + Given I have Fusion content cache enabled + And I have the following Fusion setup: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + prototype(Neos.Neos:Test.ContentCache) < prototype(Neos.Fusion:Component) { + foo = '' + renderer = ${props.foo} + @cache { + mode = 'cached' + entryIdentifier { + test = 'test' + } + } + } + +""" + + Scenario: Render a cached prototype and check if rerendering doesn't happen on second try + When I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.ContentCache { + foo = 'some-cached-string' + } + """ + Then I expect the following Fusion rendering result: + """ + some-cached-string + """ + When I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.ContentCache { + foo = 'some-other-string' + } + """ + Then I expect the following Fusion rendering result: + """ + some-cached-string + """ + + + Scenario: Check if cached got flushed before running a new scenario and no leftover of last test is there + When I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.ContentCache { + foo = 'some-new-string' + } + """ + Then I expect the following Fusion rendering result: + """ + some-new-string + """ + When I execute the following Fusion code: + """fusion + test = Neos.Neos:Test.ContentCache { + foo = 'totally-different-string' + } + """ + Then I expect the following Fusion rendering result: + """ + some-new-string + """ \ No newline at end of file diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCase.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCase.feature index d73a80cf0cf..d3ed34d23d2 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCase.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCase.feature @@ -33,7 +33,7 @@ Feature: Tests for the "Neos.Neos:ContentCase" Fusion prototype | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "root" | @@ -49,8 +49,9 @@ Feature: Tests for the "Neos.Neos:ContentCase" Fusion prototype Neos: Neos: sites: - '*': - contentRepository: default + 'a': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature index f68a96cf08b..40dbdcedf35 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/ContentCollection.feature @@ -41,7 +41,7 @@ Feature: Tests for the "Neos.Neos:ContentCollection" Fusion prototype | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "root" | @@ -56,8 +56,9 @@ Feature: Tests for the "Neos.Neos:ContentCollection" Fusion prototype Neos: Neos: sites: - '*': - contentRepository: default + 'a': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/ConvertUris.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/ConvertUris.feature index fccf8d719b6..da76e6f9217 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/ConvertUris.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/ConvertUris.feature @@ -30,7 +30,7 @@ Feature: Tests for the "Neos.Neos:ConvertUris" Fusion prototype | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "root" | @@ -47,8 +47,9 @@ Feature: Tests for the "Neos.Neos:ConvertUris" Fusion prototype Neos: Neos: sites: - '*': - contentRepository: default + 'a': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index b7073940274..4c0f6e0bbf3 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -15,7 +15,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. type: string uriPathSegment: type: string - _hiddenInIndex: + hiddenInMenu: type: bool 'Neos.Neos:Site': superTypes: @@ -71,7 +71,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. | a1b1b | a1b1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b1b", "title": "Node a1b1b"} | a1b1b | | a1b2 | a1b | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1b2", "title": "Node a1b2"} | a1b2 | | a1b3 | a1b | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b3", "title": "Node a1b3"} | a1b3 | - | a1c | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c", "title": "Node a1c", "_hiddenInIndex": true} | a1c | + | a1c | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c", "title": "Node a1c", "hiddenInMenu": true} | a1c | | a1c1 | a1c | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c1", "title": "Node a1c1"} | a1c1 | And A site exists for node name "a" and domain "http://localhost" And the sites configuration is: @@ -395,3 +395,48 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. removeNode: a nothingToRemove: a1a4,a1a4,a1a4 """ + + Scenario: Node accessors (final Node access operations) + When the Fusion context node is "a1" + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + property = ${q(node).property('title')} + identifier = ${q(node).id()} + label = ${q(node).label()} + nodeTypeName = ${q(node).nodeTypeName()} + @process.render = ${Json.stringify(value, ['JSON_PRETTY_PRINT'])} + } + """ + Then I expect the following Fusion rendering result: + """ + { + "property": "Node a1", + "identifier": "a1", + "label": "Neos.Neos:Test.DocumentType1 (a1)", + "nodeTypeName": "Neos.Neos:Test.DocumentType1" + } + """ + # if the node type config is empty, the operation should still work + When I change the node types in content repository "default" to: + """yaml + """ + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + property = ${q(node).property('title')} + identifier = ${q(node).id()} + label = ${q(node).label()} + nodeTypeName = ${q(node).nodeTypeName()} + @process.render = ${Json.stringify(value, ['JSON_PRETTY_PRINT'])} + } + """ + Then I expect the following Fusion rendering result: + """ + { + "property": "Node a1", + "identifier": "a1", + "label": "Neos.Neos:Test.DocumentType1 (a1)", + "nodeTypeName": "Neos.Neos:Test.DocumentType1" + } + """ diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/Menu.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/Menu.feature index d137b1b77ec..28ab06fe590 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/Menu.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/Menu.feature @@ -15,7 +15,7 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes type: string uriPathSegment: type: string - _hiddenInIndex: + hiddenInMenu: type: bool 'Neos.Neos:Site': superTypes: @@ -46,7 +46,7 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes | Key | Value | | workspaceName | "live" | | newContentStreamId | "cs-identifier" | - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "root" | @@ -63,7 +63,7 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes | a1b1b | a1b1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b1b", "title": "Node a1b1b"} | a1b1b | | a1b2 | a1b | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1b2", "title": "Node a1b2"} | a1b2 | | a1b3 | a1b | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b3", "title": "Node a1b3"} | a1b3 | - | a1c | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c", "title": "Node a1c", "_hiddenInIndex": true} | a1c | + | a1c | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c", "title": "Node a1c", "hiddenInMenu": true} | a1c | | a1c1 | a1c | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c1", "title": "Node a1c1"} | a1c1 | And A site exists for node name "a" and domain "http://localhost" And the sites configuration is: @@ -71,8 +71,9 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes Neos: Neos: sites: - '*': - contentRepository: default + 'a': + preset: default + uriPathSuffix: '' contentDimensions: resolver: factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory @@ -352,12 +353,12 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes """ - Scenario: MenuItems (renderHiddenInIndex) + Scenario: MenuItems (renderHiddenInMenu) When I execute the following Fusion code: """fusion test = Neos.Neos:Test.Menu { items = Neos.Neos:MenuItems { - renderHiddenInIndex = true + renderHiddenInMenu = true } } """ @@ -469,13 +470,13 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes """ - Scenario: MenuItems (startingPoint a1c, renderHiddenInIndex) + Scenario: MenuItems (startingPoint a1c, renderHiddenInMenu) When I execute the following Fusion code: """fusion test = Neos.Neos:Test.Menu { items = Neos.Neos:MenuItems { startingPoint = ${q(node).find('#a1c').get(0)} - renderHiddenInIndex = true + renderHiddenInMenu = true } } """ @@ -500,13 +501,13 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes """html
  • - Neos.Neos:Test.DocumentType1 + Neos.Neos:Test.DocumentType1 (a1)
  • @@ -528,13 +529,13 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes """html """ diff --git a/Neos.Neos/Tests/Functional/AbstractNodeTest.php b/Neos.Neos/Tests/Functional/AbstractNodeTest.php deleted file mode 100644 index b41c7069dff..00000000000 --- a/Neos.Neos/Tests/Functional/AbstractNodeTest.php +++ /dev/null @@ -1,86 +0,0 @@ -contextFactory = $this->objectManager->get(ContextFactoryInterface::class); - $contentContext = $this->contextFactory->create(['workspaceName' => 'live']); - $siteImportService = $this->objectManager->get(SiteImportService::class); - $siteImportService->importFromFile(__DIR__ . '/' . $this->fixtureFileName, $contentContext); - $this->persistenceManager->persistAll(); - - if ($this->nodeContextPath !== null) { - $this->node = $this->getNodeWithContextPath($this->nodeContextPath); - } - } - - /** - * Retrieve a node through the property mapper - * - * @param $contextPath - * @return Node - */ - protected function getNodeWithContextPath($contextPath) - { - /* @var $propertyMapper \Neos\Flow\Property\PropertyMapper */ - $propertyMapper = $this->objectManager->get(PropertyMapper::class); - $node = $propertyMapper->convert($contextPath, Node::class); - self::assertFalse($propertyMapper->getMessages()->hasErrors(), 'There were errors converting ' . $contextPath); - return $node; - } - - public function tearDown(): void - { - parent::tearDown(); - - $this->inject($this->contextFactory, 'contextInstances', []); - $this->inject($this->objectManager->get(AssetInterfaceConverter::class), 'resourcesAlreadyConvertedToAssets', []); - } -} diff --git a/Neos.Neos/Tests/Functional/Domain/Service/Fixtures/Sites.xml b/Neos.Neos/Tests/Functional/Domain/Service/Fixtures/Sites.xml deleted file mode 100644 index 1fe73b0d4cd..00000000000 --- a/Neos.Neos/Tests/Functional/Domain/Service/Fixtures/Sites.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/Neos.Neos/Tests/Functional/FlowQueryOperations/Fixtures/SortableNodes.xml b/Neos.Neos/Tests/Functional/FlowQueryOperations/Fixtures/SortableNodes.xml deleted file mode 100644 index 125b5045ecb..00000000000 --- a/Neos.Neos/Tests/Functional/FlowQueryOperations/Fixtures/SortableNodes.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Neos.Neos/Tests/Functional/FlowQueryOperations/SortOperationTest.php b/Neos.Neos/Tests/Functional/FlowQueryOperations/SortOperationTest.php deleted file mode 100755 index 99fea63ef1a..00000000000 --- a/Neos.Neos/Tests/Functional/FlowQueryOperations/SortOperationTest.php +++ /dev/null @@ -1,244 +0,0 @@ -markTestSkipped('Needs to be rewritten with ES CR'); - parent::setUp(); - $workspaceRepository = $this->objectManager->get(\Neos\ContentRepository\Domain\Repository\WorkspaceRepository::class); - $workspaceRepository->add(new Workspace('live')); - $this->persistenceManager->persistAll(); - $this->contextFactory = $this->objectManager->get(\Neos\ContentRepository\Domain\Service\ContextFactoryInterface::class); - $this->context = $this->contextFactory->create(['workspaceName' => 'live']); - - - $siteImportService = $this->objectManager->get(\Neos\Neos\Domain\Service\SiteImportService::class); - $siteImportService->importFromFile(__DIR__ . '/Fixtures/SortableNodes.xml', $this->context); - $this->persistenceManager->persistAll(); - $this->persistenceManager->clearState(); - $this->inject($this->contextFactory, 'contextInstances', []); - - // The context is not important here, just a quick way to get a (live) workspace - // $context = $this->contextFactory->create(); - $this->nodeDataRepository = $this->objectManager->get(\Neos\ContentRepository\Domain\Repository\NodeDataRepository::class); - } - - /** - * @return void - */ - public function tearDown(): void - { - parent::tearDown(); - $this->inject($this->contextFactory, 'contextInstances', []); - } - - /** - * @test+ - */ - public function callWithoutArgumentsCausesException() - { - $this->expectException(FlowQueryException::class); - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, []); - } - - /** - * @test - */ - public function invalidSortDirectionCausesException() - { - $this->expectException(FlowQueryException::class); - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['title', 'FOO']); - } - - /** - * @test - */ - public function sortByStringAscending() - { - $nodesToSort = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []) - ]; - $correctOrder = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []) - ]; - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery($nodesToSort); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['title', 'ASC']); - - self::assertEquals($correctOrder, $flowQuery->getContext()); - } - - /** - * @test - */ - public function sortByStringDescending() - { - $nodesToSort = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []) - ]; - $correctOrder = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []) - ]; - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery($nodesToSort); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['title', 'DESC']); - - self::assertEquals($correctOrder, $flowQuery->getContext()); - } - - /** - * @test - */ - public function sortByDateTimeAscending() - { - $nodesToSort = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []) - ]; - $correctOrder = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []) - ]; - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery($nodesToSort); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['_lastPublicationDateTime', 'ASC']); - - self::assertEquals($correctOrder, $flowQuery->getContext()); - } - - /** - * @test - */ - public function sortByDateTimeDescending() - { - $nodesToSort = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []) - ]; - $correctOrder = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []) - ]; - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery($nodesToSort); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['_lastPublicationDateTime', 'DESC']); - - self::assertEquals($correctOrder, $flowQuery->getContext()); - } - - /** - * @test - */ - public function invalidSortOptionCausesException() - { - $this->expectException(FlowQueryException::class); - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['title', 'ASC', 'SORT_BAR']); - } - - /** - * @test - */ - public function sortByStringNaturalCaseAscending() - { - $nodesToSort = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addb', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addc', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []) - ]; - $correctOrder = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addc', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addf', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addb', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addd', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115adde', $this->context->getWorkspace(true), []) - ]; - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery($nodesToSort); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['title', 'ASC', ['SORT_NATURAL', 'SORT_FLAG_CASE']]); - - self::assertEquals($correctOrder, $flowQuery->getContext()); - } - - /** - * @test - */ - public function sortByNumericDescending() - { - $nodesToSort = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addc', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addb', $this->context->getWorkspace(true), []), - ]; - $correctOrder = [ - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addb', $this->context->getWorkspace(true), []), - $this->nodeDataRepository->findOneByIdentifier('c381f64d-4269-429a-9c21-6d846115addc', $this->context->getWorkspace(true), []), - ]; - $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery($nodesToSort); - $operation = new SortOperation(); - $operation->evaluate($flowQuery, ['text', 'DESC', 'SORT_NUMERIC']); - - self::assertEquals($correctOrder, $flowQuery->getContext()); - } -} diff --git a/Neos.Neos/Tests/Functional/Fusion/Cache/ContentCacheFlusherTest.php b/Neos.Neos/Tests/Functional/Fusion/Cache/ContentCacheFlusherTest.php deleted file mode 100644 index 72ae26d3aba..00000000000 --- a/Neos.Neos/Tests/Functional/Fusion/Cache/ContentCacheFlusherTest.php +++ /dev/null @@ -1,260 +0,0 @@ -markTestSkipped('re-implement with Neos 9.0'); - parent::setUp(); - - $this->workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); - $this->nodeDataRepository = $this->objectManager->get(NodeDataRepository::class); - $this->contextFactory = $this->objectManager->get(ContextFactoryInterface::class); - $this->contentCacheFlusher = $this->objectManager->get(ContentCacheFlusher::class); - - $this->context = $this->contextFactory->create(['workspaceName' => 'live']); - $siteImportService = $this->objectManager->get(\Neos\Neos\Domain\Service\SiteImportService::class); - $siteImportService->importFromFile(__DIR__ . '/Fixtures/CacheableNodes.xml', $this->context); - - // Assume an empty state for $contentCacheFlusher - this is needed as importing nodes will register - // changes to the ContentCacheFlusher - $this->inject($this->contentCacheFlusher, 'workspacesToFlush', []); - $this->inject($this->contentCacheFlusher, 'tagsToFlush', []); - - $this->persistenceManager->persistAll(); - $this->persistenceManager->clearState(); - } - - /** - * @test - */ - public function flushingANodeWillResolveAllWorkspacesToFlush() - { - // Add more workspaces - $workspaceFirstLevel = new Workspace('first-level'); - $workspaceSecondLevel = new Workspace('second-level'); - $workspaceAlsoOnSecondLevel = new Workspace('also-second-level'); - $workspaceThirdLevel = new Workspace('third-level'); - - // And build up a chain - $liveWorkspace = $this->workspaceRepository->findByIdentifier('live'); - $workspaceThirdLevel->setBaseWorkspace($workspaceSecondLevel); - $workspaceSecondLevel->setBaseWorkspace($workspaceFirstLevel); - $workspaceAlsoOnSecondLevel->setBaseWorkspace($workspaceFirstLevel); - $workspaceFirstLevel->setBaseWorkspace($liveWorkspace); - - $this->workspaceRepository->add($workspaceFirstLevel); - $this->workspaceRepository->add($workspaceSecondLevel); - $this->workspaceRepository->add($workspaceAlsoOnSecondLevel); - $this->workspaceRepository->add($workspaceThirdLevel); - - $this->persistenceManager->persistAll(); - $this->persistenceManager->clearState(); - - // Make sure that we do have multiple workspaces set up in our database - self::assertEquals(5, $this->workspaceRepository->countAll()); - - // Create/Fetch a node in workspace "first-level" - $fistLevelContext = $this->contextFactory->create(['workspaceName' => $workspaceFirstLevel->getName()]); - $nodeInFirstLevelWorkspace = $fistLevelContext->getRootNode(); - - // When the node is flushed we expect three workspaces to be flushed - $this->contentCacheFlusher->registerNodeChange($nodeInFirstLevelWorkspace); - - $workspacesToFlush = ObjectAccess::getProperty($this->contentCacheFlusher, 'workspacesToFlush', true); - $workspaceChain = $workspacesToFlush['first-level']; - - self::assertArrayNotHasKey('live', $workspaceChain); - self::assertArrayHasKey('first-level', $workspaceChain); - self::assertArrayHasKey('second-level', $workspaceChain); - self::assertArrayHasKey('also-second-level', $workspaceChain); - self::assertArrayHasKey('third-level', $workspaceChain); - } - - /** - * @test - */ - public function flushingANodeWithAnAdditionalTargetWorkspaceWillAlsoResolveThatWorkspace() - { - // Add more workspaces - $workspaceFirstLevel = new Workspace('first-level'); - $workspaceSecondLevel = new Workspace('second-level'); - $workspaceAlsoOnSecondLevel = new Workspace('also-second-level'); - $workspaceThirdLevel = new Workspace('third-level'); - $workspaceAlsoFirstLevel = new Workspace('also-first-level'); - - // And build up a chain - $liveWorkspace = $this->workspaceRepository->findByIdentifier('live'); - $workspaceThirdLevel->setBaseWorkspace($workspaceSecondLevel); - $workspaceSecondLevel->setBaseWorkspace($workspaceFirstLevel); - $workspaceAlsoOnSecondLevel->setBaseWorkspace($workspaceFirstLevel); - $workspaceFirstLevel->setBaseWorkspace($liveWorkspace); - $workspaceAlsoFirstLevel->setBaseWorkspace($liveWorkspace); - - $this->workspaceRepository->add($workspaceFirstLevel); - $this->workspaceRepository->add($workspaceSecondLevel); - $this->workspaceRepository->add($workspaceAlsoOnSecondLevel); - $this->workspaceRepository->add($workspaceThirdLevel); - $this->workspaceRepository->add($workspaceAlsoFirstLevel); - - $this->persistenceManager->persistAll(); - $this->persistenceManager->clearState(); - - // Make sure that we do have multiple workspaces set up in our database - self::assertEquals(6, $this->workspaceRepository->countAll()); - - // Create/Fetch a node in workspace "first-level" - $fistLevelContext = $this->contextFactory->create(['workspaceName' => $workspaceFirstLevel->getName()]); - $nodeInFirstLevelWorkspace = $fistLevelContext->getRootNode(); - - // When the node is flushed we expect three workspaces to be flushed - $this->contentCacheFlusher->registerNodeChange($nodeInFirstLevelWorkspace, $workspaceAlsoFirstLevel); - - $workspacesToFlush = ObjectAccess::getProperty($this->contentCacheFlusher, 'workspacesToFlush', true); - - self::assertArrayHasKey('also-first-level', $workspacesToFlush); - self::assertArrayHasKey('first-level', $workspacesToFlush); - } - - /** - * @test - */ - public function aNodeChangeWillRegisterNodeIdentifierTagsForAllWorkspaces() - { - $workspaceFirstLevel = new Workspace('first-level'); - - $liveWorkspace = $this->workspaceRepository->findByIdentifier('live'); - $workspaceFirstLevel->setBaseWorkspace($liveWorkspace); - $this->workspaceRepository->add($workspaceFirstLevel); - - $this->persistenceManager->persistAll(); - $this->persistenceManager->clearState(); - - $nodeIdentifier = 'c381f64d-4269-429a-9c21-6d846115addd'; - $nodeToFlush = $this->context->getNodeByIdentifier($nodeIdentifier); - - $this->contentCacheFlusher->registerNodeChange($nodeToFlush); - - $tagsToFlush = ObjectAccess::getProperty($this->contentCacheFlusher, 'tagsToFlush', true); - - $cachingHelper = new CachingHelper(); - - $workspacesToTest = []; - $workspacesToTest[$liveWorkspace->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($liveWorkspace->getName()); - $workspacesToTest[$workspaceFirstLevel->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($workspaceFirstLevel->getName()); - - foreach ($workspacesToTest as $name => $workspaceHash) { - self::assertArrayHasKey('Node_'.$workspaceHash.'_'.$nodeIdentifier, $tagsToFlush, 'on workspace ' . $name); - self::assertArrayHasKey('DescendantOf_'.$workspaceHash.'_'.$nodeIdentifier, $tagsToFlush, 'on workspace ' . $name); - } - } - - /** - * @test - */ - public function aNodeChangeWillRegisterNodeTypeTagsForAllWorkspaces() - { - $workspaceFirstLevel = new Workspace('first-level'); - - $liveWorkspace = $this->workspaceRepository->findByIdentifier('live'); - $workspaceFirstLevel->setBaseWorkspace($liveWorkspace); - $this->workspaceRepository->add($workspaceFirstLevel); - - $this->persistenceManager->persistAll(); - $this->persistenceManager->clearState(); - - $nodeIdentifier = 'c381f64d-4269-429a-9c21-6d846115addd'; - $nodeToFlush = $this->context->getNodeByIdentifier($nodeIdentifier); - - $this->contentCacheFlusher->registerNodeChange($nodeToFlush); - - $tagsToFlush = ObjectAccess::getProperty($this->contentCacheFlusher, 'tagsToFlush', true); - - $cachingHelper = new CachingHelper(); - - $workspacesToTest = []; - $workspacesToTest[$liveWorkspace->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($liveWorkspace->getName()); - $workspacesToTest[$workspaceFirstLevel->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($workspaceFirstLevel->getName()); - - // Check for tags that respect the workspace hash - foreach ($workspacesToTest as $name => $workspaceHash) { - self::assertArrayHasKey('NodeType_'.$workspaceHash.'_Neos.Neos:Content', $tagsToFlush, 'on workspace ' . $name); - self::assertArrayHasKey('NodeType_'.$workspaceHash.'_Neos.Neos:Node', $tagsToFlush, 'on workspace ' . $name); - self::assertArrayHasKey('NodeType_'.$workspaceHash.'_Acme.Demo:Text', $tagsToFlush, 'on workspace ' . $name); - } - } - - /** - * @test - */ - public function aNodeChangeWillRegisterAllDescendantOfTagsForAllWorkspaces() - { - $workspaceFirstLevel = new Workspace('first-level'); - - $liveWorkspace = $this->workspaceRepository->findByIdentifier('live'); - $workspaceFirstLevel->setBaseWorkspace($liveWorkspace); - $this->workspaceRepository->add($workspaceFirstLevel); - - $this->persistenceManager->persistAll(); - $this->persistenceManager->clearState(); - - $nodeIdentifier = 'c381f64d-4269-429a-9c21-6d846115addd'; - $nodeToFlush = $this->context->getNodeByIdentifier($nodeIdentifier); - - $this->contentCacheFlusher->registerNodeChange($nodeToFlush); - - $tagsToFlush = ObjectAccess::getProperty($this->contentCacheFlusher, 'tagsToFlush', true); - - $cachingHelper = new CachingHelper(); - - $workspacesToTest = []; - $workspacesToTest[$liveWorkspace->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($liveWorkspace->getName()); - $workspacesToTest[$workspaceFirstLevel->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($workspaceFirstLevel->getName()); - - foreach ($workspacesToTest as $name => $workspaceHash) { - self::assertArrayHasKey('DescendantOf_'.$workspaceHash.'_c381f64d-4269-429a-9c21-6d846115addd', $tagsToFlush, 'on workspace ' . $name); - self::assertArrayHasKey('DescendantOf_'.$workspaceHash.'_c381f64d-4269-429a-9c21-6d846115adde', $tagsToFlush, 'on workspace ' . $name); - self::assertArrayHasKey('DescendantOf_'.$workspaceHash.'_c381f64d-4269-429a-9c21-6d846115addf', $tagsToFlush, 'on workspace ' . $name); - } - } - - public function tearDown(): void - { - $this->contextFactory->reset(); - parent::tearDown(); - } -} diff --git a/Neos.Neos/Tests/Functional/Fusion/Cache/Fixtures/CacheableNodes.xml b/Neos.Neos/Tests/Functional/Fusion/Cache/Fixtures/CacheableNodes.xml deleted file mode 100644 index d76d395d8d2..00000000000 --- a/Neos.Neos/Tests/Functional/Fusion/Cache/Fixtures/CacheableNodes.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php index 758ad21da46..cc239a574de 100644 --- a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php +++ b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php @@ -15,9 +15,7 @@ use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValue; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; -use Neos\ContentRepository\Core\NodeType\ClosureNodeTypeProvider; use Neos\ContentRepository\Core\NodeType\NodeType; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; @@ -30,6 +28,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\TestSuite\Unit\NodeSubjectProvider; use Neos\Fusion\Tests\Functional\FusionObjects\AbstractFusionObjectTest; +use Neos\Fusion\Tests\Functional\FusionObjects\TestingViewForFusionRuntime; use PHPUnit\Framework\MockObject\MockObject; /** @@ -107,7 +106,7 @@ public function crop() self::assertEquals('Some -', (string)$view->render()); } - protected function buildView() + protected function buildView(): TestingViewForFusionRuntime { $view = parent::buildView(); @@ -133,12 +132,7 @@ protected function setUp(): void 'ui' => [ 'label' => 'Content.Text' ] - ], - new NodeTypeManager( - new ClosureNodeTypeProvider( - fn () => [], - ) - ), + ] ); $textNodeProperties = new PropertyCollection( diff --git a/Neos.Neos/Tests/Unit/Domain/Model/SiteTest.php b/Neos.Neos/Tests/Unit/Domain/Model/SiteTest.php index b7054ba7a3e..70de94487e1 100644 --- a/Neos.Neos/Tests/Unit/Domain/Model/SiteTest.php +++ b/Neos.Neos/Tests/Unit/Domain/Model/SiteTest.php @@ -12,6 +12,7 @@ */ use Neos\Flow\Tests\UnitTestCase; use Neos\Neos\Domain\Model\Site; +use Neos\Utility\ObjectAccess; /** * Testcase for the "Site" domain model @@ -57,4 +58,51 @@ public function theSiteResourcesPackageKeyCanBeSetAndRetrieved() $site->setSiteResourcesPackageKey('Foo'); self::assertSame('Foo', $site->getSiteResourcesPackageKey()); } + + public static function getConfigurationFailingDataProvider(): iterable + { + yield 'no matching nor default site config' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => [], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'Missing configuration for "Neos.Neos.sites.siteNodeName" or fallback "Neos.Neos.sites.*"']; + yield 'referring non-string preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['preset' => false]], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'Invalid "preset" configuration for "Neos.Neos.sites.siteNodeName". Expected string, got: bool']; + yield 'referring non-existing preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['preset' => 'nonExistingPreset']], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'Site settings "Neos.Neos.sites.siteNodeName" refer to a preset "nonExistingPreset"']; + yield 'missing content repository identifier' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => []], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'There is no content repository identifier configured in Sites configuration in Settings.yaml: Neos.Neos.sites.*.contentRepository']; + yield 'missing content dimension resolver factory' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['contentRepository' => 'default']], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'No Dimension Resolver Factory configured at Neos.Neos.sites.*.contentDimensions.resolver.factoryClassName']; + } + + /** + * @test + * @dataProvider getConfigurationFailingDataProvider + */ + public function getConfigurationFailingTests(string $nodeTypeName, array $sitesConfiguration, array $sitePresetsConfiguration, string $expectedExceptionMessage): void + { + $site = new Site($nodeTypeName); + ObjectAccess::setProperty($site, 'sitesConfiguration', $sitesConfiguration, true); + ObjectAccess::setProperty($site, 'sitePresetsConfiguration', $sitePresetsConfiguration, true); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + $site->getConfiguration(); + } + + public static function getConfigurationSucceedingDataProvider(): iterable + { + yield 'minimal configuration' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['contentRepository' => 'default', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Foo']]]], 'sitePresetConfiguration' => [], 'expectedConfiguration' => ['contentRepositoryId' => 'default', 'contentDimensionResolverFactoryClassName' => 'Foo', 'contentDimensionResolverOptions' => [], 'defaultDimensionSpacePoint' => [], 'uriPathSuffix' => '']]; + yield 'full configuration' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-suffix']], 'sitePresetConfiguration' => [], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de'], 'uriPathSuffix' => 'some-suffix']]; + yield 'full configuration from fallback' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['*' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-suffix']], 'sitePresetConfiguration' => [], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de'], 'uriPathSuffix' => 'some-suffix']]; + yield 'full configuration merged with preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['preset' => 'somePreset', 'contentDimensions' => ['defaultDimensionSpacePoint' => ['country' => 'DE']], 'uriPathSuffix' => 'some-overridden-suffix']], 'sitePresetConfiguration' => ['somePreset' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-default-suffix']], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de', 'country' => 'DE'], 'uriPathSuffix' => 'some-overridden-suffix']]; + yield 'full configuration from fallback merged with preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['*' => ['preset' => 'somePreset', 'contentDimensions' => ['defaultDimensionSpacePoint' => ['country' => 'DE']], 'uriPathSuffix' => 'some-overridden-suffix']], 'sitePresetConfiguration' => ['somePreset' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-default-suffix']], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de', 'country' => 'DE'], 'uriPathSuffix' => 'some-overridden-suffix']]; + } + + /** + * @test + * @dataProvider getConfigurationSucceedingDataProvider + */ + public function getConfigurationSucceedingTests(string $nodeTypeName, array $sitesConfiguration, array $sitePresetsConfiguration, array $expectedConfiguration): void + { + $site = new Site($nodeTypeName); + ObjectAccess::setProperty($site, 'sitesConfiguration', $sitesConfiguration, true); + ObjectAccess::setProperty($site, 'sitePresetsConfiguration', $sitePresetsConfiguration, true); + + $configuration = $site->getConfiguration(); + self::assertSame($expectedConfiguration, json_decode(json_encode($configuration), true)); + } } diff --git a/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php b/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php index 39593920d4c..15a787667d8 100644 --- a/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php +++ b/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php @@ -14,6 +14,8 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Domain\Model\Workspace; use Neos\ContentRepository\Domain\Service\Context; use Neos\Flow\Tests\UnitTestCase; @@ -198,21 +200,20 @@ private function createNode(NodeAggregateId $nodeAggregateId): Node { $now = new \DateTimeImmutable(); return Node::create( - ContentSubgraphIdentity::create( - ContentRepositoryId::fromString("default"), - ContentStreamId::fromString("cs-identifier"), - DimensionSpacePoint::createWithoutDimensions(), - VisibilityConstraints::withoutRestrictions() - ), + ContentRepositoryId::fromString("default"), + WorkspaceName::forLive(), + DimensionSpacePoint::createWithoutDimensions(), $nodeAggregateId, OriginDimensionSpacePoint::createWithoutDimensions(), NodeAggregateClassification::CLASSIFICATION_REGULAR, NodeTypeName::fromString("SomeNodeTypeName"), - null, new PropertyCollection(SerializedPropertyValues::fromArray([]), new PropertyConverter(new Serializer([], []))), null, NodeTags::createEmpty(), - Timestamps::create($now, $now, null, null) + Timestamps::create($now, $now, null, null), + VisibilityConstraints::withoutRestrictions(), + null, + ContentStreamId::fromString("cs-identifier"), ); } } diff --git a/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php b/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php index e8aff26d8ea..3dc6fba48dc 100644 --- a/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php +++ b/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php @@ -65,6 +65,8 @@ class PluginImplementationTest extends UnitTestCase public function setUp(): void { + $this->markTestSkipped('TODO Doesnt test any thing really, has to be rewritten as behat test.'); + $this->pluginImplementation = $this->getAccessibleMock(PluginImplementation::class, ['buildPluginRequest'], [], '', false); $this->mockHttpUri = $this->getMockBuilder(Uri::class)->disableOriginalConstructor()->getMock(); @@ -92,6 +94,11 @@ public function setUp(): void */ public function responseHeadersDataProvider(): array { + /* + * Fyi (from christian) Multiple competing headers like that are a broken use case anyways. + * Headers by definition can appear multiple times, we can't really know if we should remove the first one and when not. + * IMHO the test is misleading the result might as well (correctly) be key => [value, value] + */ return [ [ 'Plugin response key does already exist in parent with same value', diff --git a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php index a0370ee9c0a..c1d55055280 100644 --- a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php +++ b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php @@ -343,7 +343,7 @@ private function processConfiguration(array $configuration, array $dataTypesDefa $mockNodeType = new NodeType( NodeTypeName::fromString('Some.NodeType:Name'), [], - [], + [] ); $postprocessor->process($mockNodeType, $configuration, []); return $configuration; diff --git a/Neos.TimeableNodeVisibility/Classes/Command/TimeableNodeVisibilityCommandController.php b/Neos.TimeableNodeVisibility/Classes/Command/TimeableNodeVisibilityCommandController.php index 56624a83685..aa90155bdb2 100644 --- a/Neos.TimeableNodeVisibility/Classes/Command/TimeableNodeVisibilityCommandController.php +++ b/Neos.TimeableNodeVisibility/Classes/Command/TimeableNodeVisibilityCommandController.php @@ -28,10 +28,9 @@ public function executeCommand(string $contentRepository = 'default', bool $quie $this->output->outputLine(sprintf('Enabled %d nodes with exceeded timed dates.', $handlingResult->countByType(ChangedVisibilityType::NODE_WAS_ENABLED))); foreach ($handlingResult->getByType(ChangedVisibilityType::NODE_WAS_ENABLED) as $result) { $this->output->outputLine(sprintf( - '- NodeAggregateId: %s, DimensionSpacePoint: %s, Label: %s', + '- NodeAggregateId: %s, DimensionSpacePoint: %s', $result->node->nodeAggregateId->value, - join(',', $result->node->originDimensionSpacePoint->coordinates), - $result->node->getLabel() + join(',', $result->node->originDimensionSpacePoint->coordinates) ) ); } @@ -39,10 +38,9 @@ public function executeCommand(string $contentRepository = 'default', bool $quie $this->output->outputLine(sprintf('Disabled %d nodes with exceeded timed dates.', $handlingResult->countByType(ChangedVisibilityType::NODE_WAS_DISABLED))); foreach ($handlingResult->getByType(ChangedVisibilityType::NODE_WAS_DISABLED) as $result) { $this->output->outputLine(sprintf( - '- NodeAggregateId: %s, DimensionSpacePoint: %s, Label: %s', + '- NodeAggregateId: %s, DimensionSpacePoint: %s', $result->node->nodeAggregateId->value, - join(',', $result->node->originDimensionSpacePoint->coordinates), - $result->node->getLabel() + join(',', $result->node->originDimensionSpacePoint->coordinates) ) ); } diff --git a/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php b/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php index 07bb0429ab9..b76e74eed3c 100644 --- a/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php +++ b/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php @@ -42,14 +42,9 @@ class TimeableNodeVisibilityService public function handleExceededNodeDates(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): ChangedVisibilities { $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $liveWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName); - if ($liveWorkspace === null) { - throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); - } - $now = new \DateTimeImmutable(); - $nodes = $this->getNodesWithExceededDates($contentRepository, $liveWorkspace, $now); + $nodes = $this->getNodesWithExceededDates($contentRepository, $workspaceName, $now); $results = []; /** @var Node $node */ @@ -58,7 +53,7 @@ public function handleExceededNodeDates(ContentRepositoryId $contentRepositoryId if ($this->needsEnabling($node, $now) && $nodeIsDisabled) { $contentRepository->handle( EnableNodeAggregate::create( - $liveWorkspace->workspaceName, + $workspaceName, $node->nodeAggregateId, $node->subgraphIdentity->dimensionSpacePoint, NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS @@ -72,7 +67,7 @@ public function handleExceededNodeDates(ContentRepositoryId $contentRepositoryId if ($this->needsDisabling($node, $now) && !$nodeIsDisabled) { $contentRepository->handle( DisableNodeAggregate::create( - $liveWorkspace->workspaceName, + $workspaceName, $node->nodeAggregateId, $node->subgraphIdentity->dimensionSpacePoint, NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS @@ -89,17 +84,16 @@ public function handleExceededNodeDates(ContentRepositoryId $contentRepositoryId /** * @return \Generator */ - private function getNodesWithExceededDates(ContentRepository $contentRepository, Workspace $liveWorkspace, \DateTimeImmutable $now): \Generator + private function getNodesWithExceededDates(ContentRepository $contentRepository, WorkspaceName $workspaceName, \DateTimeImmutable $now): \Generator { $dimensionSpacePoints = $contentRepository->getVariationGraph()->getDimensionSpacePoints(); foreach ($dimensionSpacePoints as $dimensionSpacePoint) { - $contentGraph = $contentRepository->getContentGraph(); + $contentGraph = $contentRepository->getContentGraph($workspaceName); // We fetch without restriction to get also all disabled nodes $subgraph = $contentGraph->getSubgraph( - $liveWorkspace->currentContentStreamId, $dimensionSpacePoint, VisibilityConstraints::withoutRestrictions() ); @@ -163,11 +157,11 @@ private function needsDisabling(Node $node, \DateTimeImmutable $now): bool private function logResult(ChangedVisibility $result): void { $this->logger->info( - sprintf('Timed node visibility: %s node [NodeAggregateId: %s, DimensionSpacePoints: %s]: %s', + sprintf('Timed node visibility: %s node [NodeAggregateId: %s, DimensionSpacePoints: %s]', $result->type->value, $result->node->nodeAggregateId->value, - join(',', $result->node->originDimensionSpacePoint->coordinates), - $result->node->getLabel()) + implode(',', $result->node->originDimensionSpacePoint->coordinates) + ) ); } } diff --git a/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php index 66f948b239f..14907e8d62e 100644 --- a/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php @@ -60,8 +60,7 @@ public function iHandleExceededNodeDates(): void public function iExpectThisNodeToBeEnabled(): void { Assert::assertNotNull($this->currentNode, 'No current node selected'); - $subgraph = $this->currentContentRepository->getContentGraph()->getSubgraph( - $this->currentContentStreamId, + $subgraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->getSubgraph( $this->currentDimensionSpacePoint, VisibilityConstraints::withoutRestrictions(), ); @@ -76,8 +75,7 @@ public function iExpectThisNodeToBeEnabled(): void public function iExpectThisNodeToBeDisabled(): void { Assert::assertNotNull($this->currentNode, 'No current node selected'); - $subgraph = $this->currentContentRepository->getContentGraph()->getSubgraph( - $this->currentContentStreamId, + $subgraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName)->getSubgraph( $this->currentDimensionSpacePoint, VisibilityConstraints::withoutRestrictions(), ); diff --git a/Neos.TimeableNodeVisibility/Tests/Behavior/Features/HandleExceeded.feature b/Neos.TimeableNodeVisibility/Tests/Behavior/Features/HandleExceeded.feature index 845bb8fc66c..02bc27ece9b 100644 --- a/Neos.TimeableNodeVisibility/Tests/Behavior/Features/HandleExceeded.feature +++ b/Neos.TimeableNodeVisibility/Tests/Behavior/Features/HandleExceeded.feature @@ -33,7 +33,7 @@ Feature: Simple handling of nodes with exceeded enableAfter and disableAfter dat | newContentStreamId | "cs-identifier" | And the graph projection is fully up to date And I am in workspace "live" - And I am in the active content stream of workspace "live" and dimension space point {} + And I am in workspace "live" and dimension space point {} And the command CreateRootNodeAggregateWithNode is executed with payload: | Key | Value | | nodeAggregateId | "lady-eleonode-rootford" |