diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index 28aad3f9561..873a700ea17 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -22,6 +22,7 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap\Helpers\TestingNodeAggregateId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -123,22 +124,35 @@ public function iChangeTheFollowingHierarchyRelationsDimensionSpacePointHash(Tab } /** - * @When /^I change the following hierarchy relation's name:$/ + * @When /^I change the following node's name:$/ * @param TableNode $payloadTable * @throws DBALException */ - public function iChangeTheFollowingHierarchyRelationsEdgeName(TableNode $payloadTable): void + public function iChangeTheFollowingNodesName(TableNode $payloadTable): void { $dataset = $this->transformPayloadTableToDataset($payloadTable); - $record = $this->transformDatasetToHierarchyRelationRecord($dataset); - unset($record['position']); + + $relationAnchorPoint = $this->dbalClient->getConnection()->executeQuery( + 'SELECT n.relationanchorpoint FROM ' . $this->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->tableNames()->hierarchyRelation(), + $this->tableNames()->node(), [ 'name' => $dataset['newName'] ], - $record + [ + 'relationanchorpoint' => $relationAnchorPoint + ] ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/TetheredNodesAreNamed.feature index 65703ac4a8f..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 workspace "live" and dimension space point {"language":"de"} And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | + | Key | Value | + | nodeAggregateId | "lady-eleonode-rootford" | + | nodeTypeName | "Neos.ContentRepository:Root" | And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {"language":"de"} | + | coveredDimensionSpacePoints | [{"language":"de"}] | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + | nodeAggregateClassification | "regular" | And the graph projection is fully up to date - Scenario: Create node variants of different type + Scenario: Remove tethered node's name When the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nodewyn-tetherton" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "to-be-hacked-to-null" | - | nodeAggregateClassification | "tethered" | + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "nodewyn-tetherton" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | originDimensionSpacePoint | {"language":"de"} | + | coveredDimensionSpacePoints | [{"language":"de"}] | + | parentNodeAggregateId | "sir-david-nodenborough" | + | nodeName | "to-be-hacked-to-null" | + | nodeAggregateClassification | "tethered" | And the graph projection is fully up to date - And I change the following hierarchy relation's name: - | Key | Value | - | contentStreamId | "cs-identifier" | - | dimensionSpacePoint | {"language":"de"} | - | parentNodeAggregateId | "sir-david-nodenborough" | - | childNodeAggregateId | "nodewyn-tetherton" | - | newName | null | + And I change the following node's name: + | Key | Value | + | contentStreamId | "cs-identifier" | + | originDimensionSpacePoint | {"language":"de"} | + | nodeAggregateId | "nodewyn-tetherton" | + | newName | null | And I run integrity violation detection Then I expect the integrity violation detection result to contain exactly 1 errors And I expect integrity violation detection result error number 1 to have code 1597923103 diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index d97e80194c7..ade91c5d169 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -321,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->tableNames->hierarchyRelation() . ' h - INNER JOIN ' . $this->tableNames->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) + ); + } + ); + } }); } @@ -408,7 +404,6 @@ private function createNodeWithHierarchy( $node->relationAnchorPoint, new DimensionSpacePointSet([$dimensionSpacePoint]), $succeedingSibling?->relationAnchorPoint, - $nodeName ); } } @@ -419,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 @@ -430,7 +424,6 @@ private function connectHierarchy( NodeRelationAnchorPoint $childNodeAnchorPoint, DimensionSpacePointSet $dimensionSpacePointSet, ?NodeRelationAnchorPoint $succeedingSiblingNodeAnchorPoint, - NodeName $relationName = null ): void { foreach ($dimensionSpacePointSet as $dimensionSpacePoint) { $position = $this->getRelationPosition( @@ -447,7 +440,6 @@ private function connectHierarchy( $hierarchyRelation = new HierarchyRelation( $parentNodeAnchorPoint, $childNodeAnchorPoint, - $relationName, $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, @@ -562,7 +554,6 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void INSERT INTO ' . $this->tableNames->hierarchyRelation() . ' ( parentnodeanchor, childnodeanchor, - `name`, position, dimensionspacepointhash, subtreetags, @@ -571,7 +562,6 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void SELECT h.parentnodeanchor, h.childnodeanchor, - h.name, h.position, h.dimensionspacepointhash, h.subtreetags, @@ -744,7 +734,6 @@ protected function copyHierarchyRelationToDimensionSpacePoint( $copy = new HierarchyRelation( $newParent, $newChild, - $sourceHierarchyRelation->name, $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, @@ -964,7 +953,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded INSERT INTO ' . $this->tableNames->hierarchyRelation() . ' ( parentnodeanchor, childnodeanchor, - `name`, position, subtreetags, dimensionspacepointhash, @@ -973,7 +961,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded SELECT h.parentnodeanchor, h.childnodeanchor, - h.name, h.position, h.subtreetags, :newDimensionSpacePointHash AS dimensionspacepointhash, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index c8520724f01..e6bc7fc028e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -39,6 +39,7 @@ private function createNodeTable(): Table DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotnull(false), DbalSchemaFactory::columnForDimensionSpacePointHash('origindimensionspacepointhash')->setNotnull(false), DbalSchemaFactory::columnForNodeTypeName('nodetypename'), + (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('properties', Type::getType(Types::TEXT)))->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION), (new Column('classification', Type::getType(Types::BINARY)))->setLength(20)->setNotnull(true), (new Column('created', Type::getType(Types::DATETIME_IMMUTABLE)))->setDefault('CURRENT_TIMESTAMP')->setNotnull(true), @@ -56,7 +57,6 @@ private function createNodeTable(): Table private function createHierarchyRelationTable(): Table { $table = new Table($this->contentGraphTableNames->hierarchyRelation(), [ - (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true), diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php index 15f11b3ff1a..53a1a5004eb 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php @@ -105,7 +105,6 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria $hierarchyRelation = new HierarchyRelation( $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, - $sourceNode->nodeName, $event->contentStreamId, $uncoveredDimensionSpacePoint, $uncoveredDimensionSpacePoint->hash, @@ -358,7 +357,6 @@ public function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, $peerNode->relationAnchorPoint, new DimensionSpacePointSet([$coveredDimensionSpacePoint]), $peerSucceedingSiblingNode?->relationAnchorPoint, - $sourceNode->nodeName ); } @@ -391,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/HierarchyRelation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php index f829f87a936..81d4218d56e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php @@ -19,7 +19,6 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @@ -32,7 +31,6 @@ public function __construct( public NodeRelationAnchorPoint $parentNodeAnchor, public NodeRelationAnchorPoint $childNodeAnchor, - public ?NodeName $name, public ContentStreamId $contentStreamId, public DimensionSpacePoint $dimensionSpacePoint, public string $dimensionSpacePointHash, @@ -55,7 +53,6 @@ public function addToDatabase(Connection $databaseConnection, ContentGraphTableN $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, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index 6ce216ba365..5af86607ea4 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -41,7 +41,6 @@ public function __construct( public SerializedPropertyValues $properties, public NodeTypeName $nodeTypeName, public NodeAggregateClassification $classification, - /** Transient node name to store a node name after fetching a node with hierarchy (not always available) */ public ?NodeName $nodeName, public Timestamps $timestamps, ) { @@ -59,6 +58,7 @@ public function updateToDatabase(Connection $databaseConnection, ContentGraphTab '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, @@ -123,6 +123,7 @@ public static function createNewInDatabase( $originDimensionSpacePointHash, $properties, $nodeTypeName, + $nodeName, $classification, $timestamps ) { @@ -134,6 +135,7 @@ public static function createNewInDatabase( 'origindimensionspacepointhash' => $originDimensionSpacePointHash, 'properties' => json_encode($properties), 'nodetypename' => $nodeTypeName->value, + 'name' => $nodeName?->value, 'classification' => $classification->value, 'created' => $timestamps->created, 'originalcreated' => $timestamps->originalCreated, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php index 2e99276d62b..78722fbff69 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php @@ -160,7 +160,7 @@ public function tetheredNodesAreNamed(): Result 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 @@ -191,7 +191,7 @@ public function subtreeTagsAreInherited(): Result // This could probably be solved with JSON_ARRAY_INTERSECT(JSON_KEYS(ph.subtreetags), JSON_KEYS(h.subtreetags) but unfortunately that's only available with MariaDB 11.2+ according to https://mariadb.com/kb/en/json_array_intersect/ $hierarchyRelationsWithMissingSubtreeTags = $this->client->getConnection()->executeQuery( 'SELECT - ph.name + ph.* FROM ' . $this->tableNames->hierarchyRelation() . ' h INNER JOIN ' . $this->tableNames->hierarchyRelation() . ' ph diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index a11b50f9b29..d21056908c7 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -177,6 +177,10 @@ public function findNodeAggregateById( } /** + * Parent node aggregates can have a greater dimension space coverage than the given child. + * Thus, it is not enough to just resolve them from the nodes and edges connected to the given child node aggregate. + * Instead, we resolve all parent node aggregate ids instead and fetch the complete aggregates from there. + * * @return iterable */ public function findParentNodeAggregates( @@ -218,7 +222,7 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint(NodeAggr ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->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') @@ -246,6 +250,17 @@ public function findTetheredChildNodeAggregates(NodeAggregateId $parentNodeAggre return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } + public function findChildNodeAggregateByName( + NodeAggregateId $parentNodeAggregateId, + NodeName $name + ): ?NodeAggregate { + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId) + ->andWhere('cn.name = :relationName') + ->setParameter('relationName', $name->value); + + return $this->mapQueryBuilderToNodeAggregate($queryBuilder); + } + public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, OriginDimensionSpacePoint $parentNodeOriginDimensionSpacePoint, DimensionSpacePointSet $dimensionSpacePointsToCheck): DimensionSpacePointSet { $queryBuilder = $this->createQueryBuilder() @@ -259,7 +274,7 @@ public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeNam ->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, @@ -277,20 +292,6 @@ public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeNam return new DimensionSpacePointSet($dimensionSpacePoints); } - /** - * @return iterable - */ - public function findChildNodeAggregatesByName( - NodeAggregateId $parentNodeAggregateId, - NodeName $name - ): iterable { - $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId) - ->andWhere('ch.name = :relationName') - ->setParameter('relationName', $name->value); - - return $this->mapQueryBuilderToNodeAggregates($queryBuilder); - } - public function countNodes(): int { $queryBuilder = $this->createQueryBuilder() @@ -320,6 +321,15 @@ private function createQueryBuilder(): QueryBuilder return $this->client->getConnection()->createQueryBuilder(); } + private function mapQueryBuilderToNodeAggregate(QueryBuilder $queryBuilder): ?NodeAggregate + { + return $this->nodeFactory->mapNodeRowsToNodeAggregate( + $this->fetchRows($queryBuilder), + $this->contentStreamId, + VisibilityConstraints::withoutRestrictions() + ); + } + /** * @param QueryBuilder $queryBuilder * @return iterable diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index a8c43d988aa..420b8d2a80f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -208,7 +208,7 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node { $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint) - ->andWhere('h.name = :edgeName')->setParameter('edgeName', $nodeName->value); + ->andWhere('n.name = :edgeName')->setParameter('edgeName', $nodeName->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } @@ -252,7 +252,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi { $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation - ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') @@ -261,7 +261,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('c.*, h.name, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') + ->select('c.*, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') ->from('tree', 'p') ->innerJoin('p', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = p.relationanchorpoint') ->innerJoin('p', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = h.childnodeanchor') @@ -344,7 +344,7 @@ public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, CountA public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClosestNodeFilter $filter): ?Node { $queryBuilderInitial = $this->createQueryBuilder() - ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') + ->select('n.*, ph.subtreetags, ph.parentnodeanchor') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') @@ -354,7 +354,7 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') + ->select('pn.*, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') ->innerJoin('cn', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') @@ -472,7 +472,7 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod $sourceTablePrefix = $backReferences ? 'd' : 's'; $destinationTablePrefix = $backReferences ? 's' : 'd'; $queryBuilder = $this->createQueryBuilder() - ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.name, {$destinationTablePrefix}h.subtreetags, r.name AS referencename, r.properties AS referenceproperties") + ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.subtreetags, r.name AS referencename, r.properties AS referenceproperties") ->from($this->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') @@ -544,7 +544,7 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId, FindAncestorNodesFilter|CountAncestorNodesFilter|FindClosestNodeFilter $filter): array { $queryBuilderInitial = $this->createQueryBuilder() - ->select('n.*, ph.name, ph.subtreetags, ph.parentnodeanchor') + ->select('n.*, ph.subtreetags, ph.parentnodeanchor') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') @@ -559,7 +559,7 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId $this->addSubtreeTagConstraints($queryBuilderInitial, 'ch'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('pn.*, h.name, h.subtreetags, h.parentnodeanchor') + ->select('pn.*, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') ->innerJoin('cn', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') @@ -581,7 +581,7 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate { $queryBuilderInitial = $this->createQueryBuilder() // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation - ->select('n.*, h.name, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') @@ -595,7 +595,7 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('cn.*, h.name, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') + ->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') ->from('tree', 'pn') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = h.childnodeanchor') diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index f41e6591b88..2c7f005bd23 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -72,7 +72,7 @@ public function findParentNode( : $originDimensionSpacePoint->hash ]; $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT p.*, ph.contentstreamid, ph.name, ph.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNames->node() . ' p + '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 @@ -103,7 +103,7 @@ public function findNodeInAggregate( DimensionSpacePoint $coveredDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNames->node() . ' n + '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 @@ -556,7 +556,6 @@ protected function mapRawDataToHierarchyRelation(array $rawData): HierarchyRelat return new HierarchyRelation( NodeRelationAnchorPoint::fromInteger((int)$rawData['parentnodeanchor']), NodeRelationAnchorPoint::fromInteger((int)$rawData['childnodeanchor']), - $rawData['name'] ? NodeName::fromString($rawData['name']) : null, ContentStreamId::fromString($rawData['contentstreamid']), DimensionSpacePoint::fromJsonString($dimensionspacepointRaw), $rawData['dimensionspacepointhash'], diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index 9fd82b24be9..1997347d513 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -42,7 +42,7 @@ public function __construct( public function buildBasicNodeAggregateQuery(): QueryBuilder { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNames->node(), 'n') ->innerJoin('n', $this->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') @@ -54,7 +54,7 @@ public function buildBasicNodeAggregateQuery(): QueryBuilder public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder { return $this->createQueryBuilder() - ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('cn.*, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNames->node(), 'pn') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') @@ -86,7 +86,7 @@ public function buildFindRootNodeAggregatesQuery(ContentStreamId $contentStreamI return $queryBuilder; } - public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.name, h.subtreetags'): QueryBuilder + public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.subtreetags'): QueryBuilder { return $this->createQueryBuilder() ->select($select) @@ -99,7 +99,7 @@ public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionS public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() - ->select('n.*, h.name, h.subtreetags') + ->select('n.*, h.subtreetags') ->from($this->tableNames->node(), 'pn') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->node(), 'n', 'h.childnodeanchor = n.relationanchorpoint') @@ -111,7 +111,7 @@ public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() - ->select('pn.*, ch.name, ch.subtreetags') + ->select('pn.*, ch.subtreetags') ->from($this->tableNames->node(), 'pn') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->node(), 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php index 1ff5fbb1dbc..678b3d69adc 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/ContentHypergraph.php @@ -221,13 +221,10 @@ public function findChildNodeAggregates( ); } - /** - * @return iterable - */ - public function findChildNodeAggregatesByName( + public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable { + ): ?NodeAggregate { $query = HypergraphChildQuery::create( $this->contentStreamId, $parentNodeAggregateId, @@ -237,7 +234,7 @@ public function findChildNodeAggregatesByName( $nodeRows = $query->execute($this->getDatabaseConnection())->fetchAllAssociative(); - return $this->nodeFactory->mapNodeRowsToNodeAggregates( + return $this->nodeFactory->mapNodeRowsToNodeAggregate( $nodeRows, VisibilityConstraints::withoutRestrictions() ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/01-CreateNodeAggregateWithNode_ConstraintChecks.feature index d62848644d9..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 @@ -152,7 +152,7 @@ Feature: Create node aggregate with node | parentNodeAggregateId | "lady-eleonode-rootford" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" Scenario: Try to create a node aggregate with a property the node type does not declare When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/02-NodeCreation/02-CreateNodeAggregateWithNode_ConstraintChecks_WithDimensions.feature index 42b2c287de4..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': @@ -54,16 +54,35 @@ Feature: Create node aggregate with node | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "lady-eleonode-rootford" | - | originDimensionSpacePoint | {"language":"gsw"} | + | originDimensionSpacePoint | {"example":"spec"} | And the graph projection is fully up to date And the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "sir-david-nodenborough" | - | originDimensionSpacePoint | {"language":"de"} | + | originDimensionSpacePoint | {"example":"source"} | Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint" + Scenario: Try to create a node aggregate using a name that is already partially covered by one of its siblings + Given the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"example":"peer"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + And the graph projection is fully up to date + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | nodeName | "document" | + + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + Scenario: Try to create a node aggregate with a root parent and a sibling already claiming the name # root nodes are special in that they have the empty DSP as origin, wich may affect constraint checks When the command CreateNodeAggregateWithNode is executed with payload: @@ -71,7 +90,7 @@ Feature: Create node aggregate with node | nodeAggregateId | "sir-david-nodenborough" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "lady-eleonode-rootford" | - | originDimensionSpacePoint | {"language":"de"} | + | originDimensionSpacePoint | {"example":"source"} | | nodeName | "document" | And the graph projection is fully up to date And the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: @@ -79,6 +98,47 @@ Feature: Create node aggregate with node | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Node" | | parentNodeAggregateId | "lady-eleonode-rootford" | - | originDimensionSpacePoint | {"language":"de"} | + | originDimensionSpacePoint | {"example":"source"} | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" + + Scenario: Try to create a node aggregate using a name of a not yet existent, tethered child of the parent + Given the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-david-nodenborough" | + | nodeTypeName | "Neos.ContentRepository.Testing:Node" | + | parentNodeAggregateId | "lady-eleonode-rootford" | + | originDimensionSpacePoint | {"example":"source"} | + And the graph projection is fully up to date + Given I change the node types in content repository "default" to: + """yaml + 'Neos.ContentRepository.Testing:LeafNode': {} + 'Neos.ContentRepository.Testing:Node': + childNodes: + tethered: + type: 'Neos.ContentRepository.Testing:LeafNode' + properties: + postalAddress: + type: 'Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress' + 'Neos.ContentRepository.Testing:NodeWithInvalidPropertyType': + properties: + postalAddress: + type: '\I\Do\Not\Exist' + 'Neos.ContentRepository.Testing:NodeWithInvalidDefaultValue': + properties: + postalAddress: + type: 'Neos\ContentRepository\Core\Tests\Behavior\Fixtures\PostalAddress' + defaultValue: + iDoNotExist: 'whatever' + 'Neos.ContentRepository.Testing:AbstractNode': + abstract: true + """ + # We don't run structure adjustments here on purpose + When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: + | Key | Value | + | nodeAggregateId | "nody-mc-nodeface" | + | nodeTypeName | "Neos.ContentRepository.Testing:LeafNode" | + | parentNodeAggregateId | "sir-david-nodenborough" | + | originDimensionSpacePoint | {"example":"source"} | + | nodeName | "tethered" | + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature index 5aa384d386b..cfb744ffc71 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/01-MoveNodes_ConstraintChecks.feature @@ -163,18 +163,56 @@ Feature: Move node to a new parent / within the current parent before a sibling Then the last command should have thrown an exception of type "NodeAggregateDoesCurrentlyNotCoverDimensionSpacePointSet" Scenario: Using the scatter strategy, try to move a node to a parent that already has a child node of the same name - Given the following CreateNodeAggregateWithNode commands are executed: + Given the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "sir-nodeward-nodington-iii" | + | sourceOrigin | {"example": "source"} | + | targetOrigin | {"example": "peer"} | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | - | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | document | + | nody-mc-nodeface | {"example": "peer"} | Neos.ContentRepository.Testing:Document | sir-nodeward-nodington-iii | document | When the command MoveNodeAggregate is executed with payload and exceptions are caught: | Key | Value | - | dimensionSpacePoint | {"example": "source"} | + | dimensionSpacePoint | {"example": "peer"} | | nodeAggregateId | "nody-mc-nodeface" | | newParentNodeAggregateId | "lady-eleonode-rootford" | | relationDistributionStrategy | "scatter" | 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 | @@ -190,9 +228,9 @@ Feature: Move node to a new parent / within the current parent before a sibling Scenario: Using the gatherAll strategy, try to move a node to a parent that already has a child node of the same name in a generalization Given the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | - | rival-destinode | {"example": "general"} | Neos.ContentRepository.Testing:Document | general-nodesworth | target-document | - | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | nodimus-prime | target-document | + | nodeAggregateId | originDimensionSpacePoint | nodeTypeName | parentNodeAggregateId | nodeName | + | rival-destinode | {"example": "general"} | Neos.ContentRepository.Testing:Document | general-nodesworth | target-document | + | nody-mc-nodeface | {"example": "source"} | Neos.ContentRepository.Testing:Document | nodimus-prime | target-document | # Remove the node with the conflicting name in all variants except the generalization And the command RemoveNodeAggregate is executed with payload: | Key | Value | @@ -208,11 +246,11 @@ Feature: Move node to a new parent / within the current parent before a sibling And the graph projection is fully up to date When the command MoveNodeAggregate is executed with payload and exceptions are caught: - | Key | Value | - | dimensionSpacePoint | {"example": "source"} | - | nodeAggregateId | "nody-mc-nodeface" | - | newParentNodeAggregateId | "general-nodesworth" | - | relationDistributionStrategy | "gatherAll" | + | Key | Value | + | dimensionSpacePoint | {"example": "source"} | + | nodeAggregateId | "nody-mc-nodeface" | + | newParentNodeAggregateId | "general-nodesworth" | + | relationDistributionStrategy | "gatherAll" | Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" Scenario: Try to move a node to a parent whose node type does not allow child nodes of the node's type @@ -267,7 +305,7 @@ Feature: Move node to a new parent / within the current parent before a sibling Scenario: Try to move existing node after a node which is not a child of the new parent When the command MoveNodeAggregate is executed with payload and exceptions are caught: | Key | Value | - | dimensionSpacePoint | {"example": "spec"} | + | dimensionSpacePoint | {"example": "spec"} | | nodeAggregateId | "sir-david-nodenborough" | | newParentNodeAggregateId | "anthony-destinode" | | newPrecedingSiblingNodeAggregateId | "sir-nodeward-nodington-iii" | @@ -295,7 +333,7 @@ Feature: Move node to a new parent / within the current parent before a sibling Scenario: Try to move existing node before a node which is not a child of the new parent When the command MoveNodeAggregate is executed with payload and exceptions are caught: | Key | Value | - | dimensionSpacePoint | {"example": "spec"} | + | dimensionSpacePoint | {"example": "spec"} | | nodeAggregateId | "sir-david-nodenborough" | | newParentNodeAggregateId | "anthony-destinode" | | newSucceedingSiblingNodeAggregateId | "sir-nodeward-nodington-iii" | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature index d869cae7195..e3ac3759a33 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/06-AdditionalConstraintChecks.feature @@ -60,20 +60,20 @@ Feature: Additional constraint checks after move node capabilities are introduce | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | parentNodeAggregateId | "sir-nodeward-nodington-iii" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | parentNodeAggregateId | "lady-abigail-nodenborough" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" When the command CreateNodeAggregateWithNode is executed with payload and exceptions are caught: | Key | Value | | nodeAggregateId | "nody-mc-nodeface" | | nodeTypeName | "Neos.ContentRepository.Testing:Document" | | parentNodeAggregateId | "general-nodesworth" | | nodeName | "document" | - Then the last command should have thrown an exception of type "NodeNameIsAlreadyOccupied" + Then the last command should have thrown an exception of type "NodeNameIsAlreadyCovered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/01_ChangeNodeAggregateName_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/09-NodeRenaming/01-ChangeNodeAggregateName_ConstraintChecks.feature similarity index 51% 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 94856b0145f..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 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: @@ -44,6 +46,16 @@ Feature: Change node name | newNodeName | "new-name" | 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: | Key | Value | @@ -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/NodeRenaming/ChangeNodeAggregateName.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRenaming/ChangeNodeAggregateName.feature index 52b9f0e276d..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 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 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 3671ab9210b..244892ae790 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeRetyping/ChangeNodeAggregateType_BasicErrorCases.feature @@ -135,7 +135,7 @@ Feature: Change node aggregate type - basic error cases | strategy | "happypath" | Then the last command should have thrown an exception of type "NodeConstraintException" - Scenario: Try to change the node type of an auto created child node to anything other than defined: + Scenario: Try to change the node type of an tethered child node: When the command CreateNodeAggregateWithNodeAndSerializedProperties is executed with payload: | Key | Value | | nodeAggregateId | "parent2-na" | @@ -152,4 +152,4 @@ Feature: Change node aggregate type - basic error cases | nodeAggregateId | "nody-mc-nodeface" | | newNodeTypeName | "Neos.ContentRepository.Testing:ParentNodeType" | | strategy | "happypath" | - Then the last command should have thrown an exception of type "NodeConstraintException" + Then the last command should have thrown an exception of type "NodeAggregateIsTethered" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index de114fdd5f1..4487f84325f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -117,7 +117,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command "ChangeNodeAggregateName" is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | newNodeName | "a-renamed" | And the graph projection is fully up to date @@ -131,11 +131,59 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la | created | originalCreated | lastModified | originalLastModified | | 2023-03-16 12:30:00 | 2023-03-16 12:30:00 | 2023-03-16 13:00:00 | 2023-03-16 13:00:00 | + Scenario: NodeAggregateNameWasChanged events update last modified timestamps only in the user workspace + When the current date and time is "2023-03-16T13:00:00+01:00" + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + And the graph projection is fully up to date + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "review" | + And the graph projection is fully up to date + And the current date and time is "2023-03-16T14:00:00+01:00" + And the command "ChangeNodeAggregateName" is executed with payload: + | Key | Value | + | workspaceName | "user-test" | + | nodeAggregateId | "a" | + | newNodeName | "a-renamed" | + And the graph projection is fully up to date + + And I am in 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" | @@ -161,7 +209,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command ChangeNodeAggregateType was published with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | nodeAggregateId | "a" | | newNodeTypeName | "Neos.ContentRepository.Testing:SpecialPage" | | strategy | "happypath" | @@ -217,7 +265,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command MoveNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | dimensionSpacePoint | {"language": "ch"} | | relationDistributionStrategy | "gatherSpecializations" | | nodeAggregateId | "a" | @@ -253,7 +301,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T13:00:00+01:00" And the command DisableNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | coveredDimensionSpacePoint | {"language": "ch"} | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | @@ -272,7 +320,7 @@ Feature: Behavior of Node timestamp properties "created", "originalCreated", "la When the current date and time is "2023-03-16T14:00:00+01:00" And the command EnableNodeAggregate is executed with payload: | Key | Value | - | workspaceName | "user-test" | + | workspaceName | "user-test" | | coveredDimensionSpacePoint | {"language": "ch"} | | nodeAggregateId | "a" | | nodeVariantSelectionStrategy | "allSpecializations" | @@ -292,7 +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 diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 01635643acb..5674559e866 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -48,7 +48,6 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConstraintException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyCovered; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsAbstract; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsNotOfTypeRoot; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsOfTypeRoot; @@ -221,6 +220,17 @@ protected function requireNodeTypeToDeclareReference(NodeTypeName $nodeTypeName, throw ReferenceCannotBeSet::becauseTheNodeTypeDoesNotDeclareIt($referenceName, $nodeTypeName); } + protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void + { + $nodeType = $this->requireNodeType($nodeTypeName); + if ($nodeType->hasTetheredNode($nodeName)) { + throw new NodeNameIsAlreadyCovered( + 'Node name "' . $nodeName->value . '" is reserved for a tethered child of parent node aggregate of type "' + . $nodeTypeName->value . '".' + ); + } + } + protected function requireNodeTypeToAllowNodesOfTypeInReference( NodeTypeName $nodeTypeName, ReferenceName $referenceName, @@ -257,19 +267,13 @@ protected function requireNodeTypeToAllowNumberOfReferencesInReference(Serialize } /** - * NodeType and NodeName must belong together to the same node, which is the to-be-checked one. - * - * @param ContentGraphInterface $contentGraph - * @param NodeType $nodeType - * @param NodeName|null $nodeName * @param array|NodeAggregateId[] $parentNodeAggregateIds * @throws NodeConstraintException */ protected function requireConstraintsImposedByAncestorsAreMet( ContentGraphInterface $contentGraph, NodeType $nodeType, - ?NodeName $nodeName, - array $parentNodeAggregateIds + array $parentNodeAggregateIds, ): void { foreach ($parentNodeAggregateIds as $parentNodeAggregateId) { $parentAggregate = $this->requireProjectedNodeAggregate( @@ -279,7 +283,7 @@ protected function requireConstraintsImposedByAncestorsAreMet( if (!$parentAggregate->classification->isTethered()) { try { $parentsNodeType = $this->requireNodeType($parentAggregate->nodeTypeName); - $this->requireNodeTypeConstraintsImposedByParentToBeMet($parentsNodeType, $nodeName, $nodeType); + $this->requireNodeTypeConstraintsImposedByParentToBeMet($parentsNodeType, $nodeType); } catch (NodeTypeNotFound $e) { // skip constraint check; Once the parent is changed to be of an available type, // the constraint checks are executed again. See handleChangeNodeAggregateType @@ -313,7 +317,6 @@ protected function requireConstraintsImposedByAncestorsAreMet( */ protected function requireNodeTypeConstraintsImposedByParentToBeMet( NodeType $parentsNodeType, - ?NodeName $nodeName, NodeType $nodeType ): void { // !!! IF YOU ADJUST THIS METHOD, also adjust the method below. @@ -324,37 +327,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; } @@ -602,35 +584,6 @@ protected function requireNodeAggregateToBeChild( ); } - /** - * @throws NodeNameIsAlreadyOccupied - */ - protected function requireNodeNameToBeUnoccupied( - ContentGraphInterface $contentGraph, - ?NodeName $nodeName, - NodeAggregateId $parentNodeAggregateId, - OriginDimensionSpacePoint $parentOriginDimensionSpacePoint, - DimensionSpacePointSet $dimensionSpacePoints - ): void { - if ($nodeName === null) { - return; - } - $dimensionSpacePointsOccupiedByChildNodeName = $contentGraph - ->getDimensionSpacePointsOccupiedByChildNodeName( - $nodeName, - $parentNodeAggregateId, - $parentOriginDimensionSpacePoint, - $dimensionSpacePoints - ); - if (count($dimensionSpacePointsOccupiedByChildNodeName) > 0) { - throw new NodeNameIsAlreadyOccupied( - 'Child node name "' . $nodeName->value . '" is already occupied for parent "' - . $parentNodeAggregateId->value . '" in dimension space points ' - . $dimensionSpacePointsOccupiedByChildNodeName->toJson() - ); - } - } - /** * @throws NodeNameIsAlreadyCovered */ @@ -638,27 +591,20 @@ protected function requireNodeNameToBeUncovered( ContentGraphInterface $contentGraph, ?NodeName $nodeName, NodeAggregateId $parentNodeAggregateId, - DimensionSpacePointSet $dimensionSpacePointsToBeCovered ): void { if ($nodeName === null) { return; } - $childNodeAggregates = $contentGraph->findChildNodeAggregatesByName( + $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 . '".' + ); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php index abe3e5b73ef..0a8f10ce699 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/TetheredNodeInternals.php @@ -62,19 +62,12 @@ protected function createEventsForMissingTetheredNode( ?NodeAggregateId $tetheredNodeAggregateId, NodeType $expectedTetheredNodeType ): Events { - $childNodeAggregates = $contentGraph->findChildNodeAggregatesByName( + $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)) { @@ -129,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.' @@ -152,11 +143,6 @@ protected function createEventsForMissingTetheredNode( $originDimensionSpacePoint, $parentNodeAggregate ); - } else { - throw new \RuntimeException( - 'There is >= 2 ChildNodeAggregates with the same name reachable from the parent' . - '- this is ambiguous and we should analyze how this may happen. That is very likely a bug.' - ); } } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index 35c9a8589b9..1c38334ff2d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -63,6 +63,8 @@ abstract protected function requireNodeTypeToNotBeAbstract(NodeType $nodeType): abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): void; + abstract protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void; + abstract protected function getPropertyConverter(): PropertyConverter; abstract protected function getNodeTypeManager(): NodeTypeManager; @@ -139,7 +141,6 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $this->requireConstraintsImposedByAncestorsAreMet( $contentGraph, $nodeType, - $command->nodeName, [$command->parentNodeAggregateId] ); } @@ -168,15 +169,12 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( $parentNodeAggregate->coveredDimensionSpacePoints ); if ($command->nodeName) { - $this->requireNodeNameToBeUnoccupied( + $this->requireNodeNameToBeUncovered( $contentGraph, $command->nodeName, $command->parentNodeAggregateId, - $parentNodeAggregate->classification->isRoot() - ? DimensionSpace\OriginDimensionSpacePoint::createWithoutDimensions() - : $command->originDimensionSpacePoint, - $coveredDimensionSpacePoints, ); + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($parentNodeAggregate->nodeTypeName, $command->nodeName); } $descendantNodeAggregateIds = $command->tetheredDescendantNodeAggregateIds->completeForNodeOfType( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php index b538bb99f6a..33452d7be36 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDuplication/NodeDuplicationCommandHandler.php @@ -99,7 +99,6 @@ private function handleCopyNodesRecursively( $this->requireConstraintsImposedByAncestorsAreMet( $contentGraph, $nodeType, - $command->targetNodeName, [$command->targetParentNodeAggregateId] ); @@ -134,16 +133,12 @@ private function handleCopyNodesRecursively( $parentNodeAggregate->coveredDimensionSpacePoints ); - // Constraint: The node name must be free in all these dimension space points + // Constraint: The node name must be free for a new child of the parent node aggregate if ($command->targetNodeName) { - $this->requireNodeNameToBeUnoccupied( + $this->requireNodeNameToBeUncovered( $contentGraph, $command->targetNodeName, $command->targetParentNodeAggregateId, - $parentNodeAggregate->classification->isRoot() - ? OriginDimensionSpacePoint::createWithoutDimensions() - : $command->targetDimensionSpacePoint, - $coveredDimensionSpacePoints, ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php index 1869aca61de..055407245ee 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeMove/NodeMove.php @@ -28,6 +28,7 @@ use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\RelationDistributionStrategy; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; @@ -41,6 +42,7 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateIsNoSibling; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregatesTypeIsAmbiguous; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; /** * @internal implementation detail of Command Handlers @@ -51,6 +53,8 @@ abstract protected function getInterDimensionalVariationGraph(): DimensionSpace\ abstract protected function areAncestorNodeTypeConstraintChecksEnabled(): bool; + abstract protected function requireNodeTypeNotToDeclareTetheredChildNodeName(NodeTypeName $nodeTypeName, NodeName $nodeName): void; + abstract protected function requireProjectedNodeAggregate( ContentGraphInterface $contentGraph, NodeAggregateId $nodeAggregateId, @@ -105,7 +109,6 @@ private function handleMoveNodeAggregate( $this->requireConstraintsImposedByAncestorsAreMet( $contentGraph, $this->requireNodeType($nodeAggregate->nodeTypeName), - $nodeAggregate->nodeName, [$command->newParentNodeAggregateId], ); @@ -118,10 +121,10 @@ private function handleMoveNodeAggregate( $contentGraph, $nodeAggregate->nodeName, $command->newParentNodeAggregateId, - // We need to check all covered DSPs of the parent node aggregate to prevent siblings - // with different node aggregate IDs but the same name - $newParentNodeAggregate->coveredDimensionSpacePoints, ); + if ($nodeAggregate->nodeName) { + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($newParentNodeAggregate->nodeTypeName, $nodeAggregate->nodeName); + } $this->requireNodeAggregateToCoverDimensionSpacePoints( $newParentNodeAggregate, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php index 930d1294f1d..547a0693554 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeRenaming/NodeRenaming.php @@ -33,6 +33,7 @@ trait NodeRenaming private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { $contentGraph = $commandHandlingDependencies->getContentGraph($command->workspaceName); + $this->requireContentStream($command->workspaceName, $commandHandlingDependencies); $expectedVersion = $this->getExpectedVersionOfContentStream($contentGraph->getContentStreamId(), $commandHandlingDependencies); $nodeAggregate = $this->requireProjectedNodeAggregate( $contentGraph, @@ -41,15 +42,12 @@ private function handleChangeNodeAggregateName(ChangeNodeAggregateName $command, $this->requireNodeAggregateToNotBeRoot($nodeAggregate, 'and Root Node Aggregates cannot be renamed'); $this->requireNodeAggregateToBeUntethered($nodeAggregate); foreach ($contentGraph->findParentNodeAggregates($command->nodeAggregateId) as $parentNodeAggregate) { - foreach ($parentNodeAggregate->occupiedDimensionSpacePoints as $occupiedParentDimensionSpacePoint) { - $this->requireNodeNameToBeUnoccupied( - $contentGraph, - $command->newNodeName, - $parentNodeAggregate->nodeAggregateId, - $occupiedParentDimensionSpacePoint, - $parentNodeAggregate->coveredDimensionSpacePoints - ); - } + $this->requireNodeNameToBeUncovered( + $contentGraph, + $command->newNodeName, + $parentNodeAggregate->nodeAggregateId, + ); + $this->requireNodeTypeNotToDeclareTetheredChildNodeName($parentNodeAggregate->nodeTypeName, $command->newNodeName); } $events = Events::with( diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index 341d64437a1..39219ddfd1b 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -50,6 +50,8 @@ trait NodeTypeChange { abstract protected function getNodeTypeManager(): NodeTypeManager; + abstract protected function requireNodeAggregateToBeUntethered(NodeAggregate $nodeAggregate): void; + abstract protected function requireProjectedNodeAggregate( ContentGraphInterface $contentRepository, NodeAggregateId $nodeAggregateId @@ -58,19 +60,16 @@ abstract protected function requireProjectedNodeAggregate( abstract protected function requireConstraintsImposedByAncestorsAreMet( ContentGraphInterface $contentGraph, NodeType $nodeType, - ?NodeName $nodeName, 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; @@ -117,6 +116,7 @@ private function handleChangeNodeAggregateType( $contentGraph, $command->nodeAggregateId ); + $this->requireNodeAggregateToBeUntethered($nodeAggregate); // node type detail checks $this->requireNodeTypeToNotBeOfTypeRoot($newNodeType); @@ -132,7 +132,6 @@ private function handleChangeNodeAggregateType( $this->requireConstraintsImposedByAncestorsAreMet( $contentGraph, $newNodeType, - $nodeAggregate->nodeName, [$parentNodeAggregate->nodeAggregateId] ); } @@ -244,7 +243,6 @@ private function requireConstraintsImposedByHappyPathStrategyAreMet( // so we use $newNodeType (the target node type of $node after the operation) here. $this->requireNodeTypeConstraintsImposedByParentToBeMet( $newNodeType, - $childNodeAggregate->nodeName, $this->requireNodeType($childNodeAggregate->nodeTypeName) ); @@ -292,7 +290,6 @@ private function deleteDisallowedNodesWhenChangingNodeType( !$childNodeAggregate->classification->isTethered() && !$this->areNodeTypeConstraintsImposedByParentValid( $newNodeType, - $childNodeAggregate->nodeName, $this->requireNodeType($childNodeAggregate->nodeTypeName) ) ) { diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php index 5ecb1bd025d..4913da031cf 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ContentGraphInterface.php @@ -115,16 +115,14 @@ public function findChildNodeAggregates( ): iterable; /** - * A node aggregate may have multiple child node aggregates with the same name - * as long as they do not share dimension space coverage + * A node aggregate can have no or exactly one child node aggregate with a given name as enforced by constraint checks * - * @return iterable * @internal only for consumption inside the Command Handler */ - public function findChildNodeAggregatesByName( + public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name - ): iterable; + ): ?NodeAggregate; /** * @return iterable diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php index e89b8aec790..48250a8107c 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php @@ -46,7 +46,7 @@ * @param NodeTypeName $nodeTypeName The node's node type name; always set, even if unknown to the NodeTypeManager * @param NodeType|null $nodeType The node's node type, null if unknown to the NodeTypeManager - @deprecated Don't rely on this too much, as the capabilities of the NodeType here will probably change a lot; Ask the {@see NodeTypeManager} instead * @param PropertyCollection $properties All properties of this node. References are NOT part of this API; To access references, {@see ContentSubgraphInterface::findReferences()} can be used; To read the serialized properties use {@see PropertyCollection::serialized()}. - * @param NodeName|null $nodeName The optionally named hierarchy relation to the node's parent. + * @param NodeName|null $nodeName The optional name of the node, describing its relation to its parent * @param NodeTags $tags explicit and inherited SubtreeTags of this node * @param Timestamps $timestamps Creation and modification timestamps of this node */ diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php b/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php deleted file mode 100644 index a457d6d1fa9..00000000000 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Exception/NodeNameIsAlreadyOccupied.php +++ /dev/null @@ -1,23 +0,0 @@ -quit(1); - } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyOccupied $exception) { + } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyCovered $exception) { $this->outputLine('A site with siteNodeName "%s" already exists', [$nodeName ?: $name]); $this->quit(1); } diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 5dca97ed900..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; @@ -204,15 +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($workspace->workspaceName)->findChildNodeAggregatesByName( + $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, @@ -414,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/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index 33de72e820b..ae5d75bb1e5 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -60,14 +60,11 @@ public function removeSiteNode(SiteNodeName $siteNodeName): void $sitesNodeAggregate = $contentGraph->findRootNodeAggregateByType( NodeTypeNameFactory::forSites() ); - - $siteNodeAggregates = $contentGraph->findChildNodeAggregatesByName( + $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, @@ -98,12 +95,12 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); } - $contentGraph = $this->contentRepository->getContentGraph($liveWorkspace->workspaceName); - $siteNodeAggregate = $contentGraph->findChildNodeAggregatesByName( - $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; }