diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index 258d58d8b3f..fa22da92ede 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -19,7 +19,11 @@ use Doctrine\DBAL\Exception\InvalidArgumentException; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\SharedModel\Id\UuidFactory; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; @@ -64,53 +68,24 @@ public function setupDbalGraphAdapterIntegrityViolationTrait() } /** - * @When /^I remove the following restriction relation:$/ + * @When /^I remove the following subtree tag:$/ * @param TableNode $payloadTable * @throws DBALException * @throws InvalidArgumentException */ - public function iRemoveTheFollowingRestrictionRelation(TableNode $payloadTable): void + public function iRemoveTheFollowingSubtreeTag(TableNode $payloadTable): void { $dataset = $this->transformPayloadTableToDataset($payloadTable); - $record = $this->transformDatasetToRestrictionRelationRecord($dataset); - - $this->dbalClient->getConnection()->delete( - $this->getTableNamePrefix() . '_restrictionrelation', - $record - ); - } - - /** - * @When /^I detach the following restriction relation from its origin:$/ - * @param TableNode $payloadTable - * @throws DBALException - */ - public function iDetachTheFollowingRestrictionRelationFromItsOrigin(TableNode $payloadTable): void - { - $dataset = $this->transformPayloadTableToDataset($payloadTable); - $record = $this->transformDatasetToRestrictionRelationRecord($dataset); - $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_restrictionrelation', - [ - 'originnodeaggregateid' => (string)TestingNodeAggregateId::nonExistent() - ], - $record - ); - } - - /** - * @When /^I detach the following restriction relation from its target:$/ - * @param TableNode $payloadTable - * @throws DBALException - */ - public function iDetachTheFollowingRestrictionRelationFromItsTarget(TableNode $payloadTable): void - { - $dataset = $this->transformPayloadTableToDataset($payloadTable); - $record = $this->transformDatasetToRestrictionRelationRecord($dataset); + $subtreeTagToRemove = SubtreeTag::fromString($dataset['subtreeTag']); + $record = $this->transformDatasetToHierarchyRelationRecord($dataset); + $subtreeTags = NodeFactory::extractNodeTagsFromJson($record['subtreetags']); + if (!$subtreeTags->contain($subtreeTagToRemove)) { + throw new \RuntimeException(sprintf('Failed to remove subtree tag "%s" because that tag is not set', $subtreeTagToRemove->value), 1708618267); + } $this->dbalClient->getConnection()->update( - $this->getTableNamePrefix() . '_restrictionrelation', + $this->getTableNamePrefix() . '_hierarchyrelation', [ - 'affectednodeaggregateid' => (string)TestingNodeAggregateId::nonExistent() + 'subtreetags' => json_encode($subtreeTags->without($subtreeTagToRemove), JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), ], $record ); @@ -234,52 +209,45 @@ private function transformDatasetToReferenceRelationRecord(array $dataset): arra { return [ 'name' => $dataset['referenceName'], - 'nodeanchorpoint' => $this->findRelationAnchorPointByIds( + 'nodeanchorpoint' => $this->findHierarchyRelationByIds( ContentStreamId::fromString($dataset['contentStreamId']), DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']), NodeAggregateId::fromString($dataset['sourceNodeAggregateId']) - ), + )['childnodeanchor'], 'destinationnodeaggregateid' => $dataset['destinationNodeAggregateId'] ]; } - private function transformDatasetToRestrictionRelationRecord(array $dataset): array - { - $dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']); - - return [ - 'contentstreamid' => $dataset['contentStreamId'], - 'dimensionspacepointhash' => $dimensionSpacePoint->hash, - 'originnodeaggregateid' => $dataset['originNodeAggregateId'], - 'affectednodeaggregateid' => $dataset['affectedNodeAggregateId'], - ]; - } - private function transformDatasetToHierarchyRelationRecord(array $dataset): array { $dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']); $parentNodeAggregateId = TestingNodeAggregateId::fromString($dataset['parentNodeAggregateId']); $childAggregateId = TestingNodeAggregateId::fromString($dataset['childNodeAggregateId']); + $parentHierarchyRelation = $parentNodeAggregateId->isNonExistent() + ? null + : $this->findHierarchyRelationByIds( + ContentStreamId::fromString($dataset['contentStreamId']), + $dimensionSpacePoint, + NodeAggregateId::fromString($dataset['parentNodeAggregateId']) + ); + + $childHierarchyRelation = $childAggregateId->isNonExistent() + ? null + : $this->findHierarchyRelationByIds( + ContentStreamId::fromString($dataset['contentStreamId']), + $dimensionSpacePoint, + NodeAggregateId::fromString($dataset['childNodeAggregateId']) + ); + return [ 'contentstreamid' => $dataset['contentStreamId'], 'dimensionspacepoint' => $dimensionSpacePoint->toJson(), 'dimensionspacepointhash' => $dimensionSpacePoint->hash, - 'parentnodeanchor' => $parentNodeAggregateId->isNonExistent() - ? 9999999 - : $this->findRelationAnchorPointByIds( - ContentStreamId::fromString($dataset['contentStreamId']), - $dimensionSpacePoint, - NodeAggregateId::fromString($dataset['parentNodeAggregateId']) - ), - 'childnodeanchor' => $childAggregateId->isNonExistent() - ? 8888888 - : $this->findRelationAnchorPointByIds( - ContentStreamId::fromString($dataset['contentStreamId']), - $dimensionSpacePoint, - NodeAggregateId::fromString($dataset['childNodeAggregateId']) - ), - 'position' => $dataset['position'] ?? 0 + 'parentnodeanchor' => $parentHierarchyRelation !== null ? $parentHierarchyRelation['childnodeanchor'] : 9999999, + 'childnodeanchor' => $childHierarchyRelation !== null ? $childHierarchyRelation['childnodeanchor'] : 8888888, + 'position' => $dataset['position'] ?? $parentHierarchyRelation !== null ? $parentHierarchyRelation['position'] : 0, + 'subtreetags' => $parentHierarchyRelation !== null ? $parentHierarchyRelation['subtreetags'] : '{}', ]; } @@ -287,34 +255,37 @@ private function findRelationAnchorPointByDataset(array $dataset): int { $dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['originDimensionSpacePoint'] ?? $dataset['dimensionSpacePoint']); - return $this->findRelationAnchorPointByIds( + return $this->findHierarchyRelationByIds( ContentStreamId::fromString($dataset['contentStreamId']), $dimensionSpacePoint, NodeAggregateId::fromString($dataset['nodeAggregateId'] ?? $dataset['childNodeAggregateId']) - ); + )['childnodeanchor']; } - private function findRelationAnchorPointByIds( + private function findHierarchyRelationByIds( ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, NodeAggregateId $nodeAggregateId - ): int { + ): array { $nodeRecord = $this->dbalClient->getConnection()->executeQuery( - 'SELECT n.relationanchorpoint - FROM ' . $this->getTableNamePrefix() . '_node n - INNER JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation h - ON n.relationanchorpoint = h.childnodeanchor - WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamid = :contentStreamId - AND h.dimensionspacepointhash = :dimensionSpacePointHash', + 'SELECT h.* + FROM ' . $this->getTableNamePrefix() . '_node n + INNER JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation h + ON n.relationanchorpoint = h.childnodeanchor + WHERE n.nodeaggregateid = :nodeAggregateId + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash = :dimensionSpacePointHash', [ 'contentStreamId' => $contentStreamId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, 'nodeAggregateId' => $nodeAggregateId->value ] )->fetchAssociative(); + if ($nodeRecord === false) { + throw new \InvalidArgumentException(sprintf('Failed to find hierarchy relation for content stream "%s", dimension space point "%s" and node aggregate id "%s"', $contentStreamId->value, $dimensionSpacePoint->hash, $nodeAggregateId->value), 1708617712); + } - return $nodeRecord['relationanchorpoint']; + return $nodeRecord; } private function transformPayloadTableToDataset(TableNode $payloadTable): array diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/RestrictionIntegrityIsProvided.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/RestrictionIntegrityIsProvided.feature deleted file mode 100644 index 0e368b7fc70..00000000000 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/RestrictionIntegrityIsProvided.feature +++ /dev/null @@ -1,75 +0,0 @@ -@contentrepository -Feature: Run integrity violation detection regarding restriction relations - - As a user of the CR I want to know whether there are nodes with restriction relations missing from their ancestors - - Background: - Given using the following content dimensions: - | Identifier | Values | Generalizations | - | language | de, gsw, fr | gsw->de | - And using the following node types: - """yaml - 'Neos.ContentRepository.Testing:Document': [] - """ - And using identifier "default", I define a content repository - And I am in content repository "default" - And the command CreateRootWorkspace is executed with payload: - | Key | Value | - | workspaceName | "live" | - | workspaceTitle | "Live" | - | workspaceDescription | "The live workspace" | - | newContentStreamId | "cs-identifier" | - And the graph projection is fully up to date - And I am in the active content stream of workspace "live" and dimension space point {"language":"de"} - And the command CreateRootNodeAggregateWithNode is executed with payload: - | Key | Value | - | nodeAggregateId | "lady-eleonode-rootford" | - | nodeTypeName | "Neos.ContentRepository:Root" | - And the 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"},{"language":"gsw"},{"language":"fr"}] | - | parentNodeAggregateId | "lady-eleonode-rootford" | - | nodeName | "document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWithNodeWasCreated was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "nody-mc-nodeface" | - | nodeTypeName | "Neos.ContentRepository.Testing:Document" | - | originDimensionSpacePoint | {"language":"de"} | - | coveredDimensionSpacePoints | [{"language":"de"},{"language":"gsw"},{"language":"fr"}] | - | parentNodeAggregateId | "sir-david-nodenborough" | - | nodeName | "child-document" | - | nodeAggregateClassification | "regular" | - And the event NodeAggregateWasDisabled was published with payload: - | Key | Value | - | contentStreamId | "cs-identifier" | - | nodeAggregateId | "sir-david-nodenborough" | - | affectedDimensionSpacePoints | [{"language":"de"},{"language":"gsw"},{"language":"fr"}] | - And the graph projection is fully up to date - - Scenario: Detach a restriction relation from its origin - When I detach the following restriction relation from its origin: - | Key | Value | - | contentStreamId | "cs-identifier" | - | dimensionSpacePoint | {"language":"de"} | - | originNodeAggregateId | "sir-david-nodenborough" | - | affectedNodeAggregateId | "nody-mc-nodeface" | - And I run integrity violation detection - Then I expect the integrity violation detection result to contain exactly 1 error - And I expect integrity violation detection result error number 1 to have code 1597846598 - - Scenario: Detach a restriction relation from its target - When I detach the following restriction relation from its target: - | Key | Value | - | contentStreamId | "cs-identifier" | - | dimensionSpacePoint | {"language":"de"} | - | originNodeAggregateId | "sir-david-nodenborough" | - | affectedNodeAggregateId | "sir-david-nodenborough" | - And I run integrity violation detection - Then I expect the integrity violation detection result to contain exactly 1 error - And I expect integrity violation detection result error number 1 to have code 1597846598 diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/RestrictionsArePropagatedRecursively.feature b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SubtreeTagsAreInherited.feature similarity index 87% rename from Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/RestrictionsArePropagatedRecursively.feature rename to Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SubtreeTagsAreInherited.feature index b5c4e458f2c..1c424425d1f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/RestrictionsArePropagatedRecursively.feature +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Projection/ProjectionIntegrityViolationDetection/SubtreeTagsAreInherited.feature @@ -1,7 +1,7 @@ @contentrepository -Feature: Run integrity violation detection regarding restriction relations +Feature: Run integrity violation detection regarding subtree tag inheritance - As a user of the CR I want to know whether there are nodes with restriction relations missing from their ancestors + As a user of the CR I want to know whether there are nodes with subtree tags that are not inherited from its ancestors Background: Given using the following content dimensions: @@ -58,18 +58,20 @@ Feature: Run integrity violation detection regarding restriction relations | parentNodeAggregateId | "sir-nodeward-nodington-iii" | | nodeName | "child-document" | | nodeAggregateClassification | "regular" | - And the event NodeAggregateWasDisabled was published with payload: + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{"language":"de"},{"language":"gsw"},{"language":"fr"}] | + | tag | "disabled" | And the graph projection is fully up to date - And I remove the following restriction relation: - | Key | Value | - | contentStreamId | "cs-identifier" | - | dimensionSpacePoint | {"language":"de"} | - | originNodeAggregateId | "sir-david-nodenborough" | - | affectedNodeAggregateId | "nody-mc-nodeface" | + And I remove the following subtree tag: + | Key | Value | + | contentStreamId | "cs-identifier" | + | dimensionSpacePoint | {"language":"de"} | + | parentNodeAggregateId | "sir-nodeward-nodington-iii" | + | childNodeAggregateId | "nody-mc-nodeface" | + | subtreeTag | "disabled" | And I run integrity violation detection Then I expect the integrity violation detection result to contain exactly 1 error And I expect integrity violation detection result error number 1 to have code 1597837797 diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index b403ce81fa4..fbda6e190b5 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -7,11 +7,10 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Types\Types; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeDisabling; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeMove; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeRemoval; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeVariation; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\RestrictionRelations; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\SubtreeTagging; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; @@ -28,8 +27,6 @@ use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionShineThroughWasAdded; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasEnabled; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; @@ -43,12 +40,16 @@ use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateDimensionsWereUpdated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\Infrastructure\DbalCheckpointStorage; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\CheckpointStorageStatusType; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStatus; @@ -67,8 +68,7 @@ final class DoctrineDbalContentGraphProjection implements ProjectionInterface, WithMarkStaleInterface { use NodeVariation; - use NodeDisabling; - use RestrictionRelations; + use SubtreeTagging; use NodeRemoval; use NodeMove; @@ -174,7 +174,6 @@ private function truncateDatabaseTables(): void $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_node'); $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_hierarchyrelation'); $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_referencerelation'); - $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_restrictionrelation'); } public function canHandle(EventInterface $event): bool @@ -188,7 +187,6 @@ public function canHandle(EventInterface $event): bool ContentStreamWasRemoved::class, NodePropertiesWereSet::class, NodeReferencesWereSet::class, - NodeAggregateWasEnabled::class, NodeAggregateTypeWasChanged::class, DimensionSpacePointWasMoved::class, DimensionShineThroughWasAdded::class, @@ -197,7 +195,8 @@ public function canHandle(EventInterface $event): bool NodeSpecializationVariantWasCreated::class, NodeGeneralizationVariantWasCreated::class, NodePeerVariantWasCreated::class, - NodeAggregateWasDisabled::class + SubtreeWasTagged::class, + SubtreeWasUntagged::class, ]); } @@ -212,7 +211,6 @@ public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void ContentStreamWasRemoved::class => $this->whenContentStreamWasRemoved($event), NodePropertiesWereSet::class => $this->whenNodePropertiesWereSet($event, $eventEnvelope), NodeReferencesWereSet::class => $this->whenNodeReferencesWereSet($event, $eventEnvelope), - NodeAggregateWasEnabled::class => $this->whenNodeAggregateWasEnabled($event), NodeAggregateTypeWasChanged::class => $this->whenNodeAggregateTypeWasChanged($event, $eventEnvelope), DimensionSpacePointWasMoved::class => $this->whenDimensionSpacePointWasMoved($event), DimensionShineThroughWasAdded::class => $this->whenDimensionShineThroughWasAdded($event), @@ -221,7 +219,8 @@ public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void NodeSpecializationVariantWasCreated::class => $this->whenNodeSpecializationVariantWasCreated($event, $eventEnvelope), NodeGeneralizationVariantWasCreated::class => $this->whenNodeGeneralizationVariantWasCreated($event, $eventEnvelope), NodePeerVariantWasCreated::class => $this->whenNodePeerVariantWasCreated($event, $eventEnvelope), - NodeAggregateWasDisabled::class => $this->whenNodeAggregateWasDisabled($event), + SubtreeWasTagged::class => $this->whenSubtreeWasTagged($event), + SubtreeWasUntagged::class => $this->whenSubtreeWasUntagged($event), default => throw new \InvalidArgumentException(sprintf('Unsupported event %s', get_debug_type($event))), }; } @@ -348,13 +347,6 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre $event->nodeName, $eventEnvelope, ); - - $this->connectRestrictionRelationsFromParentNodeToNewlyCreatedNode( - $event->contentStreamId, - $event->parentNodeAggregateId, - $event->nodeAggregateId, - $event->coveredDimensionSpacePoints - ); }); } @@ -389,47 +381,6 @@ private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $ev }); } - /** - * Copy the restriction edges from the parent Node to the newly created child node; - * so that newly created nodes inherit the visibility constraints of the parent. - * @throws \Doctrine\DBAL\DBALException - */ - private function connectRestrictionRelationsFromParentNodeToNewlyCreatedNode( - ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId, - NodeAggregateId $newlyCreatedNodeAggregateId, - DimensionSpacePointSet $dimensionSpacePointsInWhichNewlyCreatedNodeAggregateIsVisible - ): void { - // TODO: still unsure why we need an "INSERT IGNORE" here; - // normal "INSERT" can trigger a duplicate key constraint exception - $this->getDatabaseConnection()->executeUpdate(' - INSERT IGNORE INTO ' . $this->tableNamePrefix . '_restrictionrelation ( - contentstreamid, - dimensionspacepointhash, - originnodeaggregateid, - affectednodeaggregateid - ) - SELECT - r.contentstreamid, - r.dimensionspacepointhash, - r.originnodeaggregateid, - "' . $newlyCreatedNodeAggregateId->value . '" as affectednodeaggregateid - FROM - ' . $this->tableNamePrefix . '_restrictionrelation r - WHERE - r.contentstreamid = :sourceContentStreamId - and r.dimensionspacepointhash IN (:visibleDimensionSpacePoints) - and r.affectednodeaggregateid = :parentNodeAggregateId - ', [ - 'sourceContentStreamId' => $contentStreamId->value, - 'visibleDimensionSpacePoints' => $dimensionSpacePointsInWhichNewlyCreatedNodeAggregateIsVisible - ->getPointHashes(), - 'parentNodeAggregateId' => $parentNodeAggregateId->value - ], [ - 'visibleDimensionSpacePoints' => Connection::PARAM_STR_ARRAY - ]); - } - /** * @throws \Doctrine\DBAL\DBALException */ @@ -525,6 +476,9 @@ private function connectHierarchy( $dimensionSpacePoint ); + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamId, $parentNodeAnchorPoint, $dimensionSpacePoint); + $inheritedSubtreeTags = NodeTags::create(SubtreeTags::createEmpty(), $parentSubtreeTags->all()); + $hierarchyRelation = new HierarchyRelation( $parentNodeAnchorPoint, $childNodeAnchorPoint, @@ -532,7 +486,8 @@ private function connectHierarchy( $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, - $position + $position, + $inheritedSubtreeTags, ); $hierarchyRelation->addToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); @@ -646,6 +601,7 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void position, dimensionspacepoint, dimensionspacepointhash, + subtreetags, contentstreamid ) SELECT @@ -655,6 +611,7 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void h.position, h.dimensionspacepoint, h.dimensionspacepointhash, + h.subtreetags, "' . $event->newContentStreamId->value . '" AS contentstreamid FROM ' . $this->tableNamePrefix . '_hierarchyrelation h @@ -663,28 +620,6 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void 'sourceContentStreamId' => $event->sourceContentStreamId->value ]); - // - // 2) copy Hidden Node information to second content stream - // - $this->getDatabaseConnection()->executeUpdate(' - INSERT INTO ' . $this->tableNamePrefix . '_restrictionrelation ( - contentstreamid, - dimensionspacepointhash, - originnodeaggregateid, - affectednodeaggregateid - ) - SELECT - "' . $event->newContentStreamId->value . '" AS contentstreamid, - r.dimensionspacepointhash, - r.originnodeaggregateid, - r.affectednodeaggregateid - FROM - ' . $this->tableNamePrefix . '_restrictionrelation r - WHERE r.contentstreamid = :sourceContentStreamId - ', [ - 'sourceContentStreamId' => $event->sourceContentStreamId->value - ]); - // NOTE: as reference edges are attached to Relation Anchor Points (and they are lazily copy-on-written), // we do not need to copy reference edges here (but we need to do it during copy on write). }); @@ -724,15 +659,6 @@ private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): vo = ' . $this->tableNamePrefix . '_referencerelation.nodeanchorpoint ) '); - - // Drop restriction relations - $this->getDatabaseConnection()->executeUpdate(' - DELETE FROM ' . $this->tableNamePrefix . '_restrictionrelation - WHERE - contentstreamid = :contentStreamId - ', [ - 'contentStreamId' => $event->contentStreamId->value - ]); }); } @@ -839,106 +765,6 @@ function (NodeRecord $node) use ($eventEnvelope) { }); } - private function cascadeRestrictionRelations( - ContentStreamId $contentStreamId, - NodeAggregateId $parentNodeAggregateId, - NodeAggregateId $entryNodeAggregateId, - DimensionSpacePointSet $affectedDimensionSpacePoints - ): void { - $this->getDatabaseConnection()->executeUpdate( - ' - -- GraphProjector::cascadeRestrictionRelations - INSERT INTO ' . $this->tableNamePrefix . '_restrictionrelation - ( - contentstreamid, - dimensionspacepointhash, - originnodeaggregateid, - affectednodeaggregateid - ) - -- we build a recursive tree - with recursive tree as ( - -- -------------------------------- - -- INITIAL query: select the nodes of the given entry node aggregate as roots of the tree - -- -------------------------------- - select - n.relationanchorpoint, - n.nodeaggregateid, - h.dimensionspacepointhash - from - ' . $this->tableNamePrefix . '_node n - -- we need to join with the hierarchy relation, because we need the dimensionspacepointhash. - inner join ' . $this->tableNamePrefix . '_hierarchyrelation h - on h.childnodeanchor = n.relationanchorpoint - where - n.nodeaggregateid = :entryNodeAggregateId - and h.contentstreamid = :contentStreamId - and h.dimensionspacepointhash in (:dimensionSpacePointHashes) - union - -- -------------------------------- - -- RECURSIVE query: do one "child" query step - -- -------------------------------- - select - c.relationanchorpoint, - c.nodeaggregateid, - h.dimensionspacepointhash - from - tree p - inner join ' . $this->tableNamePrefix . '_hierarchyrelation h - on h.parentnodeanchor = p.relationanchorpoint - inner join ' . $this->tableNamePrefix . '_node c - on h.childnodeanchor = c.relationanchorpoint - where - h.contentstreamid = :contentStreamId - and h.dimensionspacepointhash in (:dimensionSpacePointHashes) - ) - - -- -------------------------------- - -- create new restriction relations... - -- -------------------------------- - SELECT - "' . $contentStreamId->value . '" as contentstreamid, - tree.dimensionspacepointhash, - originnodeaggregateid, - tree.nodeaggregateid as affectednodeaggregateid - FROM tree - -- -------------------------------- - -- ...by joining the tree with all restriction relations ingoing to the given parent - -- -------------------------------- - INNER JOIN ( - SELECT originnodeaggregateid FROM ' . $this->tableNamePrefix . '_restrictionrelation - WHERE contentstreamid = :contentStreamId - AND affectednodeaggregateid = :parentNodeAggregateId - AND dimensionspacepointhash IN (:affectedDimensionSpacePointHashes) - ) AS joinedrestrictingancestors - ', - [ - 'contentStreamId' => $contentStreamId->value, - 'parentNodeAggregateId' => $parentNodeAggregateId->value, - 'entryNodeAggregateId' => $entryNodeAggregateId->value, - 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes(), - 'affectedDimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes() - ], - [ - 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, - 'affectedDimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY - ] - ); - } - - /** - * @throws \Throwable - */ - private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void - { - $this->transactional(function () use ($event) { - $this->removeOutgoingRestrictionRelationsOfNodeAggregateInDimensionSpacePoints( - $event->contentStreamId, - $event->nodeAggregateId, - $event->affectedDimensionSpacePoints - ); - }); - } - /** * @param HierarchyRelation $sourceHierarchyRelation * @param ContentStreamId $contentStreamId @@ -955,20 +781,29 @@ protected function copyHierarchyRelationToDimensionSpacePoint( ?NodeRelationAnchorPoint $newParent = null, ?NodeRelationAnchorPoint $newChild = null ): HierarchyRelation { + if ($newParent === null) { + $newParent = $sourceHierarchyRelation->parentNodeAnchor; + } + if ($newChild === null) { + $newChild = $sourceHierarchyRelation->childNodeAnchor; + } + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamId, $newParent, $dimensionSpacePoint); + $inheritedSubtreeTags = NodeTags::create($sourceHierarchyRelation->subtreeTags->withoutInherited()->all(), $parentSubtreeTags->withoutInherited()->all()); $copy = new HierarchyRelation( - $newParent ?: $sourceHierarchyRelation->parentNodeAnchor, - $newChild ?: $sourceHierarchyRelation->childNodeAnchor, + $newParent, + $newChild, $sourceHierarchyRelation->name, $contentStreamId, $dimensionSpacePoint, $dimensionSpacePoint->hash, $this->getRelationPosition( - $newParent ?: $sourceHierarchyRelation->parentNodeAnchor, - $newChild ?: $sourceHierarchyRelation->childNodeAnchor, + $newParent, + $newChild, null, // todo: find proper sibling $contentStreamId, $dimensionSpacePoint - ) + ), + $inheritedSubtreeTags, ); $copy->addToDatabase($this->getDatabaseConnection(), $this->tableNamePrefix); @@ -1163,23 +998,6 @@ function (NodeRecord $nodeRecord) use ($event) { 'contentStreamId' => $event->contentStreamId->value ] ); - - // 3) restriction relations - $this->getDatabaseConnection()->executeStatement( - ' - UPDATE ' . $this->tableNamePrefix . '_restrictionrelation r - SET - r.dimensionspacepointhash = :newDimensionSpacePointHash - WHERE - r.dimensionspacepointhash = :originalDimensionSpacePointHash - AND r.contentstreamid = :contentStreamId - ', - [ - 'originalDimensionSpacePointHash' => $event->source->hash, - 'newDimensionSpacePointHash' => $event->target->hash, - 'contentStreamId' => $event->contentStreamId->value - ] - ); }); } @@ -1194,6 +1012,7 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded childnodeanchor, `name`, position, + subtreetags, dimensionspacepoint, dimensionspacepointhash, contentstreamid @@ -1203,6 +1022,7 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded h.childnodeanchor, h.name, h.position, + h.subtreetags, :newDimensionSpacePoint AS dimensionspacepoint, :newDimensionSpacePointHash AS dimensionspacepointhash, h.contentstreamid @@ -1217,30 +1037,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded 'newDimensionSpacePoint' => $event->target->toJson(), ] ); - - // 2) restriction relations - $this->getDatabaseConnection()->executeUpdate(' - INSERT INTO ' . $this->tableNamePrefix . '_restrictionrelation ( - contentstreamid, - dimensionspacepointhash, - originnodeaggregateid, - affectednodeaggregateid - ) - SELECT - r.contentstreamid, - :targetDimensionSpacePointHash, - r.originnodeaggregateid, - r.affectednodeaggregateid - FROM - ' . $this->tableNamePrefix . '_restrictionrelation r - WHERE r.contentstreamid = :contentStreamId - AND r.dimensionspacepointhash = :sourceDimensionSpacePointHash - - ', [ - 'contentStreamId' => $event->contentStreamId->value, - 'sourceDimensionSpacePointHash' => $event->source->hash, - 'targetDimensionSpacePointHash' => $event->target->hash - ]); }); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index b42e1d2a321..d70938ac215 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -29,7 +29,6 @@ public function buildSchema(AbstractSchemaManager $schemaManager): Schema $this->createNodeTable(), $this->createHierarchyRelationTable(), $this->createReferenceRelationTable(), - $this->createRestrictionRelationTable() ]); } @@ -64,7 +63,8 @@ private function createHierarchyRelationTable(): Table DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true), DbalSchemaFactory::columnForNodeAnchorPoint('parentnodeanchor'), - DbalSchemaFactory::columnForNodeAnchorPoint('childnodeanchor') + DbalSchemaFactory::columnForNodeAnchorPoint('childnodeanchor'), + (new Column('subtreetags', Type::getType(Types::JSON)))->setDefault('{}'), ]); return $table @@ -88,21 +88,4 @@ private function createReferenceRelationTable(): Table return $table ->setPrimaryKey(['name', 'position', 'nodeanchorpoint']); } - - private function createRestrictionRelationTable(): Table - { - $table = new Table($this->tableNamePrefix . '_restrictionrelation', [ - DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true), - DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true), - DbalSchemaFactory::columnForNodeAggregateId('originnodeaggregateid')->setNotnull(false), - DbalSchemaFactory::columnForNodeAggregateId('affectednodeaggregateid')->setNotnull(false), - ]); - - return $table->setPrimaryKey([ - 'contentstreamid', - 'dimensionspacepointhash', - 'originnodeaggregateid', - 'affectednodeaggregateid' - ]); - } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php deleted file mode 100644 index e3ade9c7eb2..00000000000 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeDisabling.php +++ /dev/null @@ -1,100 +0,0 @@ -transactional(function () use ($event) { - - - // TODO: still unsure why we need an "INSERT IGNORE" here; - // normal "INSERT" can trigger a duplicate key constraint exception - $this->getDatabaseConnection()->executeStatement( - ' --- GraphProjector::whenNodeAggregateWasDisabled -insert ignore into ' . $this->getTableNamePrefix() . '_restrictionrelation - (contentstreamid, dimensionspacepointhash, originnodeaggregateid, affectednodeaggregateid) - - -- we build a recursive tree - with recursive tree as ( - -- -------------------------------- - -- INITIAL query: select the root nodes of the tree; as given in $menuLevelNodeIds - -- -------------------------------- - select - n.relationanchorpoint, - n.nodeaggregateid, - h.dimensionspacepointhash - from - ' . $this->getTableNamePrefix() . '_node n - -- we need to join with the hierarchy relation, because we need the dimensionspacepointhash. - inner join ' . $this->getTableNamePrefix() . '_hierarchyrelation h - on h.childnodeanchor = n.relationanchorpoint - where - n.nodeaggregateid = :entryNodeAggregateId - and h.contentstreamid = :contentStreamId - and h.dimensionspacepointhash in (:dimensionSpacePointHashes) - union - -- -------------------------------- - -- RECURSIVE query: do one "child" query step - -- -------------------------------- - select - c.relationanchorpoint, - c.nodeaggregateid, - h.dimensionspacepointhash - from - tree p - inner join ' . $this->getTableNamePrefix() . '_hierarchyrelation h - on h.parentnodeanchor = p.relationanchorpoint - inner join ' . $this->getTableNamePrefix() . '_node c - on h.childnodeanchor = c.relationanchorpoint - where - h.contentstreamid = :contentStreamId - and h.dimensionspacepointhash in (:dimensionSpacePointHashes) - ) - - select - "' . $event->contentStreamId->value . '" as contentstreamid, - dimensionspacepointhash, - "' . $event->nodeAggregateId->value . '" as originnodeaggregateid, - nodeaggregateid as affectednodeaggregateid - from tree - ', - [ - 'entryNodeAggregateId' => $event->nodeAggregateId->value, - 'contentStreamId' => $event->contentStreamId->value, - 'dimensionSpacePointHashes' => $event->affectedDimensionSpacePoints->getPointHashes() - ], - [ - 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY - ] - ); - }); - } - - abstract protected function getDatabaseConnection(): Connection; - - abstract protected function transactional(\Closure $operations): void; -} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index e32b01fb641..f779ad9ff64 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -20,13 +20,10 @@ use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\EventStore\Model\EventEnvelope; /** * The NodeMove projection feature trait * - * Requires RestrictionRelations to work - * * @internal */ trait NodeMove @@ -61,43 +58,25 @@ private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void $newLocation->coveredDimensionSpacePoint ]); - // remove restriction relations by ancestors. We will reconnect them back after the move. - $this->removeAllRestrictionRelationsInSubtreeImposedByAncestors( - $event->contentStreamId, - $event->nodeAggregateId, - $affectedDimensionSpacePoints - ); - // do the move (depending on how the move target is specified) - switch (true) { - case $newLocation->destination instanceof SucceedingSiblingNodeMoveDestination: - $newParentNodeAggregateId = $this->moveNodeBeforeSucceedingSibling( - $event->contentStreamId, - $nodeToBeMoved, - $newLocation->coveredDimensionSpacePoint, - $newLocation->destination - ); - break; - case $newLocation->destination instanceof ParentNodeMoveDestination: - $newParentNodeAggregateId = $newLocation->destination->nodeAggregateId; - $this->moveNodeIntoParent( - $event->contentStreamId, - $nodeToBeMoved, - $newLocation->coveredDimensionSpacePoint, - $newLocation->destination - ); - break; - default: - throw new \RuntimeException('TODO'); + $newParentNodeAggregateId = match ($newLocation->destination::class) { + SucceedingSiblingNodeMoveDestination::class => $this->moveNodeBeforeSucceedingSibling( + $event->contentStreamId, + $nodeToBeMoved, + $newLocation->coveredDimensionSpacePoint, + $newLocation->destination + ), + ParentNodeMoveDestination::class => $newLocation->destination->nodeAggregateId, + }; + if ($newLocation->destination instanceof ParentNodeMoveDestination) { + $this->moveNodeIntoParent( + $event->contentStreamId, + $nodeToBeMoved, + $newLocation->coveredDimensionSpacePoint, + $newLocation->destination + ); } - - // re-build restriction relations - $this->cascadeRestrictionRelations( - $event->contentStreamId, - $newParentNodeAggregateId, - $event->nodeAggregateId, - $affectedDimensionSpacePoints - ); + $this->moveSubtreeTags($event->contentStreamId, $event->nodeAggregateId, $newParentNodeAggregateId, $newLocation->coveredDimensionSpacePoint); } } }); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php index 25b0a0d8810..86fbb00b696 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php @@ -14,8 +14,6 @@ /** * The NodeRemoval projection feature trait * - * Requires RestrictionRelations to work - * * @internal */ trait NodeRemoval @@ -34,12 +32,6 @@ private function whenNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): vo // the focus here is to be correct; that's why the method is not overly performant (for now at least). We might // lateron find tricks to improve performance $this->transactional(function () use ($event) { - $this->removeOutgoingRestrictionRelationsOfNodeAggregateInDimensionSpacePoints( - $event->contentStreamId, - $event->nodeAggregateId, - $event->affectedCoveredDimensionSpacePoints - ); - $ingoingRelations = $this->getProjectionContentGraph()->findIngoingHierarchyRelationsForNodeAggregate( $event->contentStreamId, $event->nodeAggregateId, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php index d1ec99a1369..674e2ceb381 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php @@ -12,6 +12,8 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated; @@ -90,7 +92,7 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria get_class($event) ); } - + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($event->contentStreamId, $parentNode->relationAnchorPoint, $uncoveredDimensionSpacePoint); $hierarchyRelation = new HierarchyRelation( $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, @@ -104,7 +106,8 @@ private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVaria null, $event->contentStreamId, $uncoveredDimensionSpacePoint - ) + ), + NodeTags::create(SubtreeTags::createEmpty(), $parentSubtreeTags->all()), ); $hierarchyRelation->addToDatabase($this->getDatabaseConnection(), $this->getTableNamePrefix()); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/RestrictionRelations.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/RestrictionRelations.php deleted file mode 100644 index 1e3ebed5081..00000000000 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/RestrictionRelations.php +++ /dev/null @@ -1,158 +0,0 @@ -getDatabaseConnection()->executeUpdate( - ' --- GraphProjector::removeOutgoingRestrictionRelationsOfNodeAggregateInDimensionSpacePoints - -DELETE r.* -FROM ' . $this->getTableNamePrefix() . '_restrictionrelation r -WHERE r.contentstreamid = :contentStreamId -AND r.originnodeaggregateid = :originNodeAggregateId -AND r.dimensionspacepointhash in (:dimensionSpacePointHashes)', - [ - 'contentStreamId' => $contentStreamId->value, - 'originNodeAggregateId' => $originNodeAggregateId->value, - 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes() - ], - [ - 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY - ] - ); - } - - /** - * @throws \Doctrine\DBAL\DBALException - */ - private function removeAllRestrictionRelationsUnderneathNodeAggregate( - ContentStreamId $contentStreamId, - NodeAggregateId $nodeAggregateId - ): void { - $this->getDatabaseConnection()->executeUpdate( - ' - -- GraphProjector::removeAllRestrictionRelationsUnderneathNodeAggregate - - delete r.* from - ' . $this->getTableNamePrefix() . '_restrictionrelation r - join - ( - -- we build a recursive tree - with recursive tree as ( - -- -------------------------------- - -- INITIAL query: select the root nodes of the tree - -- -------------------------------- - select - n.relationanchorpoint, - n.nodeaggregateid, - h.dimensionspacepointhash - from - ' . $this->getTableNamePrefix() . '_node n - -- we need to join with the hierarchy relation, - -- because we need the dimensionspacepointhash. - inner join ' . $this->getTableNamePrefix() . '_hierarchyrelation h - on h.childnodeanchor = n.relationanchorpoint - where - n.nodeaggregateid = :entryNodeAggregateId - and h.contentstreamid = :contentStreamId - union - -- -------------------------------- - -- RECURSIVE query: do one "child" query step - -- -------------------------------- - select - c.relationanchorpoint, - c.nodeaggregateid, - h.dimensionspacepointhash - from - tree p - inner join ' . $this->getTableNamePrefix() . '_hierarchyrelation h - on h.parentnodeanchor = p.relationanchorpoint - inner join ' . $this->getTableNamePrefix() . '_node c - on h.childnodeanchor = c.relationanchorpoint - where - h.contentstreamid = :contentStreamId - ) - select * from tree - ) as tree - - -- the "tree" CTE now contains a list of tuples (nodeAggregateId,dimensionSpacePointHash) - -- which are *descendants* of the starting NodeAggregateId in ALL DimensionSpacePointHashes - where - r.contentstreamid = :contentStreamId - and r.dimensionspacepointhash = tree.dimensionspacepointhash - and r.affectednodeaggregateid = tree.nodeaggregateid - ', - [ - 'entryNodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, - ] - ); - } - - /** - * @throws \Doctrine\DBAL\DBALException - */ - private function removeAllRestrictionRelationsInSubtreeImposedByAncestors( - ContentStreamId $contentStreamId, - NodeAggregateId $entryNodeAggregateId, - DimensionSpacePointSet $affectedDimensionSpacePoints - ): void { - $projectionContentGraph = $this->getProjectionContentGraph(); - $descendantNodeAggregateIds = $projectionContentGraph->findDescendantNodeAggregateIds( - $contentStreamId, - $entryNodeAggregateId, - $affectedDimensionSpacePoints - ); - - $this->getDatabaseConnection()->executeUpdate( - ' - -- GraphProjector::removeAllRestrictionRelationsInSubtreeImposedByAncestors - - DELETE r.* - FROM ' . $this->getTableNamePrefix() . '_restrictionrelation r - WHERE r.contentstreamid = :contentStreamId - AND r.originnodeaggregateid NOT IN (:descendantNodeAggregateIds) - AND r.affectednodeaggregateid IN (:descendantNodeAggregateIds) - AND r.dimensionspacepointhash IN (:affectedDimensionSpacePointHashes)', - [ - 'contentStreamId' => $contentStreamId->value, - 'descendantNodeAggregateIds' => array_keys($descendantNodeAggregateIds), - 'affectedDimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes() - ], - [ - 'descendantNodeAggregateIds' => Connection::PARAM_STR_ARRAY, - 'affectedDimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY - ] - ); - } - - abstract protected function getDatabaseConnection(): Connection; -} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php new file mode 100644 index 00000000000..0e6448c37b4 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php @@ -0,0 +1,238 @@ +getDatabaseConnection()->executeStatement(' + UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h + SET h.subtreetags = JSON_INSERT(h.subtreetags, :tagPath, null) + WHERE h.childnodeanchor IN ( + WITH RECURSIVE cte (id) AS ( + SELECT ch.childnodeanchor + FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation ch + INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.parentnodeanchor + WHERE + n.nodeaggregateid = :nodeAggregateId + AND ch.contentstreamid = :contentStreamId + AND ch.dimensionspacepointhash in (:dimensionSpacePointHashes) + AND NOT JSON_CONTAINS_PATH(ch.subtreetags, \'one\', :tagPath) + UNION ALL + SELECT + dh.childnodeanchor + FROM + cte + JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation dh ON dh.parentnodeanchor = cte.id + WHERE + NOT JSON_CONTAINS_PATH(dh.subtreetags, \'one\', :tagPath) + ) + SELECT DISTINCT id FROM cte + ) + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash in (:dimensionSpacePointHashes) + ', [ + 'contentStreamId' => $event->contentStreamId->value, + 'nodeAggregateId' => $event->nodeAggregateId->value, + 'dimensionSpacePointHashes' => $event->affectedDimensionSpacePoints->getPointHashes(), + 'tagPath' => '$.' . $event->tag->value, + ], [ + 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, + ]); + + $this->getDatabaseConnection()->executeStatement(' + UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h + INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = h.childnodeanchor + SET h.subtreetags = JSON_SET(h.subtreetags, :tagPath, true) + WHERE + n.nodeaggregateid = :nodeAggregateId + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash in (:dimensionSpacePointHashes) + ', [ + 'contentStreamId' => $event->contentStreamId->value, + 'nodeAggregateId' => $event->nodeAggregateId->value, + 'dimensionSpacePointHashes' => $event->affectedDimensionSpacePoints->getPointHashes(), + 'tagPath' => '$.' . $event->tag->value, + ], [ + 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, + ]); + } + + /** + * @throws \Throwable + */ + private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void + { + $this->getDatabaseConnection()->executeStatement(' + UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h + INNER JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation ph ON ph.childnodeanchor = h.parentnodeanchor + SET h.subtreetags = IF(( + SELECT + JSON_CONTAINS_PATH(ph.subtreetags, \'one\', :tagPath) + FROM + ' . $this->getTableNamePrefix() . '_hierarchyrelation ph + INNER JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation ch ON ch.parentnodeanchor = ph.childnodeanchor + INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.childnodeanchor + WHERE + n.nodeaggregateid = :nodeAggregateId + AND ph.contentstreamid = :contentStreamId + AND ph.dimensionspacepointhash in (:dimensionSpacePointHashes) + LIMIT 1 + ), JSON_SET(h.subtreetags, :tagPath, null), JSON_REMOVE(h.subtreetags, :tagPath)) + WHERE h.childnodeanchor IN ( + WITH RECURSIVE cte (id) AS ( + SELECT ch.childnodeanchor + FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation ch + INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.childnodeanchor + WHERE + n.nodeaggregateid = :nodeAggregateId + AND ch.contentstreamid = :contentStreamId + AND ch.dimensionspacepointhash in (:dimensionSpacePointHashes) + UNION ALL + SELECT + dh.childnodeanchor + FROM + cte + JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation dh ON dh.parentnodeanchor = cte.id + WHERE + JSON_EXTRACT(dh.subtreetags, :tagPath) != TRUE + ) + SELECT DISTINCT id FROM cte + ) + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash in (:dimensionSpacePointHashes) + ', [ + 'contentStreamId' => $event->contentStreamId->value, + 'nodeAggregateId' => $event->nodeAggregateId->value, + 'dimensionSpacePointHashes' => $event->affectedDimensionSpacePoints->getPointHashes(), + 'tagPath' => '$.' . $event->tag->value, + ], [ + 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY, + ]); + } + + private function moveSubtreeTags(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, NodeAggregateId $newParentNodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint): void + { + $nodeTags = $this->nodeTagsForNode($nodeAggregateId, $contentStreamId, $coveredDimensionSpacePoint); + $newParentSubtreeTags = $this->nodeTagsForNode($newParentNodeAggregateId, $contentStreamId, $coveredDimensionSpacePoint); + $newSubtreeTags = []; + foreach ($nodeTags->withoutInherited() as $tag) { + $newSubtreeTags[$tag->value] = true; + } + foreach ($newParentSubtreeTags as $tag) { + $newSubtreeTags[$tag->value] = null; + } + if ($newSubtreeTags === [] && $nodeTags->isEmpty()) { + return; + } + $this->getDatabaseConnection()->executeStatement(' + UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h + SET h.subtreetags = JSON_MERGE_PATCH(:newParentTags, JSON_MERGE_PATCH(\'{}\', h.subtreetags)) + WHERE h.childnodeanchor IN ( + WITH RECURSIVE cte (id) AS ( + SELECT ch.childnodeanchor + FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation ch + INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = ch.parentnodeanchor + WHERE + n.nodeaggregateid = :nodeAggregateId + AND ch.contentstreamid = :contentStreamId + AND ch.dimensionspacepointhash = :dimensionSpacePointHash + UNION ALL + SELECT + dh.childnodeanchor + FROM + cte + JOIN ' . $this->getTableNamePrefix() . '_hierarchyrelation dh ON dh.parentnodeanchor = cte.id + ) + SELECT id FROM cte + ) + ', [ + 'contentStreamId' => $contentStreamId->value, + 'nodeAggregateId' => $nodeAggregateId->value, + 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash, + 'newParentTags' => json_encode($newSubtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), + ]); + $this->getDatabaseConnection()->executeStatement(' + UPDATE ' . $this->getTableNamePrefix() . '_hierarchyrelation h + INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = h.childnodeanchor + SET h.subtreetags = :newParentTags + WHERE + n.nodeaggregateid = :nodeAggregateId + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash = :dimensionSpacePointHash + ', [ + 'contentStreamId' => $contentStreamId->value, + 'nodeAggregateId' => $nodeAggregateId->value, + 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash, + 'newParentTags' => json_encode($newSubtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), + ]); + } + + private function nodeTagsForNode(NodeAggregateId $nodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): NodeTags + { + $subtreeTagsJson = $this->getDatabaseConnection()->fetchOne(' + SELECT h.subtreetags FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation h + INNER JOIN ' . $this->getTableNamePrefix() . '_node n ON n.relationanchorpoint = h.childnodeanchor + WHERE + n.nodeaggregateid = :nodeAggregateId + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash = :dimensionSpacePointHash + ', [ + 'nodeAggregateId' => $nodeAggregateId->value, + 'contentStreamId' => $contentStreamId->value, + 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, + ]); + if (!is_string($subtreeTagsJson)) { + throw new \RuntimeException(sprintf('Failed to fetch SubtreeTags for node "%s" in content subgraph "%s@%s"', $nodeAggregateId->value, $dimensionSpacePoint->toJson(), $contentStreamId->value), 1698838865); + } + return NodeFactory::extractNodeTagsFromJson($subtreeTagsJson); + } + + private function subtreeTagsForHierarchyRelation(ContentStreamId $contentStreamId, NodeRelationAnchorPoint $parentNodeAnchorPoint, DimensionSpacePoint $dimensionSpacePoint): NodeTags + { + if ($parentNodeAnchorPoint->equals(NodeRelationAnchorPoint::forRootEdge())) { + return NodeTags::createEmpty(); + } + $subtreeTagsJson = $this->getDatabaseConnection()->fetchOne(' + SELECT h.subtreetags FROM ' . $this->getTableNamePrefix() . '_hierarchyrelation h + WHERE + h.childnodeanchor = :parentNodeAnchorPoint + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash = :dimensionSpacePointHash + ', [ + 'parentNodeAnchorPoint' => $parentNodeAnchorPoint->value, + 'contentStreamId' => $contentStreamId->value, + 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, + ]); + if (!is_string($subtreeTagsJson)) { + throw new \RuntimeException(sprintf('Failed to fetch SubtreeTags for hierarchy parent anchor point "%s" in content subgraph "%s@%s"', $parentNodeAnchorPoint->value, $dimensionSpacePoint->toJson(), $contentStreamId->value), 1704199847); + } + return NodeFactory::extractNodeTagsFromJson($subtreeTagsJson); + } + + abstract protected function getDatabaseConnection(): Connection; +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php index 3908f24fcaa..f2c1adb4a1f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php @@ -15,6 +15,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; use Doctrine\DBAL\Connection; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -24,7 +25,7 @@ * * @internal */ -final class HierarchyRelation +final readonly class HierarchyRelation { public function __construct( public NodeRelationAnchorPoint $parentNodeAnchor, @@ -33,7 +34,8 @@ public function __construct( public ContentStreamId $contentStreamId, public DimensionSpacePoint $dimensionSpacePoint, public string $dimensionSpacePointHash, - public int $position + public int $position, + public NodeTags $subtreeTags, ) { } @@ -49,7 +51,8 @@ public function addToDatabase(Connection $databaseConnection, string $tableNameP 'contentstreamid' => $this->contentStreamId->value, 'dimensionspacepoint' => $this->dimensionSpacePoint->toJson(), 'dimensionspacepointhash' => $this->dimensionSpacePointHash, - 'position' => $this->position + 'position' => $this->position, + 'subtreetags' => json_encode($this->subtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), ]); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php index 1ee0e3e106b..6bf6bb752a2 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php @@ -175,74 +175,33 @@ public function tetheredNodesAreNamed(): Result /** * @inheritDoc */ - public function restrictionsArePropagatedRecursively(): Result + public function subtreeTagsAreInherited(): Result { $result = new Result(); - $nodeRecordsWithMissingRestrictions = $this->client->getConnection()->executeQuery( - 'SELECT c.nodeaggregateid, h.contentstreamid, h.dimensionspacepoint - FROM ' . $this->tableNamePrefix . '_hierarchyrelation h - INNER JOIN ' . $this->tableNamePrefix . '_node p - ON p.relationanchorpoint = h.parentnodeanchor - INNER JOIN ' . $this->tableNamePrefix . '_restrictionrelation pr - ON pr.affectednodeaggregateid = p.nodeaggregateid - AND pr.contentstreamid = h.contentstreamid - AND pr.dimensionspacepointhash = h.dimensionspacepointhash - INNER JOIN ' . $this->tableNamePrefix . '_node c - ON c.relationanchorpoint = h.childnodeanchor - LEFT JOIN ' . $this->tableNamePrefix . '_restrictionrelation cr - ON cr.affectednodeaggregateid = c.nodeaggregateid - AND cr.contentstreamid = h.contentstreamid - AND cr.dimensionspacepointhash = h.dimensionspacepointhash - WHERE cr.affectednodeaggregateid IS NULL' - )->fetchAllAssociative(); - - foreach ($nodeRecordsWithMissingRestrictions as $nodeRecord) { - $result->addError(new Error( - 'Node aggregate ' . $nodeRecord['nodeaggregateid'] - . ' misses a restriction relation in content stream ' . $nodeRecord['contentstreamid'] - . ' and dimension space point ' . $nodeRecord['dimensionspacepoint'], - self::ERROR_CODE_NODE_HAS_MISSING_RESTRICTION - )); - } - - return $result; - } - /** - * @inheritDoc - */ - public function restrictionIntegrityIsProvided(): Result - { - $result = new Result(); - - $restrictionRelationRecordsWithoutOriginOrAffectedNode = $this->client->getConnection()->executeQuery( - ' - SELECT r.* FROM ' . $this->tableNamePrefix . '_restrictionrelation r - LEFT JOIN ( - ' . $this->tableNamePrefix . '_node p - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph - ON p.relationanchorpoint = ph.childnodeanchor - ) ON p.nodeaggregateid = r.originnodeaggregateid - AND ph.contentstreamid = r.contentstreamid - AND ph.dimensionspacepointhash = r.dimensionspacepointhash - LEFT JOIN ( - ' . $this->tableNamePrefix . '_node c - INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch - ON c.relationanchorpoint = ch.childnodeanchor - ) ON c.nodeaggregateid = r.affectednodeaggregateid - AND ch.contentstreamid = r.contentstreamid - AND ch.dimensionspacepointhash = r.dimensionspacepointhash - WHERE p.nodeaggregateid IS NULL - OR c.nodeaggregateid IS NULL' + // NOTE: + // This part determines if a parent hierarchy relation contains subtree tags that are not existing in the child relation. + // 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 + FROM + ' . $this->tableNamePrefix . '_hierarchyrelation h + INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph + ON ph.childnodeanchor = h.parentnodeanchor + AND ph.contentstreamid = h.contentstreamid + AND ph.dimensionspacepointhash = h.dimensionspacepointhash + WHERE + EXISTS ( + SELECT t.tag FROM JSON_TABLE(JSON_KEYS(ph.subtreetags), \'$[*]\' COLUMNS(tag VARCHAR(30) PATH \'$\')) t WHERE NOT JSON_EXISTS(h.subtreetags, CONCAT(\'$.\', t.tag)) + )' )->fetchAllAssociative(); - foreach ($restrictionRelationRecordsWithoutOriginOrAffectedNode as $relationRecord) { + foreach ($hierarchyRelationsWithMissingSubtreeTags as $hierarchyRelation) { $result->addError(new Error( - 'Restriction relation ' . $relationRecord['originnodeaggregateid'] - . ' -> ' . $relationRecord['affectednodeaggregateid'] - . ' does not connect two nodes in content stream ' . $relationRecord['contentstreamid'] - . ' and dimension space point ' . $relationRecord['dimensionspacepointhash'], - self::ERROR_CODE_RESTRICTION_INTEGRITY_IS_COMPROMISED + 'Hierarchy relation ' . \json_encode($hierarchyRelation, JSON_THROW_ON_ERROR) + . ' is missing inherited subtree tags from the parent relation.', + self::ERROR_CODE_NODE_HAS_MISSING_SUBTREE_TAG )); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 3ede770bd5d..3ce6f4b36ac 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -137,7 +137,7 @@ public function findRootNodeAggregates( FindRootNodeAggregatesFilter $filter, ): NodeAggregates { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.name, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') @@ -160,7 +160,7 @@ public function findNodeAggregatesByType( NodeTypeName $nodeTypeName ): iterable { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.name, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') @@ -177,10 +177,9 @@ public function findNodeAggregateById( NodeAggregateId $nodeAggregateId ): ?NodeAggregate { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash') + ->select('n.*, h.name, h.contentstreamid, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.childnodeanchor') - ->leftJoin('h', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = n.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = n.nodeaggregateid AND r.dimensionspacepointhash = h.dimensionspacepointhash') ->where('n.nodeaggregateid = :nodeAggregateId') ->andWhere('h.contentstreamid = :contentStreamId') ->setParameters([ @@ -202,12 +201,11 @@ public function findParentNodeAggregates( NodeAggregateId $childNodeAggregateId ): iterable { $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ph.name, ph.contentstreamid, ph.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash') + ->select('pn.*, ph.name, ph.contentstreamid, ph.subtreetags, ph.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->leftJoin('ph', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = pn.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = pn.nodeaggregateid AND r.dimensionspacepointhash = ph.dimensionspacepointhash') ->where('cn.nodeaggregateid = :nodeAggregateId') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ch.contentstreamid = :contentStreamId') @@ -235,10 +233,9 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint( ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash') + ->select('n.*, h.name, h.contentstreamid, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->leftJoin('h', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = n.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = n.nodeaggregateid AND r.dimensionspacepointhash = h.dimensionspacepointhash') ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')') ->andWhere('h.contentstreamid = :contentStreamId') ->setParameters([ @@ -369,12 +366,11 @@ public function getSubgraphs(): array private function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder { return $this->createQueryBuilder() - ->select('cn.*, ch.name, ch.contentstreamid, ch.dimensionspacepoint AS covereddimensionspacepoint, r.dimensionspacepointhash AS disableddimensionspacepointhash') + ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, ch.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->leftJoin('pn', $this->tableNamePrefix . '_restrictionrelation', 'r', 'r.originnodeaggregateid = pn.nodeaggregateid AND r.contentstreamid = :contentStreamId AND r.affectednodeaggregateid = pn.nodeaggregateid AND r.dimensionspacepointhash = ph.dimensionspacepointhash') ->where('pn.nodeaggregateid = :parentNodeAggregateId') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ch.contentstreamid = :contentStreamId') diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index c7dad61097f..420e8e4a43a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -169,20 +169,20 @@ public function countBackReferences(NodeAggregateId $nodeAggregateId, CountBackR public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid') + ->select('n.*, h.name, h.subtreetags, h.contentstreamid') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('n.nodeaggregateid = :nodeAggregateId')->setParameter('nodeAggregateId', $nodeAggregateId->value) ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash); - $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid') + ->select('n.*, h.name, h.subtreetags, h.contentstreamid') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $nodeTypeName->value) @@ -190,14 +190,14 @@ public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->andWhere('n.classification = :nodeAggregateClassification') ->setParameter('nodeAggregateClassification', NodeAggregateClassification::CLASSIFICATION_ROOT->value); - $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ch.name, ph.contentstreamid') + ->select('pn.*, ch.name, ch.subtreetags, ph.contentstreamid') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') @@ -207,7 +207,7 @@ public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node ->andWhere('ch.contentstreamid = :contentStreamId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash'); - $this->addRestrictionRelationConstraints($queryBuilder, 'cn', 'ch', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilder, 'ph'); return $this->fetchNode($queryBuilder); } @@ -239,7 +239,7 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node { $queryBuilder = $this->createQueryBuilder() - ->select('cn.*, h.name, h.contentstreamid') + ->select('cn.*, h.name, h.subtreetags, h.contentstreamid') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') @@ -247,7 +247,7 @@ private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNo ->andWhere('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->andWhere('h.name = :edgeName')->setParameter('edgeName', $nodeName->value); - $this->addRestrictionRelationConstraints($queryBuilder, 'cn', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); } @@ -290,16 +290,16 @@ 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.contentstreamid, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->select('n.*, h.name, h.subtreetags, h.contentstreamid, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); - $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('c.*, h.name, h.contentstreamid, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') + ->select('c.*, h.name, h.subtreetags, h.contentstreamid, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') ->from('tree', 'p') ->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = p.relationanchorpoint') ->innerJoin('p', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = h.childnodeanchor') @@ -311,7 +311,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi if ($filter->nodeTypes !== null) { $this->addNodeTypeCriteria($queryBuilderRecursive, $filter->nodeTypes, 'c'); } - $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'c', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderRecursive); $queryBuilderCte = $this->createQueryBuilder() ->select('*') @@ -381,33 +381,33 @@ public function countAncestorNodes(NodeAggregateId $entryNodeAggregateId, CountA public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClosestNodeFilter $filter): ?Node { $queryBuilderInitial = $this->createQueryBuilder() - ->select('n.*, ph.name, ph.contentstreamid, ph.parentnodeanchor') + ->select('n.*, ph.name, ph.subtreetags, ph.contentstreamid, ph.parentnodeanchor') ->from($this->tableNamePrefix . '_node', 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'n.relationanchorpoint = ph.childnodeanchor') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); - $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('p.*, h.name, h.contentstreamid, h.parentnodeanchor') - ->from('ancestry', 'c') - ->innerJoin('c', $this->tableNamePrefix . '_node', 'p', 'p.relationanchorpoint = c.parentnodeanchor') - ->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = p.relationanchorpoint') + ->select('pn.*, h.name, h.subtreetags, h.contentstreamid, h.parentnodeanchor') + ->from('ancestry', 'cn') + ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') + ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); - $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'p', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderRecursive); $queryBuilderCte = $this->createQueryBuilder() ->select('*') - ->from('ancestry', 'p') + ->from('ancestry', 'pn') ->setMaxResults(1) ->setParameter('contentStreamId', $this->contentStreamId->value) ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'p'); + $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'pn'); } $nodeRows = $this->fetchCteResults( $queryBuilderInitial, @@ -491,36 +491,15 @@ private function createUniqueParameterName(): string return 'param_' . (++$this->dynamicParameterCount); } - /** - * @param QueryBuilder $queryBuilder - * @param string $nodeTableAlias - * @param string $hierarchyRelationTableAlias - * @param string|null $contentStreamIdParameter if not given, condition will be on the hierachy relation content stream id - * @param string|null $dimensionspacePointHashParameter if not given, condition will be on the hierachy relation dimension space point hash - * @return void - */ - private function addRestrictionRelationConstraints(QueryBuilder $queryBuilder, string $nodeTableAlias = 'n', string $hierarchyRelationTableAlias = 'h', ?string $contentStreamIdParameter = null, ?string $dimensionspacePointHashParameter = null): void - { - if ($this->visibilityConstraints->isDisabledContentShown()) { - return; - } - $nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.'; + private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hierarchyRelationTableAlias = 'h'): void + { $hierarchyRelationTablePrefix = $hierarchyRelationTableAlias === '' ? '' : $hierarchyRelationTableAlias . '.'; - - $contentStreamIdCondition = 'r.contentstreamid = ' . ($contentStreamIdParameter ?? ($hierarchyRelationTablePrefix . 'contentstreamid')); - - $dimensionspacePointHashCondition = 'r.dimensionspacepointhash = ' . ($dimensionspacePointHashParameter ?? ($hierarchyRelationTablePrefix . 'dimensionspacepointhash')); - - $subQueryBuilder = $this->createQueryBuilder() - ->select('1') - ->from($this->tableNamePrefix . '_restrictionrelation', 'r') - ->where($contentStreamIdCondition) - ->andWhere($dimensionspacePointHashCondition) - ->andWhere('r.affectednodeaggregateid = ' . $nodeTablePrefix . 'nodeaggregateid'); - $queryBuilder->andWhere( - 'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')' - ); + $i = 0; + foreach ($this->visibilityConstraints->tagConstraints as $excludedTag) { + $queryBuilder->andWhere('NOT JSON_CONTAINS_PATH(' . $hierarchyRelationTablePrefix . 'subtreetags, \'one\', :tagPath' . $i . ')')->setParameter('tagPath' . $i, '$.' . $excludedTag->value); + $i++; + } } private function addNodeTypeCriteria(QueryBuilder $queryBuilder, NodeTypeCriteria $nodeTypeCriteria, string $nodeTableAlias = 'n'): void @@ -621,7 +600,7 @@ private function searchPropertyValueStatement(QueryBuilder $queryBuilder, Proper private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter|CountChildNodesFilter $filter): QueryBuilder { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid') + ->select('n.*, h.name, h.subtreetags, h.contentstreamid') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_node', 'n', 'h.childnodeanchor = n.relationanchorpoint') @@ -637,7 +616,7 @@ private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, Fi if ($filter->propertyValue !== null) { $this->addPropertyValueConstraints($queryBuilder, $filter->propertyValue); } - $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilder); return $queryBuilder; } @@ -646,7 +625,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.contentstreamid, r.name AS referencename, r.properties AS referenceproperties") + ->select("{$destinationTablePrefix}n.*, {$destinationTablePrefix}h.name, {$destinationTablePrefix}h.subtreetags, {$destinationTablePrefix}h.contentstreamid, r.name AS referencename, r.properties AS referenceproperties") ->from($this->tableNamePrefix . '_hierarchyrelation', 'sh') ->innerJoin('sh', $this->tableNamePrefix . '_node', 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') ->innerJoin('sh', $this->tableNamePrefix . '_referencerelation', 'r', 'r.nodeanchorpoint = sn.relationanchorpoint') @@ -657,8 +636,8 @@ private function buildReferencesQuery(bool $backReferences, NodeAggregateId $nod ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('dh.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) ->andWhere('sh.contentstreamid = :contentStreamId'); - $this->addRestrictionRelationConstraints($queryBuilder, 'dn', 'dh', ':contentStreamId', ':dimensionSpacePointHash'); - $this->addRestrictionRelationConstraints($queryBuilder, 'sn', 'sh', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilder, 'dh'); + $this->addSubtreeTagConstraints($queryBuilder, 'sh'); if ($filter->nodeTypes !== null) { $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes, "{$destinationTablePrefix}n"); } @@ -714,7 +693,7 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid') + ->select('n.*, h.name, h.subtreetags, h.contentstreamid') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value) @@ -724,7 +703,7 @@ private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNod ->andWhere('h.position ' . ($preceding ? '<' : '>') . ' (' . $siblingPositionSubQuery->getSQL() . ')') ->orderBy('h.position', $preceding ? 'DESC' : 'ASC'); - $this->addRestrictionRelationConstraints($queryBuilder, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilder); if ($filter->nodeTypes !== null) { $this->addNodeTypeCriteria($queryBuilder, $filter->nodeTypes); } @@ -746,7 +725,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.contentstreamid, ph.parentnodeanchor') + ->select('n.*, ph.name, ph.subtreetags, ph.contentstreamid, ph.parentnodeanchor') ->from($this->tableNamePrefix . '_node', 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') @@ -757,26 +736,26 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('c.nodeaggregateid = :entryNodeAggregateId'); - $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'ph', ':contentStreamId', ':dimensionSpacePointHash'); - $this->addRestrictionRelationConstraints($queryBuilderInitial, 'c', 'ch', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); + $this->addSubtreeTagConstraints($queryBuilderInitial, 'ch'); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('p.*, h.name, h.contentstreamid, h.parentnodeanchor') - ->from('ancestry', 'c') - ->innerJoin('c', $this->tableNamePrefix . '_node', 'p', 'p.relationanchorpoint = c.parentnodeanchor') - ->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = p.relationanchorpoint') + ->select('pn.*, h.name, h.subtreetags, h.contentstreamid, h.parentnodeanchor') + ->from('ancestry', 'cn') + ->innerJoin('cn', $this->tableNamePrefix . '_node', 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') + ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = pn.relationanchorpoint') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); - $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'p', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderRecursive); $queryBuilderCte = $this->createQueryBuilder() ->select('*') - ->from('ancestry', 'p') + ->from('ancestry', 'pn') ->setParameter('contentStreamId', $this->contentStreamId->value) ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); if ($filter->nodeTypes !== null) { - $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'p'); + $this->addNodeTypeCriteria($queryBuilderCte, $filter->nodeTypes, 'pn'); } return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte'); } @@ -788,7 +767,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.contentstreamid, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') + ->select('n.*, h.name, h.subtreetags, h.contentstreamid, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->tableNamePrefix . '_node', 'n') // we need to join with the hierarchy relation, because we need the node name. ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') @@ -799,16 +778,16 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('p.nodeaggregateid = :entryNodeAggregateId'); - $this->addRestrictionRelationConstraints($queryBuilderInitial, 'n', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() - ->select('c.*, h.name, h.contentstreamid, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') - ->from('tree', 'p') - ->innerJoin('p', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = p.relationanchorpoint') - ->innerJoin('p', $this->tableNamePrefix . '_node', 'c', 'c.relationanchorpoint = h.childnodeanchor') + ->select('cn.*, h.name, h.subtreetags, h.contentstreamid, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') + ->from('tree', 'pn') + ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = h.childnodeanchor') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); - $this->addRestrictionRelationConstraints($queryBuilderRecursive, 'c', 'h', ':contentStreamId', ':dimensionSpacePointHash'); + $this->addSubtreeTagConstraints($queryBuilderRecursive); $queryBuilderCte = $this->createQueryBuilder() ->select('*') diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php index c93abc75f93..86d5a2c663b 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php @@ -17,10 +17,14 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; +use Neos\ContentRepository\Core\Projection\ContentGraph\DimensionSpacePointsBySubtreeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\Reference; use Neos\ContentRepository\Core\Projection\ContentGraph\References; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; @@ -81,6 +85,7 @@ public function mapNodeRowToNode( $nodeType, $this->createPropertyCollectionFromJsonString($nodeRow['properties']), isset($nodeRow['name']) ? NodeName::fromString($nodeRow['name']) : null, + self::extractNodeTagsFromJson($nodeRow['subtreetags']), Timestamps::create( self::parseDateTimeString($nodeRow['created']), self::parseDateTimeString($nodeRow['originalcreated']), @@ -157,7 +162,7 @@ public function mapNodeRowsToNodeAggregate( $nodesByCoveredDimensionSpacePoints = []; $coverageByOccupants = []; $occupationByCovering = []; - $disabledDimensionSpacePoints = []; + $dimensionSpacePointsBySubtreeTags = DimensionSpacePointsBySubtreeTags::create(); foreach ($nodeRows as $nodeRow) { // A node can occupy exactly one DSP and cover multiple ones... @@ -188,14 +193,13 @@ public function mapNodeRowsToNodeAggregate( $occupationByCovering[$coveredDimensionSpacePoint->hash] = $occupiedDimensionSpacePoint; $nodesByCoveredDimensionSpacePoints[$coveredDimensionSpacePoint->hash] = $nodesByOccupiedDimensionSpacePoints[$occupiedDimensionSpacePoint->hash]; - // ... as we do for disabling - if (isset($nodeRow['disableddimensionspacepointhash'])) { - $disabledDimensionSpacePoints[$coveredDimensionSpacePoint->hash] = $coveredDimensionSpacePoint; + // ... as we do for explicit subtree tags + foreach (self::extractNodeTagsFromJson($nodeRow['subtreetags'])->withoutInherited() as $explicitTag) { + $dimensionSpacePointsBySubtreeTags = $dimensionSpacePointsBySubtreeTags->withSubtreeTagAndDimensionSpacePoint($explicitTag, $coveredDimensionSpacePoint); } } ksort($occupiedDimensionSpacePoints); ksort($coveredDimensionSpacePoints); - ksort($disabledDimensionSpacePoints); /** @var Node $primaryNode a nodeAggregate only exists if it at least contains one node. */ $primaryNode = current($nodesByOccupiedDimensionSpacePoints); @@ -212,7 +216,7 @@ public function mapNodeRowsToNodeAggregate( new DimensionSpacePointSet($coveredDimensionSpacePoints), $nodesByCoveredDimensionSpacePoints, OriginByCoverage::fromArray($occupationByCovering), - new DimensionSpacePointSet($disabledDimensionSpacePoints) + $dimensionSpacePointsBySubtreeTags, ); } @@ -234,7 +238,7 @@ public function mapNodeRowsToNodeAggregates( $classificationByNodeAggregate = []; $coverageByOccupantsByNodeAggregate = []; $occupationByCoveringByNodeAggregate = []; - $disabledDimensionSpacePointsByNodeAggregate = []; + $dimensionSpacePointsBySubtreeTagsByNodeAggregate = []; foreach ($nodeRows as $nodeRow) { // A node can occupy exactly one DSP and cover multiple ones... @@ -279,10 +283,12 @@ public function mapNodeRowsToNodeAggregates( = $nodesByOccupiedDimensionSpacePointsByNodeAggregate [$rawNodeAggregateId][$occupiedDimensionSpacePoint->hash]; - // ... as we do for disabling - if (isset($nodeRow['disableddimensionspacepointhash'])) { - $disabledDimensionSpacePointsByNodeAggregate - [$rawNodeAggregateId][$coveredDimensionSpacePoint->hash] = $coveredDimensionSpacePoint; + // ... as we do for explicit subtree tags + if (!array_key_exists($rawNodeAggregateId, $dimensionSpacePointsBySubtreeTagsByNodeAggregate)) { + $dimensionSpacePointsBySubtreeTagsByNodeAggregate[$rawNodeAggregateId] = DimensionSpacePointsBySubtreeTags::create(); + } + foreach (self::extractNodeTagsFromJson($nodeRow['subtreetags'])->withoutInherited() as $explicitTag) { + $dimensionSpacePointsBySubtreeTagsByNodeAggregate[$rawNodeAggregateId] = $dimensionSpacePointsBySubtreeTagsByNodeAggregate[$rawNodeAggregateId]->withSubtreeTagAndDimensionSpacePoint($explicitTag, $coveredDimensionSpacePoint); } } @@ -310,13 +316,29 @@ public function mapNodeRowsToNodeAggregates( OriginByCoverage::fromArray( $occupationByCoveringByNodeAggregate[$rawNodeAggregateId] ), - new DimensionSpacePointSet( - $disabledDimensionSpacePointsByNodeAggregate[$rawNodeAggregateId] ?? [] - ) + $dimensionSpacePointsBySubtreeTagsByNodeAggregate[$rawNodeAggregateId], ); } } + public static function extractNodeTagsFromJson(string $subtreeTagsJson): NodeTags + { + $explicitTags = []; + $inheritedTags = []; + $subtreeTagsArray = json_decode($subtreeTagsJson, true, 512, JSON_THROW_ON_ERROR); + foreach ($subtreeTagsArray as $tagValue => $explicit) { + if ($explicit) { + $explicitTags[] = $tagValue; + } else { + $inheritedTags[] = $tagValue; + } + } + return NodeTags::create( + tags: SubtreeTags::fromStrings(...$explicitTags), + inheritedTags: SubtreeTags::fromStrings(...$inheritedTags) + ); + } + private static function parseDateTimeString(string $string): \DateTimeImmutable { $result = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $string); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index 00d5356a176..8c8678191c3 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -23,6 +23,7 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; @@ -71,7 +72,7 @@ public function findParentNode( : $originDimensionSpacePoint->hash ]; $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT p.*, ph.contentstreamid, ph.name FROM ' . $this->tableNamePrefix . '_node p + 'SELECT p.*, ph.contentstreamid, ph.name, ph.subtreetags FROM ' . $this->tableNamePrefix . '_node p INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph ON ph.childnodeanchor = p.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch ON ch.parentnodeanchor = p.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_node c ON ch.childnodeanchor = c.relationanchorpoint @@ -101,7 +102,7 @@ public function findNodeInAggregate( DimensionSpacePoint $coveredDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name FROM ' . $this->tableNamePrefix . '_node n + 'SELECT n.*, h.name, h.subtreetags FROM ' . $this->tableNamePrefix . '_node n INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId AND h.contentstreamid = :contentStreamId @@ -129,7 +130,7 @@ public function findNodeByIds( OriginDimensionSpacePoint $originDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name FROM ' . $this->tableNamePrefix . '_node n + 'SELECT n.*, h.name, h.subtreetags FROM ' . $this->tableNamePrefix . '_node n INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId AND n.origindimensionspacepointhash = :originDimensionSpacePointHash @@ -655,7 +656,8 @@ protected function mapRawDataToHierarchyRelation(array $rawData): HierarchyRelat ContentStreamId::fromString($rawData['contentstreamid']), DimensionSpacePoint::fromJsonString($rawData['dimensionspacepoint']), $rawData['dimensionspacepointhash'], - (int)$rawData['position'] + (int)$rawData['position'], + NodeFactory::extractNodeTagsFromJson($rawData['subtreetags']), ); } diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/Feature/NodeDisabling.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/Feature/SubtreeTagging.php similarity index 85% rename from Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/Feature/NodeDisabling.php rename to Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/Feature/SubtreeTagging.php index f96b2535b29..6ce6ddd541c 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/Feature/NodeDisabling.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/Feature/SubtreeTagging.php @@ -17,20 +17,20 @@ use Doctrine\DBAL\Connection; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\ProjectionHypergraph; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\RestrictionHyperrelationRecord; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasEnabled; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; /** - * The node disabling feature set for the hypergraph projector + * The subtree tagging feature set for the hypergraph projector * * @internal */ -trait NodeDisabling +trait SubtreeTagging { /** * @throws \Throwable */ - private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void + private function whenSubtreeWasTagged(SubtreeWasTagged $event): void { $this->transactional(function () use ($event) { $descendantNodeAggregateIdsByAffectedDimensionSpacePoint @@ -58,7 +58,7 @@ private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): /** * @throws \Throwable */ - private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void + private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void { $this->transactional(function () use ($event) { $restrictionRelations = $this->getProjectionHypergraph()->findOutgoingRestrictionRelations( diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php index 735fe6df329..3ff4ca76833 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php @@ -18,13 +18,13 @@ use Doctrine\DBAL\Schema\AbstractSchemaManager; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\ContentStreamForking; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeCreation; -use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeDisabling; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeModification; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeReferencing; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeRemoval; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeRenaming; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeTypeChange; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\NodeVariation; +use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\Feature\SubtreeTagging; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Projection\SchemaBuilder\HypergraphSchemaBuilder; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Repository\ContentHypergraph; use Neos\ContentGraph\PostgreSQLAdapter\Domain\Repository\NodeFactory; @@ -33,8 +33,6 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasEnabled; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved; @@ -44,6 +42,8 @@ use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\Infrastructure\DbalCheckpointStorage; use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; @@ -63,7 +63,7 @@ final class HypergraphProjection implements ProjectionInterface { use ContentStreamForking; use NodeCreation; - use NodeDisabling; + use SubtreeTagging; use NodeModification; use NodeReferencing; use NodeRemoval; @@ -183,9 +183,9 @@ public function canHandle(EventInterface $event): bool // NodeCreation RootNodeAggregateWithNodeWasCreated::class, NodeAggregateWithNodeWasCreated::class, - // NodeDisabling - NodeAggregateWasDisabled::class, - NodeAggregateWasEnabled::class, + // SubtreeTagging + SubtreeWasTagged::class, + SubtreeWasUntagged::class, // NodeModification NodePropertiesWereSet::class, // NodeReferencing @@ -216,9 +216,9 @@ public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void // NodeCreation RootNodeAggregateWithNodeWasCreated::class => $this->whenRootNodeAggregateWithNodeWasCreated($event), NodeAggregateWithNodeWasCreated::class => $this->whenNodeAggregateWithNodeWasCreated($event), - // NodeDisabling - NodeAggregateWasDisabled::class => $this->whenNodeAggregateWasDisabled($event), - NodeAggregateWasEnabled::class => $this->whenNodeAggregateWasEnabled($event), + // SubtreeTagging + SubtreeWasTagged::class => $this->whenSubtreeWasTagged($event), + SubtreeWasUntagged::class => $this->whenSubtreeWasUntagged($event), // NodeModification NodePropertiesWereSet::class => $this->whenNodePropertiesWereSet($event), // NodeReferencing diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php index 894b86cc3b2..9820ddba50c 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Repository/NodeFactory.php @@ -18,9 +18,11 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; +use Neos\ContentRepository\Core\Projection\ContentGraph\DimensionSpacePointsBySubtreeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Reference; use Neos\ContentRepository\Core\Projection\ContentGraph\References; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; @@ -92,6 +94,8 @@ public function mapNodeRowToNode( $this->propertyConverter ), $nodeRow['nodename'] ? NodeName::fromString($nodeRow['nodename']) : null, + // TODO implement {@see \Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory::mapNodeRowToNode()} + NodeTags::createEmpty(), Timestamps::create( // TODO replace with $nodeRow['created'] and $nodeRow['originalcreated'] once projection has implemented support self::parseDateTimeString('2023-03-17 12:00:00'), @@ -250,7 +254,8 @@ public function mapNodeRowsToNodeAggregate( new DimensionSpacePointSet($coveredDimensionSpacePoints), $nodesByCoveredDimensionSpacePoint, OriginByCoverage::fromArray($occupationByCovered), - new DimensionSpacePointSet($disabledDimensionSpacePoints) + // TODO implement (see \Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory::mapNodeRowsToNodeAggregate()) + DimensionSpacePointsBySubtreeTags::create(), ); } @@ -346,7 +351,8 @@ public function mapNodeRowsToNodeAggregates(array $nodeRows, VisibilityConstrain new DimensionSpacePointSet($coveredDimensionSpacePoints[$key]), $nodesByCoveredDimensionSpacePoint[$key], OriginByCoverage::fromArray($occupationByCovered[$key]), - new DimensionSpacePointSet($disabledDimensionSpacePoints[$key]) + // TODO implement (see \Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory::mapNodeRowsToNodeAggregates()) + DimensionSpacePointsBySubtreeTags::create(), ); } } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature index d41691053ff..82db6e57770 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/01-DisableNodeAggregate_ConstraintChecks.feature @@ -56,7 +56,7 @@ Feature: Constraint checks on node aggregate disabling | Key | Value | | nodeAggregateId | "i-do-not-exist" | | nodeVariantSelectionStrategy | "allVariants" | - Then the last command should have thrown an exception of type "NodeAggregateCurrentlyDoesNotExist" + | tag | "disabled" | Scenario: Try to disable an already disabled node aggregate Given the command DisableNodeAggregate is executed with payload: @@ -73,11 +73,12 @@ Feature: Constraint checks on node aggregate disabling | coveredDimensionSpacePoint | {"language": "de"} | | nodeVariantSelectionStrategy | "allVariants" | Then I expect exactly 4 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 3 is of type "NodeAggregateWasDisabled" with payload: + And event at index 3 is of type "SubtreeWasTagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{"language":"de"},{"language":"gsw"}] | + | tag | "disabled" | Scenario: Try to disable a node aggregate in a non-existing dimension space point diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature index bd52248617d..ed23f5e23db 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/02-DisableNodeAggregate_WithoutDimensions.feature @@ -50,11 +50,12 @@ Feature: Disable a node aggregate | nodeVariantSelectionStrategy | "allVariants" | Then I expect exactly 8 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 7 is of type "NodeAggregateWasDisabled" with payload: + And event at index 7 is of type "SubtreeWasTagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [[]] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature index 29d50add32f..b4df608c120 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/03-DisableNodeAggregate_WithDimensions.feature @@ -60,11 +60,12 @@ Feature: Disable a node aggregate | nodeVariantSelectionStrategy | "allSpecializations" | Then I expect exactly 9 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 8 is of type "NodeAggregateWasDisabled" with payload: + And event at index 8 is of type "SubtreeWasTagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{"language":"de"}, {"language":"ltz"}, {"language":"gsw"}] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" @@ -311,11 +312,12 @@ Feature: Disable a node aggregate | nodeVariantSelectionStrategy | "allVariants" | Then I expect exactly 9 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 8 is of type "NodeAggregateWasDisabled" with payload: + And event at index 8 is of type "SubtreeWasTagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{"language":"ltz"}, {"language":"mul"}, {"language":"de"}, {"language":"en"}, {"language":"gsw"}] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature index 2cb99fedc0b..b44170f3870 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/05-EnableNodeAggregate_WithoutDimensions.feature @@ -56,11 +56,12 @@ Feature: Enable a node aggregate | nodeVariantSelectionStrategy | "allVariants" | Then I expect exactly 9 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 8 is of type "NodeAggregateWasEnabled" with payload: + And event at index 8 is of type "SubtreeWasUntagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [[]] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" @@ -137,11 +138,12 @@ Feature: Enable a node aggregate | nodeAggregateId | "sir-david-nodenborough" | | nodeVariantSelectionStrategy | "allVariants" | Then I expect exactly 10 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 9 is of type "NodeAggregateWasEnabled" with payload: + And event at index 9 is of type "SubtreeWasUntagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [[]] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" @@ -214,11 +216,12 @@ Feature: Enable a node aggregate | nodeAggregateId | "nody-mc-nodeface" | | nodeVariantSelectionStrategy | "allVariants" | Then I expect exactly 10 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 9 is of type "NodeAggregateWasEnabled" with payload: + And event at index 9 is of type "SubtreeWasUntagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "nody-mc-nodeface" | | affectedDimensionSpacePoints | [[]] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature index 7e2a1641fc4..97f56b109d3 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/06-EnableNodeAggregate_WithDimensions.feature @@ -74,11 +74,12 @@ Feature: Enable a node aggregate | nodeVariantSelectionStrategy | "allSpecializations" | Then I expect exactly 12 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 11 is of type "NodeAggregateWasEnabled" with payload: + And event at index 11 is of type "SubtreeWasUntagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{"language":"de"},{"language":"ltz"},{"language":"gsw"}] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" @@ -369,11 +370,12 @@ Feature: Enable a node aggregate | nodeVariantSelectionStrategy | "allVariants" | Then I expect exactly 12 events to be published on stream with prefix "ContentStream:cs-identifier" - And event at index 11 is of type "NodeAggregateWasEnabled" with payload: + And event at index 11 is of type "SubtreeWasUntagged" with payload: | Key | Expected | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{"language":"mul"},{"language":"de"},{"language":"en"},{"language":"gsw"},{"language":"ltz"}] | + | tag | "disabled" | When the graph projection is fully up to date And I am in the active content stream of workspace "live" diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature index cd58c9d668a..d7acb6a6c92 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/06-NodeDisabling/09-CreateNodeVariantOfDisabledNode.feature @@ -118,7 +118,8 @@ Feature: Variation of hidden nodes Then I expect node aggregate identifier "the-great-nodini" and node path "court-magician" to lead to no node When I am in dimension space point {"language":"mul"} - Then I expect node aggregate identifier "the-great-nodini" and node path "court-magician" to lead to node cs-identifier;the-great-nodini;{"language":"mul"} + Then I expect node aggregate identifier "the-great-nodini" and node path "court-magician" to lead to no node + #cs-identifier;the-great-nodini;{"language":"mul"} Scenario: Generalize a node where the generalization target is disabled Given I am in dimension space point {"language":"ltz"} diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateConsideringDisableStateWithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateConsideringDisableStateWithoutDimensions.feature index eee4aed8bc4..e8f664d3ec1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateConsideringDisableStateWithoutDimensions.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/08-NodeMove/MoveNodeAggregateConsideringDisableStateWithoutDimensions.feature @@ -77,11 +77,12 @@ Feature: Move a node aggregate considering disable state but without content dim And the graph projection is fully up to date Scenario: Move a node disabled by one of its ancestors to a new parent that is enabled - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | And the graph projection is fully up to date When the command MoveNodeAggregate is executed with payload: @@ -100,11 +101,12 @@ Feature: Move a node aggregate considering disable state but without content dim And I expect this node to be a child of node cs-identifier;sir-nodeward-nodington-iii;{} Scenario: Move a node disabled by itself to a new parent that is enabled - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "nody-mc-nodeface" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | And the graph projection is fully up to date When the command MoveNodeAggregate is executed with payload: @@ -120,11 +122,12 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/child-document" to lead to no node Scenario: Move a node that disables one of its descendants to a new parent that is enabled - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | And the graph projection is fully up to date When the command MoveNodeAggregate is executed with payload: @@ -141,16 +144,18 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document/child-document" to lead to no node Scenario: Move a node that is disabled by one of its ancestors to a new parent that disables itself - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | - And the event NodeAggregateWasDisabled was published with payload: + | tag | "disabled" | + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | And the graph projection is fully up to date When the command MoveNodeAggregate is executed with payload: @@ -166,16 +171,18 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/child-document" to lead to no node Scenario: Move a node that is disabled by itself to a new parent that disables itself - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | - And the event NodeAggregateWasDisabled was published with payload: + | tag | "disabled" | + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | When the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -190,11 +197,12 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/document" to lead to no node Scenario: Move a node that is enabled to a new parent that disables itself - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | When the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -209,16 +217,18 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/document" to lead to no node Scenario: Move a node that disables any of its descendants to a new parent that disables itself - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | - And the event NodeAggregateWasDisabled was published with payload: + | tag | "disabled" | + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | When the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -234,16 +244,18 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/document/child-document" to lead to no node Scenario: Move a node that is disabled by one of its ancestors to a new parent that is disabled by one of its ancestors - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | - And the event NodeAggregateWasDisabled was published with payload: + | tag | "disabled" | + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | When the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -258,16 +270,18 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/child-document" to lead to no node Scenario: Move a node that is disabled by itself to a new parent that is disabled by one of its ancestors - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | - And the event NodeAggregateWasDisabled was published with payload: + | tag | "disabled" | + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | When the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -282,16 +296,18 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "sir-david-nodenborough" and node path "esquire/esquire-child/document" to lead to no node Scenario: Move a node that disables any of its descendants to a new parent that is disabled by one of its ancestors - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | - And the event NodeAggregateWasDisabled was published with payload: + | tag | "disabled" | + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | When the command MoveNodeAggregate is executed with payload: | Key | Value | @@ -307,11 +323,12 @@ Feature: Move a node aggregate considering disable state but without content dim Then I expect node aggregate identifier "nody-mc-nodeface" and node path "esquire/esquire-child/document/child-document" to lead to no node Scenario: Move a node that is enabled to a new parent that is disabled by one of its ancestors - Given the event NodeAggregateWasDisabled was published with payload: + Given the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-nodeward-nodington-iii" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | When the command MoveNodeAggregate is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature index abf1450618a..4dac588d49e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/Timestamps.feature @@ -249,7 +249,7 @@ 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 | | | - Scenario: NodeAggregateWasEnabled and NodeAggregateWasDisabled events don't update last modified timestamps + Scenario: SubtreeWasTagged and SubtreeWasUntagged events don't update last modified timestamps When the current date and time is "2023-03-16T13:00:00+01:00" And the command DisableNodeAggregate is executed with payload: | Key | Value | diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithDimensions.feature new file mode 100644 index 00000000000..6da6d7e9020 --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithDimensions.feature @@ -0,0 +1,189 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Tag subtree with dimensions + + As a user of the CR I want to tag a node and expect its descendants to also be tagged. + + These are the test cases with dimensions being involved + + Background: + Given using the following content dimensions: + | Identifier | Values | Generalizations | + | language | mul, de, en, gsw, ltz | ltz->de->mul, gsw->de->mul, en->mul | + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': {} + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in the active content stream of workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | originDimensionSpacePoint | + | a | Neos.ContentRepository.Testing:Document | root | a | {"language":"mul"} | + | a1 | Neos.ContentRepository.Testing:Document | a | a1 | {"language":"de"} | + | a1a | Neos.ContentRepository.Testing:Document | a1 | a1a | {"language":"de"} | + + Scenario: Subtree tags are properly copied upon node specializations + Given I am in dimension space point {"language":"de"} + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"mul"} | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"mul"} | + + And the graph projection is fully up to date + + When I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: + """ + a + a1 (tag1*) + a1a (tag2*,tag1) + """ + + When I am in dimension space point {"language":"mul"} + And I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: + """ + a + a1 + a1a (tag2*) + """ + + Scenario: Subtree tags are properly copied upon node generalizations + Given I am in dimension space point {"language":"de"} + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "a" | + | sourceOrigin | {"language":"mul"} | + | targetOrigin | {"language":"de"} | + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + + Given I am in dimension space point {"language":"mul"} + + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a" | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag2" | + + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"mul"} | + + When I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: + """ + a (tag2*) + a1 (tag2) + """ + + When I am in dimension space point {"language":"de"} + And I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: + """ + a (tag1*,tag2*) + a1 (tag1,tag2) + a1a (tag1,tag2) + """ + + Scenario: Subtree tags are properly copied upon node variant recreation + When the command CreateWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-id" | + And the graph projection is fully up to date + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | nodeAggregateId | "a1" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + And the graph projection is fully up to date + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | nodeAggregateId | "a1a" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + And the graph projection is fully up to date + And the command PublishWorkspace is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | newContentStreamId | "new-user-cs-id" | + And the graph projection is fully up to date + + And the command RemoveNodeAggregate is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | nodeAggregateId | "a1" | + | coveredDimensionSpacePoint | {"language":"gsw"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + And the graph projection is fully up to date + + And the command TagSubtree is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | nodeAggregateId | "a" | + | coveredDimensionSpacePoint | {"language":"de"} | + | nodeVariantSelectionStrategy | "allSpecializations" | + | tag | "tag1" | + + And the command CreateNodeVariant is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | nodeAggregateId | "a1" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + And the graph projection is fully up to date + When the command CreateNodeVariant is executed with payload: + | Key | Value | + | workspaceName | "user-ws" | + | nodeAggregateId | "a1a" | + | sourceOrigin | {"language":"de"} | + | targetOrigin | {"language":"gsw"} | + And the graph projection is fully up to date + And I am in the active content stream of workspace "user-ws" and dimension space point {"language":"gsw"} + And I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: + """ + a (tag1*) + a1 (tag1) + a1a (tag1) + """ diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature new file mode 100644 index 00000000000..4fc466efac3 --- /dev/null +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/SubtreeTagging/TagSubtree_WithoutDimensions.feature @@ -0,0 +1,216 @@ +@contentrepository @adapters=DoctrineDBAL +Feature: Tag subtree without dimensions + + As a user of the CR I want to tag a node aggregate and expect its descendants to also be tagged. + + These are the test cases without dimensions being involved + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository.Testing:Document': {} + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | workspaceTitle | "Live" | + | workspaceDescription | "The live workspace" | + | newContentStreamId | "cs-identifier" | + And the graph projection is fully up to date + And I am in the active content stream of workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.ContentRepository:Root" | + And the graph projection is fully up to date + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | + | a | Neos.ContentRepository.Testing:Document | root | a | + | a1 | Neos.ContentRepository.Testing:Document | a | a1 | + | a1a | Neos.ContentRepository.Testing:Document | a1 | a1a | + | a1a1 | Neos.ContentRepository.Testing:Document | a1a | a1a1 | + | a1a1a | Neos.ContentRepository.Testing:Document | a1a1 | a1a1a | + | a1a1b | Neos.ContentRepository.Testing:Document | a1a1 | a1a1b | + | a1a2 | Neos.ContentRepository.Testing:Document | a1a | a1a2 | + | a1b | Neos.ContentRepository.Testing:Document | a1 | a1b | + | a2 | Neos.ContentRepository.Testing:Document | a | a2 | + | b | Neos.ContentRepository.Testing:Document | root | b | + | b1 | Neos.ContentRepository.Testing:Document | b | b1 | + + Scenario: Tagging the same node twice with the same subtree tag is ignored + When the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + And the graph projection is fully up to date + Then I expect exactly 14 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 13 is of type "SubtreeWasTagged" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | affectedDimensionSpacePoints | [[]] | + | tag | "tag1" | + When the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + Then I expect exactly 14 events to be published on stream with prefix "ContentStream:cs-identifier" + + Scenario: Untagging a node without tags is ignored + Then I expect exactly 13 events to be published on stream with prefix "ContentStream:cs-identifier" + When the command UntagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + Then I expect exactly 13 events to be published on stream with prefix "ContentStream:cs-identifier" + + Scenario: Untagging a node that is only implicitly tagged (inherited) is ignored + When the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + And the graph projection is fully up to date + Then I expect exactly 14 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 13 is of type "SubtreeWasTagged" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | affectedDimensionSpacePoints | [[]] | + | tag | "tag1" | + When the command UntagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + Then I expect exactly 14 events to be published on stream with prefix "ContentStream:cs-identifier" + + Scenario: Tagging subtree with arbitrary strategy since dimensions are not involved + When the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + + Then I expect exactly 14 events to be published on stream with prefix "ContentStream:cs-identifier" + And event at index 13 is of type "SubtreeWasTagged" with payload: + | Key | Expected | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1" | + | affectedDimensionSpacePoints | [[]] | + | tag | "tag1" | + + When the graph projection is fully up to date + And I am in content stream "cs-identifier" + Then I expect the graph projection to consist of exactly 12 nodes + + When I am in the active content stream of workspace "live" and dimension space point {} + Then I expect the node with aggregate identifier "a1" to be explicitly tagged "tag1" + Then I expect the node with aggregate identifier "a1a" to inherit the tag "tag1" + Then I expect the node with aggregate identifier "a1a1" to inherit the tag "tag1" + Then I expect the node with aggregate identifier "a1a1b" to inherit the tag "tag1" + + When the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a1" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag1" | + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "b" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag2" | + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "b1" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag3" | + And the command TagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag4" | + And the graph projection is fully up to date + + When I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: + """ + a + a1 (tag1*) + a1a (tag4*,tag1) + a1a1 (tag1*,tag4) + a1a1a (tag1,tag4) + a1a1b (tag1,tag4) + a1a2 (tag1,tag4) + a1b (tag1) + a2 + """ + When I execute the findSubtree query for entry node aggregate id "b" I expect the following tree with tags: + """ + b (tag2*) + b1 (tag3*,tag2) + """ + + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "a1a" | + | newParentNodeAggregateId | "b1" | + And the graph projection is fully up to date + When I execute the findSubtree query for entry node aggregate id "a" I expect the following tree with tags: + """ + a + a1 (tag1*) + a1b (tag1) + a2 + """ + When I execute the findSubtree query for entry node aggregate id "b" I expect the following tree with tags: + """ + b (tag2*) + b1 (tag3*,tag2) + a1a (tag4*,tag3,tag2) + a1a1 (tag4*,tag1*,tag3,tag2) + a1a1a (tag4*,tag3,tag2) + a1a1b (tag4*,tag3,tag2) + a1a2 (tag4*,tag3,tag2) + """ + + When the command CreateNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a3" | + | nodeTypeName | "Neos.ContentRepository.Testing:Document" | + | parentNodeAggregateId | "a1a" | + When I execute the findSubtree query for entry node aggregate id "b" I expect the following tree with tags: + """ + b (tag2*) + b1 (tag3*,tag2) + a1a (tag4*,tag3,tag2) + a1a1 (tag4*,tag1*,tag3,tag2) + a1a1a (tag4*,tag3,tag2) + a1a1b (tag4*,tag3,tag2) + a1a2 (tag4*,tag3,tag2) + a1a3 (tag4,tag3,tag2) + """ + + When the command UntagSubtree is executed with payload: + | Key | Value | + | nodeAggregateId | "a1a" | + | nodeVariantSelectionStrategy | "allVariants" | + | tag | "tag4" | + When I execute the findSubtree query for entry node aggregate id "b" I expect the following tree with tags: + """ + b (tag2*) + b1 (tag3*,tag2) + a1a (tag3,tag2) + a1a1 (tag4*,tag1*,tag3,tag2) + a1a1a (tag4*,tag3,tag2) + a1a1b (tag4*,tag3,tag2) + a1a2 (tag4*,tag3,tag2) + a1a3 (tag3,tag2) + """ diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php index 9a821b57018..40ba511c16d 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php @@ -26,6 +26,9 @@ use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateDimensionsWereUpdated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\WorkspaceWasCreated; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasDiscarded; @@ -91,6 +94,8 @@ public function __construct() RootNodeAggregateWithNodeWasCreated::class, RootWorkspaceWasCreated::class, RootNodeAggregateDimensionsWereUpdated::class, + SubtreeWasTagged::class, + SubtreeWasUntagged::class, WorkspaceRebaseFailed::class, WorkspaceWasCreated::class, WorkspaceWasRenamed::class, @@ -162,6 +167,12 @@ public function denormalize(Event $event): EventInterface } assert(is_array($eventDataAsArray)); /** {@see EventInterface::fromArray()} */ - return $eventClassName::fromArray($eventDataAsArray); + $eventInstance = $eventClassName::fromArray($eventDataAsArray); + return match ($eventInstance::class) { + // upcast disabled / enabled events to the corresponding SubtreeTag events + NodeAggregateWasDisabled::class => new SubtreeWasTagged($eventInstance->contentStreamId, $eventInstance->nodeAggregateId, $eventInstance->affectedDimensionSpacePoints, SubtreeTag::disabled()), + NodeAggregateWasEnabled::class => new SubtreeWasUntagged($eventInstance->contentStreamId, $eventInstance->nodeAggregateId, $eventInstance->affectedDimensionSpacePoints, SubtreeTag::disabled()), + default => $eventInstance, + }; } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index 3321ff4ca79..50d3ade7798 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -41,8 +41,6 @@ use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFound; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Exception\ReferenceCannotBeSet; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Exception\NodeAggregateCurrentlyDisablesDimensionSpacePoint; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Exception\NodeAggregateCurrentlyDoesNotDisableDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\NodeVariation\Exception\DimensionSpacePointIsAlreadyOccupied; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyType; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php index 2604c46927b..fd79e8976e3 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php @@ -47,6 +47,9 @@ use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\UpdateRootNodeAggregateDimensions; use Neos\ContentRepository\Core\Feature\RootNodeCreation\RootNodeHandling; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\TagSubtree; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\UntagSubtree; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\SubtreeTagging; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; @@ -61,6 +64,7 @@ final class NodeAggregateCommandHandler implements CommandHandlerInterface use RootNodeHandling; use NodeCreation; use NodeDisabling; + use SubtreeTagging; use NodeModification; use NodeMove; use NodeReferencing; @@ -134,6 +138,8 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo => $this->handleUpdateRootNodeAggregateDimensions($command, $contentRepository), DisableNodeAggregate::class => $this->handleDisableNodeAggregate($command, $contentRepository), EnableNodeAggregate::class => $this->handleEnableNodeAggregate($command, $contentRepository), + TagSubtree::class => $this->handleTagSubtree($command, $contentRepository), + UntagSubtree::class => $this->handleUntagSubtree($command, $contentRepository), ChangeNodeAggregateName::class => $this->handleChangeNodeAggregateName($command, $contentRepository), }; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasDisabled.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasDisabled.php index 60639d39f69..1953496efde 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasDisabled.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasDisabled.php @@ -18,13 +18,15 @@ use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\Feature\Common\EmbedsContentStreamAndNodeAggregateId; use Neos\ContentRepository\Core\Feature\Common\PublishableToOtherContentStreamsInterface; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * A node aggregate was disabled * - * @api events are the persistence-API of the content repository + * @deprecated This event will never be emitted, it is up-casted to a corresponding {@see SubtreeWasTagged} event instead in the {@see EventNormalizer}. This implementation is just kept for backwards-compatibility + * @internal */ final class NodeAggregateWasDisabled implements EventInterface, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasEnabled.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasEnabled.php index 2fe7344543b..87a79a00045 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasEnabled.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Event/NodeAggregateWasEnabled.php @@ -16,15 +16,18 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\EventStore\EventInterface; +use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\Feature\Common\EmbedsContentStreamAndNodeAggregateId; use Neos\ContentRepository\Core\Feature\Common\PublishableToOtherContentStreamsInterface; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * A node aggregate was enabled * - * @api events are the persistence-API of the content repository + * @deprecated This event will never be emitted, it is up-casted to a corresponding {@see SubtreeWasUntagged} event instead in the {@see EventNormalizer}. This implementation is just kept for backwards-compatibility + * @internal */ final class NodeAggregateWasEnabled implements EventInterface, diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateCurrentlyDisablesDimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateCurrentlyDisablesDimensionSpacePoint.php deleted file mode 100644 index 66177dd2517..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeDisabling/Exception/NodeAggregateCurrentlyDisablesDimensionSpacePoint.php +++ /dev/null @@ -1,24 +0,0 @@ -coveredDimensionSpacePoint ); - - if ($nodeAggregate->disablesDimensionSpacePoint($command->coveredDimensionSpacePoint)) { + if ($nodeAggregate->getDimensionSpacePointsTaggedWith(SubtreeTag::disabled())->contains($command->coveredDimensionSpacePoint)) { // already disabled, so we can return a no-operation. return EventsToPublish::empty(); } @@ -74,10 +74,11 @@ private function handleDisableNodeAggregate( ); $events = Events::with( - new NodeAggregateWasDisabled( + new SubtreeWasTagged( $contentStreamId, $command->nodeAggregateId, $affectedDimensionSpacePoints, + SubtreeTag::disabled(), ), ); @@ -115,8 +116,7 @@ public function handleEnableNodeAggregate( $nodeAggregate, $command->coveredDimensionSpacePoint ); - - if (!$nodeAggregate->disablesDimensionSpacePoint($command->coveredDimensionSpacePoint)) { + if (!$nodeAggregate->getDimensionSpacePointsTaggedWith(SubtreeTag::disabled())->contains($command->coveredDimensionSpacePoint)) { // already enabled, so we can return a no-operation. return EventsToPublish::empty(); } @@ -129,10 +129,11 @@ public function handleEnableNodeAggregate( ); $events = Events::with( - new NodeAggregateWasEnabled( + new SubtreeWasUntagged( $contentStreamId, $command->nodeAggregateId, $affectedDimensionSpacePoints, + SubtreeTag::disabled(), ) ); diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php index 92b138f0c92..80258dce6e8 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Command/SetSerializedNodeProperties.php @@ -18,10 +18,10 @@ use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; -use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php new file mode 100644 index 00000000000..2f6d1d3c562 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/TagSubtree.php @@ -0,0 +1,105 @@ + $array + */ + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeAggregateId::fromString($array['nodeAggregateId']), + DimensionSpacePoint::fromArray($array['coveredDimensionSpacePoint']), + NodeVariantSelectionStrategy::from($array['nodeVariantSelectionStrategy']), + SubtreeTag::fromString($array['tag']), + ); + } + + public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName, ContentStreamId $targetContentStreamId): self + { + return new self( + $targetWorkspaceName, + $this->nodeAggregateId, + $this->coveredDimensionSpacePoint, + $this->nodeVariantSelectionStrategy, + $this->tag, + ); + } + + public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool + { + return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) + && $this->coveredDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php new file mode 100644 index 00000000000..498f75fc193 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Command/UntagSubtree.php @@ -0,0 +1,106 @@ + $array + */ + public static function fromArray(array $array): self + { + return new self( + WorkspaceName::fromString($array['workspaceName']), + NodeAggregateId::fromString($array['nodeAggregateId']), + DimensionSpacePoint::fromArray($array['coveredDimensionSpacePoint']), + NodeVariantSelectionStrategy::from($array['nodeVariantSelectionStrategy']), + SubtreeTag::fromString($array['tag']), + ); + } + + public function createCopyForWorkspace(WorkspaceName $targetWorkspaceName, ContentStreamId $targetContentStreamId): self + { + return new self( + $targetWorkspaceName, + $this->nodeAggregateId, + $this->coveredDimensionSpacePoint, + $this->nodeVariantSelectionStrategy, + $this->tag, + ); + } + + public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool + { + return $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId) + && $this->coveredDimensionSpacePoint->equals($nodeIdToPublish->dimensionSpacePoint); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Dto/SubtreeTag.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Dto/SubtreeTag.php new file mode 100644 index 00000000000..d15b558b19e --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Dto/SubtreeTag.php @@ -0,0 +1,66 @@ + + */ + private static array $instances = []; + + private static function instance(string $value): self + { + if (!array_key_exists($value, self::$instances)) { + self::$instances[$value] = new self($value); + } + return self::$instances[$value]; + } + + private function __construct(public string $value) + { + $regexPattern = '/^[a-z0-9_.-]{1,36}$/'; + if (preg_match($regexPattern, $value) !== 1) { + throw new \InvalidArgumentException(sprintf('The SubtreeTag value "%s" does not adhere to the regular expression "%s"', $value, $regexPattern), 1695467813); + } + } + + public static function fromString(string $value): self + { + return self::instance($value); + } + + public static function disabled(): self + { + return self::instance('disabled'); + } + + public function equals(self $other): bool + { + return $this === $other; + } + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Dto/SubtreeTags.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Dto/SubtreeTags.php new file mode 100644 index 00000000000..0cdafd7cad7 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Dto/SubtreeTags.php @@ -0,0 +1,120 @@ + + */ +final readonly class SubtreeTags implements \IteratorAggregate, \Countable, \JsonSerializable +{ + /** + * @var array + */ + private array $tags; + + + private function __construct(SubtreeTag ...$tags) + { + $tagsByValue = []; + foreach ($tags as $tag) { + $tagsByValue[$tag->value] = $tag; + } + $this->tags = $tagsByValue; + } + + public static function createEmpty(): self + { + return new self(); + } + + /** + * @param array $tags + */ + public static function fromArray(array $tags): self + { + return new self(...$tags); + } + + public static function fromStrings(string ...$tags): self + { + return new self(...array_map(SubtreeTag::fromString(...), $tags)); + } + + public function without(SubtreeTag $subtreeTagToRemove): self + { + if (!$this->contain($subtreeTagToRemove)) { + return $this; + } + return new self(...array_filter($this->tags, static fn (SubtreeTag $tag) => !$tag->equals($subtreeTagToRemove))); + } + + public function isEmpty(): bool + { + return $this->tags === []; + } + + public function count(): int + { + return count($this->tags); + } + + public function contain(SubtreeTag $tag): bool + { + return array_key_exists($tag->value, $this->tags); + } + + public function intersection(self $other): self + { + return self::fromArray(array_intersect_key($this->tags, $other->tags)); + } + + public function merge(self $other): self + { + return self::fromArray(array_merge($this->tags, $other->tags)); + } + + /** + * @param \Closure(SubtreeTag): mixed $callback + * @return array + */ + public function map(\Closure $callback): array + { + return array_map($callback, array_values($this->tags)); + } + + /** + * @return array + */ + public function toStringArray(): array + { + return $this->map(static fn (SubtreeTag $tag) => $tag->value); + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator(array_values($this->tags)); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return array_values($this->tags); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Event/SubtreeWasTagged.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Event/SubtreeWasTagged.php new file mode 100644 index 00000000000..7d8571e4b34 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Event/SubtreeWasTagged.php @@ -0,0 +1,83 @@ +contentStreamId; + } + + public function getNodeAggregateId(): NodeAggregateId + { + return $this->nodeAggregateId; + } + + public function createCopyForContentStream(ContentStreamId $targetContentStreamId): self + { + return new self( + $targetContentStreamId, + $this->nodeAggregateId, + $this->affectedDimensionSpacePoints, + $this->tag, + ); + } + + public static function fromArray(array $values): EventInterface + { + return new self( + ContentStreamId::fromString($values['contentStreamId']), + NodeAggregateId::fromString($values['nodeAggregateId']), + DimensionSpacePointSet::fromArray($values['affectedDimensionSpacePoints']), + SubtreeTag::fromString($values['tag']), + ); + } + + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Event/SubtreeWasUntagged.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Event/SubtreeWasUntagged.php new file mode 100644 index 00000000000..68cda9ccc8b --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/Event/SubtreeWasUntagged.php @@ -0,0 +1,84 @@ +contentStreamId; + } + + public function getNodeAggregateId(): NodeAggregateId + { + return $this->nodeAggregateId; + } + + public function createCopyForContentStream(ContentStreamId $targetContentStreamId): self + { + return new self( + $targetContentStreamId, + $this->nodeAggregateId, + $this->affectedDimensionSpacePoints, + $this->tag, + ); + } + + public static function fromArray(array $values): EventInterface + { + return new self( + ContentStreamId::fromString($values['contentStreamId']), + NodeAggregateId::fromString($values['nodeAggregateId']), + DimensionSpacePointSet::fromArray($values['affectedDimensionSpacePoints']), + SubtreeTag::fromString($values['tag']), + ); + } + + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php new file mode 100644 index 00000000000..05f3c58248e --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/SubtreeTagging/SubtreeTagging.php @@ -0,0 +1,122 @@ +requireContentStream($command->workspaceName, $contentRepository); + $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); + $nodeAggregate = $this->requireProjectedNodeAggregate($contentStreamId, $command->nodeAggregateId, $contentRepository); + $this->requireNodeAggregateToCoverDimensionSpacePoint( + $nodeAggregate, + $command->coveredDimensionSpacePoint + ); + + if ($nodeAggregate->getDimensionSpacePointsTaggedWith($command->tag)->contains($command->coveredDimensionSpacePoint)) { + // already explicitly tagged with the same Subtree Tag, so we can return a no-operation. + return EventsToPublish::empty(); + } + + $affectedDimensionSpacePoints = $command->nodeVariantSelectionStrategy + ->resolveAffectedDimensionSpacePoints( + $command->coveredDimensionSpacePoint, + $nodeAggregate, + $this->getInterDimensionalVariationGraph() + ); + + $events = Events::with( + new SubtreeWasTagged( + $contentStreamId, + $command->nodeAggregateId, + $affectedDimensionSpacePoints, + $command->tag, + ), + ); + + return new EventsToPublish( + ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ->getEventStreamName(), + NodeAggregateEventPublisher::enrichWithCommand( + $command, + $events + ), + ExpectedVersion::ANY() + ); + } + + public function handleUntagSubtree(UntagSubtree $command, ContentRepository $contentRepository): EventsToPublish + { + $contentStreamId = $this->requireContentStream($command->workspaceName, $contentRepository); + $this->requireDimensionSpacePointToExist($command->coveredDimensionSpacePoint); + $nodeAggregate = $this->requireProjectedNodeAggregate( + $contentStreamId, + $command->nodeAggregateId, + $contentRepository + ); + $this->requireNodeAggregateToCoverDimensionSpacePoint( + $nodeAggregate, + $command->coveredDimensionSpacePoint + ); + + if (!$nodeAggregate->getDimensionSpacePointsTaggedWith($command->tag)->contains($command->coveredDimensionSpacePoint)) { + // not explicitly tagged with the given Subtree Tag, so we can return a no-operation. + return EventsToPublish::empty(); + } + + $affectedDimensionSpacePoints = $command->nodeVariantSelectionStrategy + ->resolveAffectedDimensionSpacePoints( + $command->coveredDimensionSpacePoint, + $nodeAggregate, + $this->getInterDimensionalVariationGraph() + ); + + $events = Events::with( + new SubtreeWasUntagged( + $contentStreamId, + $command->nodeAggregateId, + $affectedDimensionSpacePoints, + $command->tag, + ) + ); + + return new EventsToPublish( + ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(), + NodeAggregateEventPublisher::enrichWithCommand($command, $events), + ExpectedVersion::ANY() + ); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 623f23212f3..b1b299c6a29 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -524,7 +524,7 @@ function () use ($matchingCommands, $contentRepository, $baseWorkspace, $command if (!($matchingCommand instanceof RebasableToOtherWorkspaceInterface)) { throw new \RuntimeException( 'ERROR: The command ' . get_class($matchingCommand) - . ' does not implement RebasableToOtherContentStreamsInterface; but it should!' + . ' does not implement ' . RebasableToOtherWorkspaceInterface::class . '; but it should!' ); } @@ -661,7 +661,7 @@ function () use ($commandsToKeep, $contentRepository, $baseWorkspace, $command): if (!($matchingCommand instanceof RebasableToOtherWorkspaceInterface)) { throw new \RuntimeException( 'ERROR: The command ' . get_class($matchingCommand) - . ' does not implement RebasableToOtherContentStreamsInterface; but it should!' + . ' does not implement ' . RebasableToOtherWorkspaceInterface::class . '; but it should!' ); } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/DimensionSpacePointsBySubtreeTags.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/DimensionSpacePointsBySubtreeTags.php new file mode 100644 index 00000000000..0df86afe932 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/DimensionSpacePointsBySubtreeTags.php @@ -0,0 +1,60 @@ + $dimensionSpacePointsBySubtreeTags + */ + public function __construct( + private array $dimensionSpacePointsBySubtreeTags, + ) { + } + + public static function create(): self + { + return new self([]); + } + + public function withSubtreeTagAndDimensionSpacePoint(SubtreeTag $subtreeTag, DimensionSpacePoint $dimensionSpacePoint): self + { + $dimensionSpacePointsBySubtreeTags = $this->dimensionSpacePointsBySubtreeTags; + if (!array_key_exists($subtreeTag->value, $dimensionSpacePointsBySubtreeTags)) { + $dimensionSpacePointsBySubtreeTags[$subtreeTag->value] = DimensionSpacePointSet::fromArray([]); + } + if ($dimensionSpacePointsBySubtreeTags[$subtreeTag->value]->contains($dimensionSpacePoint)) { + return $this; + } + $dimensionSpacePointsBySubtreeTags[$subtreeTag->value] = $dimensionSpacePointsBySubtreeTags[$subtreeTag->value]->getUnion(DimensionSpacePointSet::fromArray([$dimensionSpacePoint])); + return new self($dimensionSpacePointsBySubtreeTags); + } + + /** + * Returns the dimension space points the specified $subtreeTag is _explicitly_ set in, or an empty set if none of the variants are tagged with $subtreeTag + */ + public function forSubtreeTag(SubtreeTag $subtreeTag): DimensionSpacePointSet + { + return $this->dimensionSpacePointsBySubtreeTags[$subtreeTag->value] ?? DimensionSpacePointSet::fromArray([]); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->dimensionSpacePointsBySubtreeTags; + } +} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php index 4bb69d0c16e..2318c0fe515 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Node.php @@ -47,6 +47,7 @@ * @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 NodeTags $tags explicit and inherited SubtreeTags of this node * @param Timestamps $timestamps Creation and modification timestamps of this node */ private function __construct( @@ -58,6 +59,7 @@ private function __construct( public ?NodeType $nodeType, public PropertyCollection $properties, public ?NodeName $nodeName, + public NodeTags $tags, public Timestamps $timestamps, ) { if ($this->classification->isTethered() && $this->nodeName === null) { @@ -68,9 +70,9 @@ private function __construct( /** * @internal The signature of this method can change in the future! */ - public static function create(ContentSubgraphIdentity $subgraphIdentity, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeAggregateClassification $classification, NodeTypeName $nodeTypeName, ?NodeType $nodeType, PropertyCollection $properties, ?NodeName $nodeName, Timestamps $timestamps): self + public static function create(ContentSubgraphIdentity $subgraphIdentity, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, NodeAggregateClassification $classification, NodeTypeName $nodeTypeName, ?NodeType $nodeType, PropertyCollection $properties, ?NodeName $nodeName, NodeTags $tags, Timestamps $timestamps): self { - return new self($subgraphIdentity, $nodeAggregateId, $originDimensionSpacePoint, $classification, $nodeTypeName, $nodeType, $properties, $nodeName, $timestamps); + return new self($subgraphIdentity, $nodeAggregateId, $originDimensionSpacePoint, $classification, $nodeTypeName, $nodeType, $properties, $nodeName, $tags, $timestamps); } /** diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php index e17410810c6..b4066da3db4 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeAggregate.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint; @@ -50,6 +51,20 @@ */ final class NodeAggregate { + /** + * @param ContentStreamId $contentStreamId ID of the content stream of this node aggregate + * @param NodeAggregateId $nodeAggregateId ID of this node aggregate + * @param NodeAggregateClassification $classification whether this aggregate represents a root, regular or tethered node + * @param NodeTypeName $nodeTypeName name of the node type of this aggregate + * @param NodeName|null $nodeName optional name of this aggregate + * @param OriginDimensionSpacePointSet $occupiedDimensionSpacePoints dimension space points this aggregate occupies + * @param array $nodesByOccupiedDimensionSpacePoint + * @param CoverageByOrigin $coverageByOccupant + * @param DimensionSpacePointSet $coveredDimensionSpacePoints + * @param array $nodesByCoveredDimensionSpacePoint + * @param OriginByCoverage $occupationByCovered + * @param DimensionSpacePointsBySubtreeTags $dimensionSpacePointsBySubtreeTags dimension space points for every subtree tag this aggregate is *explicitly* tagged with (excluding inherited tags) + */ public function __construct( public readonly ContentStreamId $contentStreamId, public readonly NodeAggregateId $nodeAggregateId, @@ -57,18 +72,12 @@ public function __construct( public readonly NodeTypeName $nodeTypeName, public readonly ?NodeName $nodeName, public readonly OriginDimensionSpacePointSet $occupiedDimensionSpacePoints, - /** @var array */ private readonly array $nodesByOccupiedDimensionSpacePoint, private readonly CoverageByOrigin $coverageByOccupant, public readonly DimensionSpacePointSet $coveredDimensionSpacePoints, - /** @var array */ private readonly array $nodesByCoveredDimensionSpacePoint, private readonly OriginByCoverage $occupationByCovered, - /** - * The dimension space point set this node aggregate disables. - * This is *not* necessarily the set it is disabled in, since that is determined by its ancestors - */ - public readonly DimensionSpacePointSet $disabledDimensionSpacePoints + private readonly DimensionSpacePointsBySubtreeTags $dimensionSpacePointsBySubtreeTags, ) { } @@ -144,8 +153,14 @@ public function getOccupationByCovered(DimensionSpacePoint $coveredDimensionSpac return $occupation; } - public function disablesDimensionSpacePoint(DimensionSpacePoint $dimensionSpacePoint): bool + /** + * Returns the dimension space points this aggregate is *explicitly* tagged in with the specified $subtreeTag + * NOTE: This won't respect inherited subtree tags! + * + * @internal This is a low level concept that is not meant to be used outside the core or tests + */ + public function getDimensionSpacePointsTaggedWith(SubtreeTag $subtreeTag): DimensionSpacePointSet { - return $this->disabledDimensionSpacePoints->contains($dimensionSpacePoint); + return $this->dimensionSpacePointsBySubtreeTags->forSubtreeTag($subtreeTag); } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeTags.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeTags.php new file mode 100644 index 00000000000..8317643ab22 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/NodeTags.php @@ -0,0 +1,148 @@ + + * @api + */ +final readonly class NodeTags implements \IteratorAggregate, \Countable, \JsonSerializable +{ + private function __construct( + private SubtreeTags $tags, + private SubtreeTags $inheritedTags, + ) { + } + + public static function create(SubtreeTags $tags, SubtreeTags $inheritedTags): self + { + $intersection = $tags->intersection($inheritedTags); + if (!$intersection->isEmpty()) { + throw new \InvalidArgumentException(sprintf('tags and inherited tags must not contain the same values, but the following tag%s appear%s in both sets: "%s"', $intersection->count() === 1 ? '' : 's', $intersection->count() === 1 ? 's' : '', implode('", "', $intersection->toStringArray())), 1709891871); + } + return new self($tags, $inheritedTags); + } + + public static function createEmpty(): self + { + return new self(SubtreeTags::createEmpty(), SubtreeTags::createEmpty()); + } + + public function without(SubtreeTag $subtreeTagToRemove): self + { + if (!$this->tags->contain($subtreeTagToRemove) && !$this->inheritedTags->contain($subtreeTagToRemove)) { + return $this; + } + return new self($this->tags->without($subtreeTagToRemove), $this->inheritedTags->without($subtreeTagToRemove)); + } + + public function withoutInherited(): self + { + if ($this->inheritedTags->isEmpty()) { + return $this; + } + return self::create($this->tags, SubtreeTags::createEmpty()); + } + + public function onlyInherited(): self + { + if ($this->tags->isEmpty()) { + return $this; + } + return self::create(SubtreeTags::createEmpty(), $this->inheritedTags); + } + + public function isEmpty(): bool + { + return $this->tags->isEmpty() && $this->inheritedTags->isEmpty(); + } + + public function count(): int + { + return $this->tags->count() + $this->inheritedTags->count(); + } + + public function contain(SubtreeTag $tag): bool + { + return $this->tags->contain($tag) || $this->inheritedTags->contain($tag); + } + + public function all(): SubtreeTags + { + return SubtreeTags::fromArray([...iterator_to_array($this->tags), ...iterator_to_array($this->inheritedTags)]); + } + + /** + * @param \Closure(SubtreeTag $tag, bool $inherited): mixed $callback + * @return array + */ + public function map(\Closure $callback): array + { + return [ + ...array_map(static fn (SubtreeTag $tag) => $callback($tag, false), iterator_to_array($this->tags)), + ...array_map(static fn (SubtreeTag $tag) => $callback($tag, true), iterator_to_array($this->inheritedTags)), + ]; + } + + /** + * @return array + */ + public function toStringArray(): array + { + return $this->map(static fn (SubtreeTag $tag) => $tag->value); + } + + public function getIterator(): Traversable + { + foreach ($this->tags as $tag) { + yield $tag; + } + foreach ($this->inheritedTags as $tag) { + yield $tag; + } + } + + + + /** + * The JSON representation contains the tag names as keys and a value of `true` for explicitly set tags and `null` for inherited tags. + * Example: ['someExplicitlySetTag' => true, 'someInheritedTag' => null] + * + * @return array + */ + public function jsonSerialize(): array + { + $convertedSubtreeTags = []; + foreach ($this->tags as $tag) { + $convertedSubtreeTags[$tag->value] = true; + } + foreach ($this->inheritedTags as $tag) { + $convertedSubtreeTags[$tag->value] = null; + } + return $convertedSubtreeTags; + } +} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectionRunner.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectionRunner.php index cfabeca608e..bec4bbb7321 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectionRunner.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectionRunner.php @@ -45,8 +45,7 @@ public function run(): Result ->nodeAggregatesAreConsistentlyClassifiedPerContentStream()); $result->merge($this->projectionIntegrityViolationDetector->referenceIntegrityIsProvided()); $result->merge($this->projectionIntegrityViolationDetector->referencesAreDistinctlySorted()); - $result->merge($this->projectionIntegrityViolationDetector->restrictionIntegrityIsProvided()); - $result->merge($this->projectionIntegrityViolationDetector->restrictionsArePropagatedRecursively()); + $result->merge($this->projectionIntegrityViolationDetector->subtreeTagsAreInherited()); $result->merge($this->projectionIntegrityViolationDetector->siblingsAreDistinctlySorted()); return $result; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectorInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectorInterface.php index 416a8b62e75..82f7a9c2f9c 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectorInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/ProjectionIntegrityViolationDetectorInterface.php @@ -30,12 +30,10 @@ interface ProjectionIntegrityViolationDetectorInterface public const ERROR_CODE_NODE_AGGREGATE_IS_AMBIGUOUSLY_CLASSIFIED = 1597825384; public const ERROR_CODE_NODE_IS_DISCONNECTED_FROM_THE_ROOT = 1597754245; public const ERROR_CODE_NODE_DOES_NOT_COVER_ITS_ORIGIN = 1597828607; - public const ERROR_CODE_NODE_HAS_MISSING_RESTRICTION = 1597837797; - public const ERROR_CODE_RESTRICTION_INTEGRITY_IS_COMPROMISED = 1597846598; + public const ERROR_CODE_NODE_HAS_MISSING_SUBTREE_TAG = 1597837797; public const ERROR_CODE_HIERARCHY_INTEGRITY_IS_COMPROMISED = 1597909228; public const ERROR_CODE_SIBLINGS_ARE_AMBIGUOUSLY_SORTED = 1597910918; public const ERROR_CODE_REFERENCE_INTEGRITY_IS_COMPROMISED = 1597919585; - public const ERROR_CODE_REFERENCES_ARE_AMBIGUOUSLY_SORTED = 1597922989; public const ERROR_CODE_TETHERED_NODE_IS_UNNAMED = 1597923103; public const ERROR_CODE_NODE_HAS_MULTIPLE_PARENTS = 1597925698; @@ -89,38 +87,31 @@ public function siblingsAreDistinctlySorted(): Result; public function tetheredNodesAreNamed(): Result; /** - * A is marked as hidden, so B and C should have incoming restriction edges. - * This test should fail if e.g. in the example below, the restriction edge from A to C is missing. - * - * ┌─────┐ - * │ A │━━┓ - * └─────┘ ┃ - * │ ┃ - * │ ┃ - * ┌─────┐ ┃ - * │ B │◀━┛ - * └─────┘ ┃ - * │ - * │ ┃ <-- this Restriction Edge is missing. - * ┌─────┐ - * │ C │◀ ┛ - * └─────┘ - */ - public function restrictionsArePropagatedRecursively(): Result; - - /** - * Checks that the restriction edges are connected at source (e.g. to "A") and at destination (e.g. to "B") - * - * ┌─────┐ - * │ A │━━┓ <-- checks that A exists (for each restriction edge) - * └─────┘ ┃ - * │ ┃ - * │ ┃ - * ┌─────┐ ┃ - * │ B │◀━┛ <-- checks that B exists (for each restriction edge) - * └─────┘ + * A is tagged with a subtree tag, so B and C should inherit that subtree tag (or explicitly have it set) + * This test should fail if e.g. in the example below, C is missing the "foo" tag (* = explicitly set, = inherited): + * + * ┌────────────────────────┐ + * │ A │ + * │ │ + * │ SubtreeTags: foo* │ + * │ │ + * └───────────┬────────────┘ + * │ + * ┌───────────┴────────────┐ + * │ B │ + * │ │ + * │ SubtreeTags: foo, bar* │ + * │ │ + * └───────────┬────────────┘ + * │ + * ┌───────────┴────────────┐ + * │ C │ + * │ │ + * │ SubtreeTags: bar │ <-- is missing the inherited "foo" subtree tag + * │ │ + * └────────────────────────┘ */ - public function restrictionIntegrityIsProvided(): Result; + public function subtreeTagsAreInherited(): Result; /** * Checks that the reference edges are connected at source (e.g. to "A") and at destination (e.g. to "B") diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php index 8a2ab3168bc..cb01f67a2cf 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/VisibilityConstraints.php @@ -14,44 +14,51 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; + /** - * The context parameters value object + * The visibility constraints define a context in which the content graph is accessed. * - * Maybe future: "Node Filter" tree or so as replacement of ReadNodePrivilege? + * For example: In the `frontend` context, nodes with the `disabled` tag are excluded. In the `backend` context {@see self::withoutRestrictions()} they are included * * @api */ -final class VisibilityConstraints implements \JsonSerializable +final readonly class VisibilityConstraints implements \JsonSerializable { - protected bool $disabledContentShown = false; - - private function __construct(bool $disabledContentShown) - { - $this->disabledContentShown = $disabledContentShown; + /** + * @param SubtreeTags $tagConstraints A set of {@see SubtreeTag} instances that will be _excluded_ from the results of any content graph query + */ + private function __construct( + public SubtreeTags $tagConstraints, + ) { } public function isDisabledContentShown(): bool { - return $this->disabledContentShown; + return $this->tagConstraints->contain(SubtreeTag::disabled()); } public function getHash(): string { - return md5('disabled' . $this->disabledContentShown); + return md5(implode('|', $this->tagConstraints->toStringArray())); } - public static function withoutRestrictions(): VisibilityConstraints + public static function withoutRestrictions(): self { - return new VisibilityConstraints(true); + return new self(SubtreeTags::createEmpty()); } public static function frontend(): VisibilityConstraints { - return new VisibilityConstraints(false); + return new self(SubtreeTags::fromStrings('disabled')); } - public function jsonSerialize(): string + /** + * @return array + */ + public function jsonSerialize(): array { - return $this->getHash(); + return get_object_vars($this); } } diff --git a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenState.php b/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenState.php deleted file mode 100644 index 48d5974df38..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenState.php +++ /dev/null @@ -1,38 +0,0 @@ -client->getConnection(); - $result = $connection->executeQuery( - ' - SELECT * FROM ' . $this->tableName . ' - WHERE contentstreamid = :contentStreamId - AND dimensionspacepointhash = :dimensionSpacePointHash - AND nodeaggregateid = :nodeAggregateId - ', - [ - 'contentStreamId' => $contentStreamId->value, - 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, - 'nodeAggregateId' => $nodeAggregateId->value, - ] - )->fetch(); - - if (is_array($result)) { - return new NodeHiddenState(true); - } else { - return new NodeHiddenState(false); - } - } -} diff --git a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjection.php b/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjection.php deleted file mode 100644 index 350c88fcd65..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjection.php +++ /dev/null @@ -1,266 +0,0 @@ - - */ -class NodeHiddenStateProjection implements ProjectionInterface -{ - private ?NodeHiddenStateFinder $nodeHiddenStateFinder; - private DbalCheckpointStorage $checkpointStorage; - - public function __construct( - private readonly DbalClientInterface $dbalClient, - private readonly string $tableName - ) { - $this->checkpointStorage = new DbalCheckpointStorage( - $this->dbalClient->getConnection(), - $this->tableName . '_checkpoint', - self::class - ); - } - - public function setUp(): void - { - foreach ($this->determineRequiredSqlStatements() as $statement) { - $this->getDatabaseConnection()->executeStatement($statement); - } - $this->checkpointStorage->setUp(); - } - - public function status(): ProjectionStatus - { - $checkpointStorageStatus = $this->checkpointStorage->status(); - if ($checkpointStorageStatus->type === CheckpointStorageStatusType::ERROR) { - return ProjectionStatus::error($checkpointStorageStatus->details); - } - if ($checkpointStorageStatus->type === CheckpointStorageStatusType::SETUP_REQUIRED) { - return ProjectionStatus::setupRequired($checkpointStorageStatus->details); - } - try { - $this->getDatabaseConnection()->connect(); - } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); - } - try { - $requiredSqlStatements = $this->determineRequiredSqlStatements(); - } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); - } - if ($requiredSqlStatements !== []) { - return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); - } - return ProjectionStatus::ok(); - } - - /** - * @return array - */ - private function determineRequiredSqlStatements(): array - { - $connection = $this->dbalClient->getConnection(); - $schemaManager = $connection->getSchemaManager(); - if (!$schemaManager instanceof AbstractSchemaManager) { - throw new \RuntimeException('Failed to retrieve Schema Manager', 1625653914); - } - - $nodeHiddenStateTable = new Table($this->tableName, [ - DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotNull(true), - DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotNull(false), - DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotNull(false), - DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotNull(false), - (new Column('hidden', Type::getType(Types::BOOLEAN)))->setDefault(false)->setNotnull(false) - ]); - $nodeHiddenStateTable->setPrimaryKey( - ['contentstreamid', 'nodeaggregateid', 'dimensionspacepointhash'] - ); - - $schema = DbalSchemaFactory::createSchemaWithTables($schemaManager, [$nodeHiddenStateTable]); - return DbalSchemaDiff::determineRequiredSqlStatements($connection, $schema); - } - - public function reset(): void - { - $this->getDatabaseConnection()->exec('TRUNCATE ' . $this->tableName); - $this->checkpointStorage->acquireLock(); - $this->checkpointStorage->updateAndReleaseLock(SequenceNumber::none()); - } - - public function canHandle(EventInterface $event): bool - { - return in_array($event::class, [ - NodeAggregateWasDisabled::class, - NodeAggregateWasEnabled::class, - ContentStreamWasForked::class, - DimensionSpacePointWasMoved::class - ]); - } - - public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void - { - match ($event::class) { - NodeAggregateWasDisabled::class => $this->whenNodeAggregateWasDisabled($event), - NodeAggregateWasEnabled::class => $this->whenNodeAggregateWasEnabled($event), - ContentStreamWasForked::class => $this->whenContentStreamWasForked($event), - DimensionSpacePointWasMoved::class => $this->whenDimensionSpacePointWasMoved($event), - default => throw new \InvalidArgumentException(sprintf('Unsupported event %s', get_debug_type($event))), - }; - } - - public function getCheckpointStorage(): DbalCheckpointStorage - { - return $this->checkpointStorage; - } - - public function getState(): NodeHiddenStateFinder - { - if (!isset($this->nodeHiddenStateFinder)) { - $this->nodeHiddenStateFinder = new NodeHiddenStateFinder( - $this->dbalClient, - $this->tableName - ); - } - return $this->nodeHiddenStateFinder; - } - - - private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void - { - $this->transactional(function () use ($event) { - foreach ($event->affectedDimensionSpacePoints as $dimensionSpacePoint) { - if ( - !$this->getState()->findHiddenState( - $event->contentStreamId, - $dimensionSpacePoint, - $event->nodeAggregateId - )->isHidden - ) { - $nodeHiddenState = new NodeHiddenStateRecord( - $event->contentStreamId, - $event->nodeAggregateId, - $dimensionSpacePoint, - true - ); - $nodeHiddenState->addToDatabase($this->getDatabaseConnection(), $this->tableName); - } - } - }); - } - - private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void - { - $this->getDatabaseConnection()->executeQuery( - ' - DELETE FROM - ' . $this->tableName . ' - WHERE - contentstreamid = :contentStreamId - AND nodeaggregateid = :nodeAggregateId - AND dimensionspacepointhash IN (:dimensionSpacePointHashes) - ', - [ - 'contentStreamId' => $event->contentStreamId->value, - 'nodeAggregateId' => $event->nodeAggregateId->value, - 'dimensionSpacePointHashes' => $event->affectedDimensionSpacePoints->getPointHashes() - ], - [ - 'dimensionSpacePointHashes' => Connection::PARAM_STR_ARRAY - ] - ); - } - - private function whenContentStreamWasForked(ContentStreamWasForked $event): void - { - $this->transactional(function () use ($event) { - $this->getDatabaseConnection()->executeUpdate(' - INSERT INTO ' . $this->tableName . ' ( - contentstreamid, - nodeaggregateid, - dimensionspacepoint, - dimensionspacepointhash, - hidden - ) - SELECT - "' . $event->newContentStreamId->value . '" AS contentstreamid, - nodeaggregateid, - dimensionspacepoint, - dimensionspacepointhash, - hidden - FROM - ' . $this->tableName . ' h - WHERE h.contentstreamid = :sourceContentStreamId - ', [ - 'sourceContentStreamId' => $event->sourceContentStreamId->value - ]); - }); - } - - private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $event): void - { - $this->transactional(function () use ($event) { - $this->getDatabaseConnection()->executeStatement( - ' - UPDATE ' . $this->tableName . ' nhs - SET - nhs.dimensionspacepoint = :newDimensionSpacePoint, - nhs.dimensionspacepointhash = :newDimensionSpacePointHash - WHERE - nhs.dimensionspacepointhash = :originalDimensionSpacePointHash - AND nhs.contentstreamid = :contentStreamId - ', - [ - 'originalDimensionSpacePointHash' => $event->source->hash, - 'newDimensionSpacePointHash' => $event->target->hash, - 'newDimensionSpacePoint' => $event->target->toJson(), - 'contentStreamId' => $event->contentStreamId->value - ] - ); - }); - } - - private function transactional(\Closure $operations): void - { - $this->getDatabaseConnection()->transactional($operations); - } - - private function getDatabaseConnection(): Connection - { - return $this->dbalClient->getConnection(); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjectionFactory.php b/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjectionFactory.php deleted file mode 100644 index 3436706b939..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateProjectionFactory.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @internal - */ -class NodeHiddenStateProjectionFactory implements ProjectionFactoryInterface -{ - public function __construct( - private readonly DbalClientInterface $dbalClient - ) { - } - - public function build( - ProjectionFactoryDependencies $projectionFactoryDependencies, - array $options, - ): NodeHiddenStateProjection { - $projectionShortName = strtolower(str_replace( - 'Projection', - '', - (new \ReflectionClass(NodeHiddenStateProjection::class))->getShortName() - )); - - return new NodeHiddenStateProjection( - $this->dbalClient, - sprintf( - 'cr_%s_p_%s', - $projectionFactoryDependencies->contentRepositoryId->value, - $projectionShortName - ), - ); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateRecord.php b/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateRecord.php deleted file mode 100644 index 8324162fbac..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/NodeHiddenState/NodeHiddenStateRecord.php +++ /dev/null @@ -1,82 +0,0 @@ -contentStreamId = $contentStreamId; - $this->nodeAggregateId = $nodeAggregateId; - $this->dimensionSpacePoint = $dimensionSpacePoint; - $this->hidden = $hidden; - } - - public function addToDatabase(Connection $databaseConnection, string $tableName): void - { - if (is_null($this->contentStreamId)) { - throw new \BadMethodCallException( - 'Cannot add NodeHiddenState to database without a contentStreamId', - 1645383933 - ); - } - if (is_null($this->nodeAggregateId)) { - throw new \BadMethodCallException( - 'Cannot add NodeHiddenState to database without a nodeAggregateId', - 1645383950 - ); - } - if (is_null($this->dimensionSpacePoint)) { - throw new \BadMethodCallException( - 'Cannot add NodeHiddenState to database without a dimensionSpacePoint', - 1645383962 - ); - } - $databaseConnection->insert($tableName, [ - 'contentStreamId' => $this->contentStreamId->value, - 'nodeAggregateId' => $this->nodeAggregateId->value, - 'dimensionSpacePoint' => $this->dimensionSpacePoint->toJson(), - 'dimensionSpacePointHash' => $this->dimensionSpacePoint->hash, - 'hidden' => (int)$this->hidden, - ]); - } -} diff --git a/Neos.ContentRepository.Core/Tests/Unit/Feature/SubtreeTagging/Dto/SubtreeTagTest.php b/Neos.ContentRepository.Core/Tests/Unit/Feature/SubtreeTagging/Dto/SubtreeTagTest.php new file mode 100644 index 00000000000..df3d9c11288 --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/Feature/SubtreeTagging/Dto/SubtreeTagTest.php @@ -0,0 +1,70 @@ +value); + } + + /** + * @test + */ + public function fromStringFailsIfStringContainsColon(): void + { + $this->expectException(\InvalidArgumentException::class); + SubtreeTag::fromString('invalid:tag'); + } + + /** + * @test + */ + public function fromStringFailsIfStringContainsUpperCaseCharacters(): void + { + $this->expectException(\InvalidArgumentException::class); + SubtreeTag::fromString('invalidTag'); + } + + /** + * @test + */ + public function fromStringFailsIfStringContainsSpecialCharacters(): void + { + $this->expectException(\InvalidArgumentException::class); + SubtreeTag::fromString('invälid'); + } + + /** + * @test + */ + public function equalsReturnsTrueIfTagValuesMatch(): void + { + self::assertTrue(SubtreeTag::fromString('some-tag')->equals(SubtreeTag::fromString('some-tag'))); + } + + /** + * @test + */ + public function equalsReturnsFalseIfTagValuesDontMatch(): void + { + self::assertFalse(SubtreeTag::fromString('some-tag')->equals(SubtreeTag::fromString('some_tag'))); + } + + /** + * @test + */ + public function canBeSerialized(): void + { + self::assertSame('"some-tag"', json_encode(SubtreeTag::fromString('some-tag'))); + } + +} diff --git a/Neos.ContentRepository.Core/Tests/Unit/Feature/SubtreeTagging/Dto/SubtreeTagsTest.php b/Neos.ContentRepository.Core/Tests/Unit/Feature/SubtreeTagging/Dto/SubtreeTagsTest.php new file mode 100644 index 00000000000..356e0fcd762 --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/Feature/SubtreeTagging/Dto/SubtreeTagsTest.php @@ -0,0 +1,186 @@ +toStringArray()); + } + + /** + * @test + */ + public function withoutReturnsSameInstanceIfSpecifiedTagIsNotContained(): void + { + $tags = SubtreeTags::fromStrings('foo', 'bar'); + self::assertSame($tags, $tags->without(SubtreeTag::fromString('baz'))); + } + + /** + * @test + */ + public function withoutReturnsInstanceWithoutSpecifiedTag(): void + { + $tags = SubtreeTags::fromStrings('foo', 'bar') + ->without(SubtreeTag::fromString('foo')); + self::assertSame(['bar'], $tags->toStringArray()); + } + + /** + * @test + */ + public function fromArrayFailsIfArrayContainsString(): void + { + $this->expectException(\TypeError::class); + SubtreeTags::fromArray([SubtreeTag::fromString('foo'), 'bar']); + } + + /** + * @test + */ + public function fromArrayReturnsInstance(): void + { + self::assertSame(['foo', 'bar'], SubtreeTags::fromArray([SubtreeTag::fromString('foo'), SubtreeTag::fromString('bar'), SubtreeTag::fromString('foo')])->toStringArray()); + } + + public static function isEmptyDataProvider(): iterable + { + yield 'empty' => ['tags' => [], 'expectedResult' => true]; + yield 'single tag' => ['tags' => ['foo'], 'expectedResult' => false]; + yield 'four tags with one duplicate' => ['tags' => ['foo', 'bar', 'baz', 'foo'], 'expectedResult' => false]; + } + + + /** + * @test + * @dataProvider isEmptyDataProvider + */ + public function isEmptyTests(array $tags, bool $expectedResult): void + { + self::assertSame($expectedResult, SubtreeTags::fromStrings(...$tags)->isEmpty()); + } + + public static function countDataProvider(): iterable + { + yield 'empty' => ['tags' => [], 'expectedResult' => 0]; + yield 'single tag' => ['tags' => ['foo'], 'expectedResult' => 1]; + yield 'four tags with one duplicate' => ['tags' => ['foo', 'bar', 'baz', 'foo'], 'expectedResult' => 3]; + } + + + /** + * @test + * @dataProvider countDataProvider + */ + public function countTests(array $tags, int $expectedResult): void + { + self::assertSame($expectedResult, SubtreeTags::fromStrings(...$tags)->count()); + } + + public static function containDataProvider(): iterable + { + yield 'empty' => ['tags' => [], 'tag' => 'foo', 'expectedResult' => false]; + yield 'not contained' => ['tags' => ['foo', 'bar'], 'tag' => 'baz', 'expectedResult' => false]; + yield 'is contained' => ['tags' => ['foo', 'bar'], 'tag' => 'bar', 'expectedResult' => true]; + } + + + /** + * @test + * @dataProvider containDataProvider + */ + public function containTests(array $tags, string $tag, bool $expectedResult): void + { + self::assertSame($expectedResult, SubtreeTags::fromStrings(...$tags)->contain(SubtreeTag::fromString($tag))); + } + + + public static function intersectionDataProvider(): iterable + { + yield 'empty' => ['tags1' => [], 'tags2' => [], 'expectedResult' => []]; + yield 'one empty' => ['tags1' => [], 'tags2' => ['foo'], 'expectedResult' => []]; + yield 'two empty' => ['tags1' => ['foo'], 'tags2' => [], 'expectedResult' => []]; + yield 'no intersection' => ['tags1' => ['foo', 'bar'], 'tags2' => ['baz', 'foos'], 'expectedResult' => []]; + yield 'with intersection' => ['tags1' => ['foo', 'bar', 'baz'], 'tags2' => ['baz', 'bars', 'foo'], 'expectedResult' => ['foo', 'baz']]; + yield 'with intersection reversed' => ['tags1' => ['baz', 'bars', 'foo'], 'tags2' => ['foo', 'bar', 'baz'], 'expectedResult' => ['baz', 'foo']]; + } + + + /** + * @test + * @dataProvider intersectionDataProvider + */ + public function intersectionTests(array $tags1, array $tags2, array $expectedResult): void + { + self::assertSame($expectedResult, SubtreeTags::fromStrings(...$tags1)->intersection(SubtreeTags::fromStrings(...$tags2))->toStringArray()); + } + + public static function mergeDataProvider(): iterable + { + yield 'empty' => ['tags1' => [], 'tags2' => [], 'expectedResult' => []]; + yield 'one empty' => ['tags1' => [], 'tags2' => ['foo'], 'expectedResult' => ['foo']]; + yield 'two empty' => ['tags1' => ['foo'], 'tags2' => [], 'expectedResult' => ['foo']]; + yield 'no intersection' => ['tags1' => ['foo', 'bar'], 'tags2' => ['baz', 'foos'], 'expectedResult' => ['foo', 'bar', 'baz', 'foos']]; + yield 'with intersection' => ['tags1' => ['foo', 'bar', 'baz'], 'tags2' => ['baz', 'bars', 'foo'], 'expectedResult' => ['foo', 'bar', 'baz', 'bars']]; + } + + + /** + * @test + * @dataProvider mergeDataProvider + */ + public function mergeTests(array $tags1, array $tags2, array $expectedResult): void + { + self::assertSame($expectedResult, SubtreeTags::fromStrings(...$tags1)->merge(SubtreeTags::fromStrings(...$tags2))->toStringArray()); + } + + /** + * @test + */ + public function mapAppliesCallback(): void + { + $result = SubtreeTags::fromStrings('foo', 'bar', 'baz')->map(static fn (SubtreeTag $tag) => strtoupper($tag->value)); + self::assertSame(['FOO', 'BAR', 'BAZ'], $result); + } + + /** + * @test + */ + public function toStringArrayReturnsEmptyArrayForEmptySet(): void + { + self::assertSame([], SubtreeTags::createEmpty()->toStringArray()); + } + + /** + * @test + */ + public function toStringArrayReturnsTagsAsStrings(): void + { + self::assertSame(['foo', 'bar'], SubtreeTags::fromStrings('foo', 'bar')->toStringArray()); + } + + /** + * @test + */ + public function canBeSerialized(): void + { + self::assertSame('["foo","bar"]', json_encode(SubtreeTags::fromStrings('foo', 'bar', 'foo'))); + } +} diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/DimensionSpacePointsBySubtreeTagsTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/DimensionSpacePointsBySubtreeTagsTest.php new file mode 100644 index 00000000000..befb615740f --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/DimensionSpacePointsBySubtreeTagsTest.php @@ -0,0 +1,95 @@ + 'value1.1', 'dimensionB' => 'value1']); + $tag = SubtreeTag::fromString('tag1'); + $dimensionSpacePointsBySubtreeTags = DimensionSpacePointsBySubtreeTags::create() + ->withSubtreeTagAndDimensionSpacePoint($tag, $dsp); + self::assertSame($dimensionSpacePointsBySubtreeTags, $dimensionSpacePointsBySubtreeTags->withSubtreeTagAndDimensionSpacePoint($tag, $dsp)); + } + + /** + * @test + */ + public function withSubtreeTagAndDimensionSpacePointAddsTagWithDSP(): void + { + $dsp1 = DimensionSpacePoint::fromArray(['dimensionA' => 'value1.1', 'dimensionB' => 'value1']); + $dimensionSpacePointsBySubtreeTags = DimensionSpacePointsBySubtreeTags::create() + ->withSubtreeTagAndDimensionSpacePoint(SubtreeTag::fromString('tag1'), $dsp1); + + $expectedJson = '{"tag1":[{"dimensionA":"value1.1","dimensionB":"value1"}]}'; + self::assertJsonStringEqualsJsonString($expectedJson, json_encode($dimensionSpacePointsBySubtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT)); + } + + /** + * @test + */ + public function withSubtreeTagAndDimensionSpacePointMergesTagWithDSPs(): void + { + $dsp1 = DimensionSpacePoint::fromArray(['dimensionA' => 'value1.1', 'dimensionB' => 'value1']); + $dsp2 = DimensionSpacePoint::fromArray(['dimensionA' => 'value1.2', 'dimensionB' => 'value2']); + $dimensionSpacePointsBySubtreeTags = DimensionSpacePointsBySubtreeTags::create() + ->withSubtreeTagAndDimensionSpacePoint(SubtreeTag::fromString('tag1'), $dsp1) + ->withSubtreeTagAndDimensionSpacePoint(SubtreeTag::fromString('tag2'), $dsp2) + ->withSubtreeTagAndDimensionSpacePoint(SubtreeTag::fromString('tag2'), $dsp1); + + $expectedJson = '{"tag1":[{"dimensionA":"value1.1","dimensionB":"value1"}],"tag2":[{"dimensionA":"value1.2","dimensionB":"value2"},{"dimensionA":"value1.1","dimensionB":"value1"}]}'; + self::assertJsonStringEqualsJsonString($expectedJson, json_encode($dimensionSpacePointsBySubtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT)); + } + + /** + * @test + */ + public function forSubtreeTagReturnsEmptyDSPIfSubtreeTagIsNotContained(): void + { + $dimensionSpacePointsBySubtreeTags = DimensionSpacePointsBySubtreeTags::create(); + self::assertTrue($dimensionSpacePointsBySubtreeTags->forSubtreeTag(SubtreeTag::fromString('some-tag'))->equals(DimensionSpacePointSet::fromArray([]))); + } + + /** + * @test + */ + public function forSubtreeTagReturnsMatchingDSPs(): void + { + $dsp1 = DimensionSpacePoint::fromArray(['dimensionA' => 'value1.1', 'dimensionB' => 'value1']); + $dsp2 = DimensionSpacePoint::fromArray(['dimensionA' => 'value1.2', 'dimensionB' => 'value2']); + $dimensionSpacePointsBySubtreeTags = DimensionSpacePointsBySubtreeTags::create() + ->withSubtreeTagAndDimensionSpacePoint(SubtreeTag::fromString('tag1'), $dsp1) + ->withSubtreeTagAndDimensionSpacePoint(SubtreeTag::fromString('tag2'), $dsp2) + ->withSubtreeTagAndDimensionSpacePoint(SubtreeTag::fromString('tag2'), $dsp1); + + self::assertTrue($dimensionSpacePointsBySubtreeTags->forSubtreeTag(SubtreeTag::fromString('tag2'))->equals(DimensionSpacePointSet::fromArray([$dsp1, $dsp2]))); + } +} diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodeTagsTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodeTagsTest.php new file mode 100644 index 00000000000..47d96ce33fe --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/NodeTagsTest.php @@ -0,0 +1,212 @@ +expectException(\InvalidArgumentException::class); + NodeTags::create(SubtreeTags::fromStrings('foo', 'bar'), SubtreeTags::fromStrings('foos', 'bar')); + } + + /** + * @test + */ + public function withoutReturnsSameInstanceIfSpecifiedTagIsNotContained(): void + { + $tags = NodeTags::create(SubtreeTags::fromStrings('foo', 'bar'), SubtreeTags::fromStrings('baz')); + self::assertSame($tags, $tags->without(SubtreeTag::fromString('foos'))); + } + + /** + * @test + */ + public function withoutReturnsInstanceWithoutSpecifiedTag(): void + { + $tags = NodeTags::create(SubtreeTags::fromStrings('foo', 'bar'), SubtreeTags::fromStrings('baz')) + ->without(SubtreeTag::fromString('bar')) + ->without(SubtreeTag::fromString('baz')); + self::assertSame(['foo'], $tags->toStringArray()); + } + + public static function withoutInheritedDataProvider(): iterable + { + yield 'both empty' => ['tags' => [], 'inheritedTags' => [], 'expectedResult' => []]; + yield 'no explicit' => ['tags' => [], 'inheritedTags' => ['foos'], 'expectedResult' => []]; + yield 'no inherited' => ['tags' => ['foo', 'bar'], 'inheritedTags' => [], 'expectedResult' => ['foo', 'bar']]; + yield 'both' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos', 'bars'], 'expectedResult' => ['foo', 'bar']]; + } + + + /** + * @test + * @dataProvider withoutInheritedDataProvider + */ + public function withoutInheritedTests(array $tags, array $inheritedTags, array $expectedResult): void + { + self::assertSame($expectedResult, NodeTags::create(SubtreeTags::fromStrings(...$tags), SubtreeTags::fromStrings(...$inheritedTags))->withoutInherited()->toStringArray()); + } + + public static function onlyInheritedDataProvider(): iterable + { + yield 'both empty' => ['tags' => [], 'inheritedTags' => [], 'expectedResult' => []]; + yield 'no explicit' => ['tags' => [], 'inheritedTags' => ['foos'], 'expectedResult' => ['foos']]; + yield 'no inherited' => ['tags' => ['foo', 'bar'], 'inheritedTags' => [], 'expectedResult' => []]; + yield 'both' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos', 'bars'], 'expectedResult' => ['foos', 'bars']]; + } + + + /** + * @test + * @dataProvider onlyInheritedDataProvider + */ + public function onlyInheritedTests(array $tags, array $inheritedTags, array $expectedResult): void + { + self::assertSame($expectedResult, NodeTags::create(SubtreeTags::fromStrings(...$tags), SubtreeTags::fromStrings(...$inheritedTags))->onlyInherited()->toStringArray()); + } + + public static function isEmptyDataProvider(): iterable + { + yield 'both empty' => ['tags' => [], 'inheritedTags' => [], 'expectedResult' => true]; + yield 'no explicit' => ['tags' => [], 'inheritedTags' => ['foos'], 'expectedResult' => false]; + yield 'no inherited' => ['tags' => ['foo', 'bar'], 'inheritedTags' => [], 'expectedResult' => false]; + yield 'both' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos'], 'expectedResult' => false]; + } + + + /** + * @test + * @dataProvider isEmptyDataProvider + */ + public function isEmptyTests(array $tags, array $inheritedTags, bool $expectedResult): void + { + self::assertSame($expectedResult, NodeTags::create(SubtreeTags::fromStrings(...$tags), SubtreeTags::fromStrings(...$inheritedTags))->isEmpty()); + } + + public static function countDataProvider(): iterable + { + yield 'both empty' => ['tags' => [], 'inheritedTags' => [], 'expectedResult' => 0]; + yield 'no explicit' => ['tags' => [], 'inheritedTags' => ['foos'], 'expectedResult' => 1]; + yield 'no inherited' => ['tags' => ['foo', 'bar'], 'inheritedTags' => [], 'expectedResult' => 2]; + yield 'both' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos'], 'expectedResult' => 3]; + } + + + /** + * @test + * @dataProvider countDataProvider + */ + public function countTests(array $tags, array $inheritedTags, int $expectedResult): void + { + self::assertSame($expectedResult, NodeTags::create(SubtreeTags::fromStrings(...$tags), SubtreeTags::fromStrings(...$inheritedTags))->count()); + } + + public static function containDataProvider(): iterable + { + yield 'both empty' => ['tags' => [], 'inheritedTags' => [], 'tag' => 'foo', 'expectedResult' => false]; + yield 'not contained' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos'], 'tag' => 'baz', 'expectedResult' => false]; + yield 'is contained in explicit' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos'], 'tag' => 'bar', 'expectedResult' => true]; + yield 'is contained in inherited' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos'], 'tag' => 'foos', 'expectedResult' => true]; + } + + + /** + * @test + * @dataProvider containDataProvider + */ + public function containTests(array $tags, array $inheritedTags, string $tag, bool $expectedResult): void + { + self::assertSame($expectedResult, NodeTags::create(SubtreeTags::fromStrings(...$tags), SubtreeTags::fromStrings(...$inheritedTags))->contain(SubtreeTag::fromString($tag))); + } + + public static function allDataProvider(): iterable + { + yield 'both empty' => ['tags' => [], 'inheritedTags' => [], 'expectedResult' => []]; + yield 'no explicit' => ['tags' => [], 'inheritedTags' => ['foos'], 'expectedResult' => ['foos']]; + yield 'no inherited' => ['tags' => ['foo', 'bar'], 'inheritedTags' => [], 'expectedResult' => ['foo', 'bar']]; + yield 'both' => ['tags' => ['foo', 'bar'], 'inheritedTags' => ['foos'], 'expectedResult' => ['foo', 'bar', 'foos']]; + } + + + /** + * @test + * @dataProvider allDataProvider + */ + public function allTests(array $tags, array $inheritedTags, array $expectedResult): void + { + self::assertSame($expectedResult, NodeTags::create(SubtreeTags::fromStrings(...$tags), SubtreeTags::fromStrings(...$inheritedTags))->all()->toStringArray()); + } + + /** + * @test + */ + public function mapAppliesCallback(): void + { + $result = NodeTags::create(SubtreeTags::fromStrings('foo', 'bar'), SubtreeTags::fromStrings('baz'))->map(static fn (SubtreeTag $tag, bool $inherited) => strtoupper($tag->value) . ($inherited ? 'i' : 'e')); + self::assertSame(['FOOe', 'BARe', 'BAZi'], $result); + } + + /** + * @test + */ + public function toStringArrayReturnsEmptyArrayForEmptySet(): void + { + self::assertSame([], NodeTags::createEmpty()->toStringArray()); + } + + /** + * @test + */ + public function toStringArrayReturnsTagsAsStrings(): void + { + self::assertSame(['foo', 'bar', 'baz'], NodeTags::create(SubtreeTags::fromStrings('foo', 'bar'), SubtreeTags::fromStrings('baz'))->toStringArray()); + } + + /** + * @test + */ + public function canBeIterated(): void + { + $result = []; + foreach (NodeTags::create(SubtreeTags::fromStrings('foo', 'bar'), SubtreeTags::fromStrings('baz')) as $tag) { + $result[] = $tag->value; + } + self::assertSame(['foo', 'bar', 'baz'], $result); + } + + /** + * @test + */ + public function canBeSerialized(): void + { + self::assertSame('{"foo":true,"bar":true,"baz":null}', json_encode(NodeTags::create(SubtreeTags::fromStrings('foo', 'bar'), SubtreeTags::fromStrings('baz')))); + } + +} diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index e55a373025d..a976b696257 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php @@ -10,47 +10,48 @@ use League\Flysystem\FilesystemException; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\VariantType; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\EventStore\EventNormalizer; -use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; -use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMappings; -use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; -use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; -use Neos\ContentRepository\Export\ProcessorInterface; -use Neos\ContentRepository\Export\ProcessorResult; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; -use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; -use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; +use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; +use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMappings; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMapping; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\OriginNodeMoveMappings; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\SucceedingSiblingNodeMoveDestination; +use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; +use Neos\ContentRepository\Core\Feature\NodeReferencing\Dto\SerializedNodeReferences; use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; -use Neos\ContentRepository\Export\Severity; -use Neos\ContentRepository\LegacyNodeMigration\Exception\MigrationException; -use Neos\ContentRepository\LegacyNodeMigration\Helpers\SerializedPropertyValuesAndReferences; -use Neos\ContentRepository\LegacyNodeMigration\Helpers\VisitedNodeAggregate; -use Neos\ContentRepository\LegacyNodeMigration\Helpers\VisitedNodeAggregates; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; -use Neos\ContentRepository\Core\NodeType\NodeType; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\SharedModel\Node\PropertyNames; +use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvent; +use Neos\ContentRepository\Export\ProcessorInterface; +use Neos\ContentRepository\Export\ProcessorResult; +use Neos\ContentRepository\Export\Severity; +use Neos\ContentRepository\LegacyNodeMigration\Exception\MigrationException; +use Neos\ContentRepository\LegacyNodeMigration\Helpers\SerializedPropertyValuesAndReferences; +use Neos\ContentRepository\LegacyNodeMigration\Helpers\VisitedNodeAggregate; +use Neos\ContentRepository\LegacyNodeMigration\Helpers\VisitedNodeAggregates; use Neos\Flow\Persistence\Doctrine\DataTypes\JsonArrayType; use Neos\Flow\Property\PropertyMapper; use Neos\Neos\Domain\Service\NodeTypeNameFactory; @@ -287,9 +288,9 @@ public function processNodeDataWithoutFallbackToEmptyDimension(NodeAggregateId $ // create node aggregate $this->exportEvent(new NodeAggregateWithNodeWasCreated($this->contentStreamId, $nodeAggregateId, $nodeTypeName, $originDimensionSpacePoint, $this->interDimensionalVariationGraph->getSpecializationSet($originDimensionSpacePoint->toDimensionSpacePoint()), $parentNodeAggregate->nodeAggregateId, $nodeName, $serializedPropertyValuesAndReferences->serializedPropertyValues, NodeAggregateClassification::CLASSIFICATION_REGULAR, null)); } - // nodes are hidden via NodeAggregateWasDisabled event + // nodes are hidden via SubtreeWasTagged event if ($this->isNodeHidden($nodeDataRow)) { - $this->exportEvent(new NodeAggregateWasDisabled($this->contentStreamId, $nodeAggregateId, $this->interDimensionalVariationGraph->getSpecializationSet($originDimensionSpacePoint->toDimensionSpacePoint(), true, $this->visitedNodes->alreadyVisitedOriginDimensionSpacePoints($nodeAggregateId)->toDimensionSpacePointSet()))); + $this->exportEvent(new SubtreeWasTagged($this->contentStreamId, $nodeAggregateId, $this->interDimensionalVariationGraph->getSpecializationSet($originDimensionSpacePoint->toDimensionSpacePoint(), true, $this->visitedNodes->alreadyVisitedOriginDimensionSpacePoints($nodeAggregateId)->toDimensionSpacePointSet()), SubtreeTag::disabled())); } foreach ($serializedPropertyValuesAndReferences->references as $referencePropertyName => $destinationNodeAggregateIds) { $this->nodeReferencesWereSetEvents[] = new NodeReferencesWereSet($this->contentStreamId, $nodeAggregateId, new OriginDimensionSpacePointSet([$originDimensionSpacePoint]), ReferenceName::fromString($referencePropertyName), SerializedNodeReferences::fromNodeAggregateIds($destinationNodeAggregateIds)); diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature index ef6572be925..d6204a60d80 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Hidden.feature @@ -35,7 +35,7 @@ Feature: Simple migrations without content dimensions for hidden state migration | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden" property false must not get disabled When I have the following node data rows: @@ -58,7 +58,7 @@ Feature: Simple migrations without content dimensions for hidden state migration | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "1989-01-01 10:10:10"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with active "hidden before" property, after a "hidden after" property must not get disabled When I have the following node data rows: @@ -93,7 +93,7 @@ Feature: Simple migrations without content dimensions for hidden state migration | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden after" property in future and a "hidden before" property later in future must not get disabled When I have the following node data rows: @@ -116,7 +116,7 @@ Feature: Simple migrations without content dimensions for hidden state migration | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2098-01-01 10:10:10"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a active "hidden before" property must not get disabled When I have the following node data rows: @@ -139,7 +139,7 @@ Feature: Simple migrations without content dimensions for hidden state migration | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "disableAfterDateTime": {"type": "DateTimeImmutable", "value": "1990-01-01 10:10:10"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden after" property in future must not get disabled When I have the following node data rows: @@ -162,5 +162,5 @@ Feature: Simple migrations without content dimensions for hidden state migration | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}, "enableAfterDateTime": {"type": "DateTimeImmutable", "value": "2099-01-01 10:10:10"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature index a619755f726..1a48d0d583d 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/HiddenWithoutTimeableNodeVisibility.feature @@ -28,7 +28,7 @@ Feature: Simple migrations without content dimensions for hidden state migration | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | Scenario: A node with a "hidden" property false must not get disabled When I have the following node data rows: @@ -48,10 +48,10 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | 1989-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -62,8 +62,8 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1989-01-01 10:10:10 | 1990-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -75,8 +75,8 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | 1990-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -88,10 +88,10 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | 2099-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -102,8 +102,8 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2098-01-01 10:10:10 | 2099-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -115,10 +115,10 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | 2098-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -129,8 +129,8 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | 1990-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -142,10 +142,10 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 1990-01-01 10:10:10 | | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -156,8 +156,8 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | 2099-01-01 10:10:10 | | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | @@ -169,9 +169,9 @@ Feature: Simple migrations without content dimensions for hidden state migration | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | 0 | | 2099-01-01 10:10:10 | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported - | Type | Payload | - | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | + | Type | Payload | + | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | - | NodeAggregateWasDisabled | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id"} | + | SubtreeWasTagged | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "tag": "disabled"} | And I expect the following warnings to be logged | Skipped the migration of your "hiddenBeforeDateTime" and "hiddenAfterDateTime" properties as your target NodeTypes do not inherit "Neos.TimeableNodeVisibility:Timeable". Please install neos/timeable-node-visibility, if you want to migrate them. | diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 453805a1774..4ef1379ae98 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -38,6 +38,7 @@ use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeCopying; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeCreation; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeDisabling; +use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\SubtreeTagging; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeModification; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeMove; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeReferencing; @@ -71,6 +72,7 @@ trait CRTestSuiteTrait use NodeCreation; use NodeCopying; use NodeDisabling; + use SubtreeTagging; use NodeModification; use NodeMove; use NodeReferencing; diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php index 144c8fc27bc..eefe8c9de6b 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/NodeDisabling.php @@ -18,7 +18,6 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; @@ -75,42 +74,6 @@ public function theCommandDisableNodeAggregateIsExecutedWithPayloadAndExceptions } } - /** - * @Given /^the event NodeAggregateWasDisabled was published with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theEventNodeAggregateWasDisabledWasPublishedWithPayload(TableNode $payloadTable) - { - $eventPayload = $this->readPayloadTable($payloadTable); - $streamName = ContentStreamEventStreamName::fromContentStreamId( - array_key_exists('contentStreamId', $eventPayload) - ? ContentStreamId::fromString($eventPayload['contentStreamId']) - : $this->currentContentStreamId - ); - - $this->publishEvent('NodeAggregateWasDisabled', $streamName->getEventStreamName(), $eventPayload); - } - - - /** - * @Given /^the event NodeAggregateWasEnabled was published with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theEventNodeAggregateWasEnabledWasPublishedWithPayload(TableNode $payloadTable) - { - $eventPayload = $this->readPayloadTable($payloadTable); - $streamName = ContentStreamEventStreamName::fromContentStreamId( - array_key_exists('contentStreamId', $eventPayload) - ? ContentStreamId::fromString($eventPayload['contentStreamId']) - : $this->currentContentStreamId - ); - - $this->publishEvent('NodeAggregateWasEnabled', $streamName->getEventStreamName(), $eventPayload); - } - - /** * @Given /^the command EnableNodeAggregate is executed with payload:$/ * @param TableNode $payloadTable diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php new file mode 100644 index 00000000000..79a5a080566 --- /dev/null +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/SubtreeTagging.php @@ -0,0 +1,154 @@ +readPayloadTable($payloadTable); + $workspaceName = isset($commandArguments['workspaceName']) + ? WorkspaceName::fromString($commandArguments['workspaceName']) + : $this->currentWorkspaceName; + $coveredDimensionSpacePoint = isset($commandArguments['coveredDimensionSpacePoint']) + ? DimensionSpacePoint::fromArray($commandArguments['coveredDimensionSpacePoint']) + : $this->currentDimensionSpacePoint; + + $command = TagSubtree::create( + $workspaceName, + NodeAggregateId::fromString($commandArguments['nodeAggregateId']), + $coveredDimensionSpacePoint, + NodeVariantSelectionStrategy::from($commandArguments['nodeVariantSelectionStrategy']), + SubtreeTag::fromString($commandArguments['tag']), + ); + + $this->lastCommandOrEventResult = $this->currentContentRepository->handle($command); + } + + /** + * @Given /^the command TagSubtree is executed with payload and exceptions are caught:$/ + * @param TableNode $payloadTable + */ + public function theCommandTagSubtreeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void + { + try { + $this->theCommandTagSubtreeIsExecutedWithPayload($payloadTable); + } catch (\Exception $exception) { + $this->lastCommandException = $exception; + } + } + + /** + * @Given /^the event SubtreeWasTagged was published with payload:$/ + * @param TableNode $payloadTable + * @throws \Exception + */ + public function theEventSubtreeWasTaggedWasPublishedWithPayload(TableNode $payloadTable) + { + $eventPayload = $this->readPayloadTable($payloadTable); + $streamName = ContentStreamEventStreamName::fromContentStreamId( + array_key_exists('contentStreamId', $eventPayload) + ? ContentStreamId::fromString($eventPayload['contentStreamId']) + : $this->currentContentStreamId + ); + + $this->publishEvent('SubtreeWasTagged', $streamName->getEventStreamName(), $eventPayload); + } + + + /** + * @Given /^the event SubtreeWasUntagged was published with payload:$/ + * @param TableNode $payloadTable + * @throws \Exception + */ + public function theEventSubtreeWasUntaggedWasPublishedWithPayload(TableNode $payloadTable) + { + $eventPayload = $this->readPayloadTable($payloadTable); + $streamName = ContentStreamEventStreamName::fromContentStreamId( + array_key_exists('contentStreamId', $eventPayload) + ? ContentStreamId::fromString($eventPayload['contentStreamId']) + : $this->currentContentStreamId + ); + + $this->publishEvent('SubtreeWasUntagged', $streamName->getEventStreamName(), $eventPayload); + } + + + /** + * @Given /^the command UntagSubtree is executed with payload:$/ + * @param TableNode $payloadTable + * @throws \Exception + */ + public function theCommandUntagSubtreeIsExecutedWithPayload(TableNode $payloadTable): void + { + $commandArguments = $this->readPayloadTable($payloadTable); + $workspaceName = isset($commandArguments['workspaceName']) + ? WorkspaceName::fromString($commandArguments['workspaceName']) + : $this->currentWorkspaceName; + $coveredDimensionSpacePoint = isset($commandArguments['coveredDimensionSpacePoint']) + ? DimensionSpacePoint::fromArray($commandArguments['coveredDimensionSpacePoint']) + : $this->currentDimensionSpacePoint; + + $command = UntagSubtree::create( + $workspaceName, + NodeAggregateId::fromString($commandArguments['nodeAggregateId']), + $coveredDimensionSpacePoint, + NodeVariantSelectionStrategy::from($commandArguments['nodeVariantSelectionStrategy']), + SubtreeTag::fromString($commandArguments['tag']), + ); + + $this->lastCommandOrEventResult = $this->currentContentRepository->handle($command); + } + + /** + * @Given /^the command UntagSubtree is executed with payload and exceptions are caught:$/ + * @param TableNode $payloadTable + */ + public function theCommandUntagSubtreeIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void + { + try { + $this->theCommandUntagSubtreeIsExecutedWithPayload($payloadTable); + } catch (\Exception $exception) { + $this->lastCommandException = $exception; + } + } +} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php index d0ce2e32de6..a00c286d08b 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/GenericCommandExecutionAndEventPublication.php @@ -19,15 +19,17 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; +use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeMove\Command\MoveNodeAggregate; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNodeAndSerializedProperties; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\DisableNodeAggregate; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Command\EnableNodeAggregate; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Feature\NodeReferencing\Command\SetNodeReferences; use Neos\ContentRepository\Core\Feature\NodeModification\Command\SetSerializedNodeProperties; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\TagSubtree; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Command\UntagSubtree; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace; @@ -108,6 +110,8 @@ protected static function resolveShortCommandName(string $shortCommandName): str 'SetSerializedNodeProperties' => SetSerializedNodeProperties::class, 'DisableNodeAggregate' => DisableNodeAggregate::class, 'EnableNodeAggregate' => EnableNodeAggregate::class, + 'TagSubtree' => TagSubtree::class, + 'UntagSubtree' => UntagSubtree::class, 'MoveNodeAggregate' => MoveNodeAggregate::class, 'SetNodeReferences' => SetNodeReferences::class, default => throw new \Exception( @@ -214,8 +218,7 @@ public function eventNumberIs(int $eventNumber, string $eventType, TableNode $pa $key = $assertionTableRow['Key']; $actualValue = Arrays::getValueByPath($actualEventPayload, $key); - // Note: For dimension space points we switch to an array comparison because the order is not deterministic (@see https://github.com/neos/neos-development-collection/issues/4769) - if ($key === 'affectedDimensionSpacePoints' || $key === 'affectedOccupiedDimensionSpacePoints') { + if ($key === 'affectedDimensionSpacePoints') { $expected = DimensionSpacePointSet::fromJsonString($assertionTableRow['Expected']); $actual = DimensionSpacePointSet::fromArray($actualValue); Assert::assertTrue($expected->equals($actual), 'Actual Dimension Space Point set "' . json_encode($actualValue) . '" does not match expected Dimension Space Point set "' . $assertionTableRow['Expected'] . '"'); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php index 168a6f7a055..119ed394de0 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/NodeTraversalTrait.php @@ -225,12 +225,13 @@ public function iExecuteTheRetrieveNodePathQueryIExpectTheFollowingNodes(string } /** - * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdsSerialized I expect the following tree: - * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdsSerialized I expect no results - * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdsSerialized and filter :filterSerialized I expect the following tree: - * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdsSerialized and filter :filterSerialized I expect no results + * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdSerialized I expect the following tree: + * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdSerialized I expect no results + * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdSerialized and filter :filterSerialized I expect the following tree: + * @When I execute the findSubtree query for entry node aggregate id :entryNodeIdSerialized and filter :filterSerialized I expect no results + * @When /^I execute the findSubtree query for entry node aggregate id "(?[^"]*)" I expect the following tree (?with tags):$/ */ - public function iExecuteTheFindSubtreeQueryIExpectTheFollowingTrees(string $entryNodeIdSerialized, string $filterSerialized = null, PyStringNode $expectedTree = null): void + public function iExecuteTheFindSubtreeQueryIExpectTheFollowingTrees(string $entryNodeIdSerialized, string $filterSerialized = null, PyStringNode $expectedTree = null, string $withTags = null): void { $entryNodeAggregateId = NodeAggregateId::fromString($entryNodeIdSerialized); $filterValues = !empty($filterSerialized) ? json_decode($filterSerialized, true, 512, JSON_THROW_ON_ERROR) : []; @@ -245,7 +246,11 @@ public function iExecuteTheFindSubtreeQueryIExpectTheFollowingTrees(string $entr while ($subtreeStack !== []) { /** @var Subtree $subtree */ $subtree = array_shift($subtreeStack); - $result[] = str_repeat(' ', $subtree->level) . $subtree->node->nodeAggregateId->value; + $tags = []; + if ($withTags !== null) { + $tags = [...array_map(static fn(string $tag) => $tag . '*', $subtree->node->tags->withoutInherited()->toStringArray()), ...$subtree->node->tags->onlyInherited()->toStringArray()]; + } + $result[] = str_repeat(' ', $subtree->level) . $subtree->node->nodeAggregateId->value . ($tags !== [] ? ' (' . implode(',', $tags) . ')' : ''); $subtreeStack = [...$subtree->children, ...$subtreeStack]; } Assert::assertSame($expectedTree?->getRaw() ?? '', implode(chr(10), $result)); @@ -263,8 +268,7 @@ public function iExecuteTheFindDescendantNodesQueryIExpectTheFollowingNodes(stri $subgraph = $this->getCurrentSubgraph(); $actualNodeIds = array_map(static fn(Node $node) => $node->nodeAggregateId->value, iterator_to_array($subgraph->findDescendantNodes($entryNodeAggregateId, $filter))); - // Note: In contrast to other similar checks, in this case we use assertEqualsCanonicalizing() instead of assertSame() because the order of descendant nodes is not completely deterministic (@see https://github.com/neos/neos-development-collection/issues/4769) - Assert::assertEqualsCanonicalizing($expectedNodeIds, $actualNodeIds, 'findDescendantNodes returned an unexpected result'); + Assert::assertSame($expectedNodeIds, $actualNodeIds, 'findDescendantNodes returned an unexpected result'); $actualCount = $subgraph->countDescendantNodes($entryNodeAggregateId, CountDescendantNodesFilter::fromFindDescendantNodesFilter($filter)); Assert::assertSame($expectedTotalCount ?? count($expectedNodeIds), $actualCount, 'countDescendantNodes returned an unexpected result'); } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php index ab7d13c76e7..9c1177f28c2 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeAggregateTrait.php @@ -15,6 +15,7 @@ namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -90,11 +91,12 @@ public function iExpectThisNodeAggregateToDisableDimensionSpacePoints(string $se { $expectedDisabledDimensionSpacePoints = DimensionSpacePointSet::fromJsonString($serializedExpectedDisabledDimensionSpacePoints); $this->assertOnCurrentNodeAggregate(function (NodeAggregate $nodeAggregate) use ($expectedDisabledDimensionSpacePoints) { + $actualDisabledDimensionSpacePoints = $nodeAggregate->getDimensionSpacePointsTaggedWith(SubtreeTag::disabled()); Assert::assertEquals( $expectedDisabledDimensionSpacePoints, - $nodeAggregate->disabledDimensionSpacePoints, + $actualDisabledDimensionSpacePoints, 'Expected disabled dimension space point set ' . $expectedDisabledDimensionSpacePoints->toJson() . ', got ' . - $nodeAggregate->disabledDimensionSpacePoints->toJson() + $actualDisabledDimensionSpacePoints->toJson() ); }); } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php index c438a60fa74..2294884b5a7 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/ProjectedNodeTrait.php @@ -16,6 +16,7 @@ use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Psr7\Uri; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindReferencesFilter; @@ -96,6 +97,7 @@ public function iExpectANodeIdentifiedByXToExistInTheContentGraph(string $serial }); } + /** * @Then /^I expect node aggregate identifier "([^"]*)" to lead to node (.*)$/ */ @@ -195,6 +197,51 @@ public function iExpectNodeAggregateIdAndNodePathToLeadToNoNode(string $serializ $this->iExpectPathToLeadToNoNode($serializedNodePath); } + /** + * @Then /^I expect the node with aggregate identifier "([^"]*)" to be explicitly tagged "([^"]*)"$/ + */ + public function iExpectTheNodeWithAggregateIdentifierToBeExplicitlyTagged(string $serializedNodeAggregateId, string $serializedTag): void + { + $nodeAggregateId = NodeAggregateId::fromString($serializedNodeAggregateId); + $expectedTag = SubtreeTag::fromString($serializedTag); + $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodeAggregateId, $expectedTag) { + $currentNode = $subgraph->findNodeById($nodeAggregateId); + Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); + Assert::assertTrue($currentNode->tags->withoutInherited()->contain($expectedTag)); + return $currentNode; + }); + } + + /** + * @Then /^I expect the node with aggregate identifier "([^"]*)" to inherit the tag "([^"]*)"$/ + */ + public function iExpectTheNodeWithAggregateIdentifierToInheritTheTag(string $serializedNodeAggregateId, string $serializedTag): void + { + $nodeAggregateId = NodeAggregateId::fromString($serializedNodeAggregateId); + $expectedTag = SubtreeTag::fromString($serializedTag); + $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodeAggregateId, $expectedTag) { + $currentNode = $subgraph->findNodeById($nodeAggregateId); + Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); + Assert::assertTrue($currentNode->tags->onlyInherited()->contain($expectedTag)); + return $currentNode; + }); + } + + /** + * @Then /^I expect the node with aggregate identifier "([^"]*)" to not contain the tag "([^"]*)"$/ + */ + public function iExpectTheNodeWithAggregateIdentifierToNotContainTheTag(string $serializedNodeAggregateId, string $serializedTag): void + { + $nodeAggregateId = NodeAggregateId::fromString($serializedNodeAggregateId); + $expectedTag = SubtreeTag::fromString($serializedTag); + $this->initializeCurrentNodeFromContentSubgraph(function (ContentSubgraphInterface $subgraph) use ($nodeAggregateId, $expectedTag) { + $currentNode = $subgraph->findNodeById($nodeAggregateId); + Assert::assertNotNull($currentNode, 'No node could be found by node aggregate id "' . $nodeAggregateId->value . '" in content subgraph "' . $this->currentDimensionSpacePoint->toJson() . '@' . $this->currentContentStreamId->value . '"'); + Assert::assertFalse($currentNode->tags->contain($expectedTag), sprintf('Node with id "%s" in content subgraph "%s@%s", was not expected to contain the subtree tag "%s" but it does', $nodeAggregateId->value, $this->currentDimensionSpacePoint->toJson(), $this->currentContentStreamId->value, $expectedTag->value)); + return $currentNode; + }); + } + protected function initializeCurrentNodeFromContentGraph(callable $query): void { $this->currentNode = $query($this->currentContentRepository->getContentGraph()); diff --git a/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php b/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php index 80cd87080db..5bb74d9b078 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php +++ b/Neos.ContentRepository.TestSuite/Classes/Unit/NodeSubjectProvider.php @@ -32,6 +32,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\PropertyCollection; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; @@ -104,6 +105,7 @@ public function createMinimalNodeOfType( $this->propertyConverter ), $nodeName, + NodeTags::createEmpty(), Timestamps::create( new \DateTimeImmutable(), new \DateTimeImmutable(), diff --git a/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml b/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml index 564750f56d8..89c3069794e 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml @@ -24,12 +24,3 @@ Neos\ContentRepository\Core\Projection\Workspace\WorkspaceProjectionFactory: value: Neos\ContentRepository\Core\Projection\Workspace\WorkspaceProjectionFactory 2: object: 'Neos\ContentRepository\Core\Infrastructure\DbalClientInterface' - -Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateProjectionFactory: - scope: singleton - factoryObjectName: Neos\ContentRepositoryRegistry\Infrastructure\GenericObjectFactory - arguments: - 1: - value: Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateProjectionFactory - 2: - object: 'Neos\ContentRepository\Core\Infrastructure\DbalClientInterface' diff --git a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml index 650abaaff54..37d72c41045 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Settings.yaml @@ -73,8 +73,6 @@ Neos: factoryObjectName: Neos\ContentRepository\Core\Projection\ContentStream\ContentStreamProjectionFactory 'Neos.ContentRepository:Workspace': factoryObjectName: Neos\ContentRepository\Core\Projection\Workspace\WorkspaceProjectionFactory - 'Neos.ContentRepository:NodeHiddenState': - factoryObjectName: Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateProjectionFactory # NOTE: the following name must be stable, because we use it f.e. in Neos UI to register # catchUpHooks for content cache flushing #'Neos.ContentRepository:ContentGraph': diff --git a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php b/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php index 8477bb071e7..841185221fa 100644 --- a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php +++ b/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php @@ -2,6 +2,7 @@ namespace Neos\Neos\FrontendRouting\CatchUpHook; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; use Neos\ContentRepository\Core\Projection\CatchUpHookInterface; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventInterface; @@ -13,7 +14,6 @@ use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; use Neos\Neos\FrontendRouting\Projection\DocumentNodeInfo; use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; use Neos\Flow\Mvc\Routing\RouterCachingService; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -43,7 +43,7 @@ public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $even NodeAggregateWasRemoved::class => $this->onBeforeNodeAggregateWasRemoved($eventInstance), NodePropertiesWereSet::class => $this->onBeforeNodePropertiesWereSet($eventInstance), NodeAggregateWasMoved::class => $this->onBeforeNodeAggregateWasMoved($eventInstance), - NodeAggregateWasDisabled::class => $this->onBeforeNodeAggregateWasDisabled($eventInstance), + SubtreeWasTagged::class => $this->onBeforeSubtreeWasTagged($eventInstance), default => null }; } @@ -54,7 +54,7 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event NodeAggregateWasRemoved::class => $this->flushAllCollectedTags(), NodePropertiesWereSet::class => $this->flushAllCollectedTags(), NodeAggregateWasMoved::class => $this->flushAllCollectedTags(), - NodeAggregateWasDisabled::class => $this->flushAllCollectedTags(), + SubtreeWasTagged::class => $this->flushAllCollectedTags(), default => null }; } @@ -69,7 +69,7 @@ public function onAfterCatchUp(): void // Nothing to do here } - private function onBeforeNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void + private function onBeforeSubtreeWasTagged(SubtreeWasTagged $event): void { if (!$this->getState()->isLiveContentStream($event->contentStreamId)) { return; diff --git a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php index 75b91a8e521..f56b5e5fa48 100644 --- a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php +++ b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php @@ -14,8 +14,6 @@ use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionShineThroughWasAdded; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasEnabled; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\CoverageNodeMoveMapping; use Neos\ContentRepository\Core\Feature\NodeMove\Dto\ParentNodeMoveDestination; @@ -28,6 +26,8 @@ use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateDimensionsWereUpdated; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated; use Neos\ContentRepository\Core\Infrastructure\DbalCheckpointStorage; use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; @@ -150,8 +150,8 @@ public function canHandle(EventInterface $event): bool NodePeerVariantWasCreated::class, NodeGeneralizationVariantWasCreated::class, NodeSpecializationVariantWasCreated::class, - NodeAggregateWasDisabled::class, - NodeAggregateWasEnabled::class, + SubtreeWasTagged::class, + SubtreeWasUntagged::class, NodeAggregateWasRemoved::class, NodePropertiesWereSet::class, NodeAggregateWasMoved::class, @@ -171,8 +171,8 @@ public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void NodePeerVariantWasCreated::class => $this->whenNodePeerVariantWasCreated($event), NodeGeneralizationVariantWasCreated::class => $this->whenNodeGeneralizationVariantWasCreated($event), NodeSpecializationVariantWasCreated::class => $this->whenNodeSpecializationVariantWasCreated($event), - NodeAggregateWasDisabled::class => $this->whenNodeAggregateWasDisabled($event), - NodeAggregateWasEnabled::class => $this->whenNodeAggregateWasEnabled($event), + SubtreeWasTagged::class => $this->whenSubtreeWasTagged($event), + SubtreeWasUntagged::class => $this->whenSubtreeWasUntagged($event), NodeAggregateWasRemoved::class => $this->whenNodeAggregateWasRemoved($event), NodePropertiesWereSet::class => $this->whenNodePropertiesWereSet($event, $eventEnvelope), NodeAggregateWasMoved::class => $this->whenNodeAggregateWasMoved($event), @@ -451,9 +451,9 @@ private function copyVariants( } } - private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void + private function whenSubtreeWasTagged(SubtreeWasTagged $event): void { - if (!$this->getState()->isLiveContentStream($event->contentStreamId)) { + if ($event->tag->value !== 'disabled' || !$this->getState()->isLiveContentStream($event->contentStreamId)) { return; } foreach ($event->affectedDimensionSpacePoints as $dimensionSpacePoint) { @@ -482,9 +482,9 @@ private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): } } - private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void + private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void { - if (!$this->getState()->isLiveContentStream($event->contentStreamId)) { + if ($event->tag->value !== 'disabled' || !$this->getState()->isLiveContentStream($event->contentStreamId)) { return; } foreach ($event->affectedDimensionSpacePoints as $dimensionSpacePoint) { diff --git a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php index 702e54d9116..fd67227a3de 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php +++ b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php @@ -25,8 +25,6 @@ use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\Feature\DimensionSpaceAdjustment\Event\DimensionSpacePointWasMoved; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasDisabled; -use Neos\ContentRepository\Core\Feature\NodeDisabling\Event\NodeAggregateWasEnabled; use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet; use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved; use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet; @@ -34,6 +32,8 @@ use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated; use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Event\RootWorkspaceWasCreated; use Neos\ContentRepository\Core\Infrastructure\DbalCheckpointStorage; use Neos\ContentRepository\Core\Infrastructure\DbalClientInterface; @@ -169,8 +169,8 @@ public function canHandle(EventInterface $event): bool NodePropertiesWereSet::class, NodeReferencesWereSet::class, NodeAggregateWithNodeWasCreated::class, - NodeAggregateWasDisabled::class, - NodeAggregateWasEnabled::class, + SubtreeWasTagged::class, + SubtreeWasUntagged::class, NodeAggregateWasRemoved::class, DimensionSpacePointWasMoved::class, NodeGeneralizationVariantWasCreated::class, @@ -187,8 +187,8 @@ public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void NodePropertiesWereSet::class => $this->whenNodePropertiesWereSet($event), NodeReferencesWereSet::class => $this->whenNodeReferencesWereSet($event), NodeAggregateWithNodeWasCreated::class => $this->whenNodeAggregateWithNodeWasCreated($event), - NodeAggregateWasDisabled::class => $this->whenNodeAggregateWasDisabled($event), - NodeAggregateWasEnabled::class => $this->whenNodeAggregateWasEnabled($event), + SubtreeWasTagged::class => $this->whenSubtreeWasTagged($event), + SubtreeWasUntagged::class => $this->whenSubtreeWasUntagged($event), NodeAggregateWasRemoved::class => $this->whenNodeAggregateWasRemoved($event), DimensionSpacePointWasMoved::class => $this->whenDimensionSpacePointWasMoved($event), NodeSpecializationVariantWasCreated::class => $this->whenNodeSpecializationVariantWasCreated($event), @@ -273,7 +273,7 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre ); } - private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): void + private function whenSubtreeWasTagged(SubtreeWasTagged $event): void { foreach ($event->affectedDimensionSpacePoints as $dimensionSpacePoint) { $this->markAsChanged( @@ -284,7 +284,7 @@ private function whenNodeAggregateWasDisabled(NodeAggregateWasDisabled $event): } } - private function whenNodeAggregateWasEnabled(NodeAggregateWasEnabled $event): void + private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void { foreach ($event->affectedDimensionSpacePoints as $dimensionSpacePoint) { $this->markAsChanged( diff --git a/Neos.Neos/Classes/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index d205eba8320..8da535af03b 100644 --- a/Neos.Neos/Classes/Service/LinkingService.php +++ b/Neos.Neos/Classes/Service/LinkingService.php @@ -14,9 +14,8 @@ namespace Neos\Neos\Service; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateFinder; -use Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateProjection; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\Neos\FrontendRouting\NodeAddressFactory; @@ -357,17 +356,10 @@ public function createNodeUri( $workspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId( $node->subgraphIdentity->contentStreamId ); - $nodeHiddenStateFinder = $contentRepository->projectionState(NodeHiddenStateFinder::class); - $hiddenState = $nodeHiddenStateFinder->findHiddenState( - $node->subgraphIdentity->contentStreamId, - $node->subgraphIdentity->dimensionSpacePoint, - $node->nodeAggregateId - ); - $request = $controllerContext->getRequest()->getMainRequest(); $uriBuilder = clone $controllerContext->getUriBuilder(); $uriBuilder->setRequest($request); - $action = $workspace && $workspace->isPublicWorkspace() && !$hiddenState->isHidden ? 'show' : 'preview'; + $action = $workspace && $workspace->isPublicWorkspace() && $node->tags->contain(SubtreeTag::disabled()) ? 'show' : 'preview'; return $uriBuilder ->reset() diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature index 7713ae5b2fe..786be2fb737 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/DisableNodes.feature @@ -155,11 +155,12 @@ Feature: Routing behavior of removed, disabled and re-enabled nodes | nodeAggregateId | "sir-david-nodenborough" | | coveredDimensionSpacePoint | {} | | nodeVariantSelectionStrategy | "allVariants" | - And the event NodeAggregateWasDisabled was published with payload: + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | And the graph projection is fully up to date And The documenturipath projection is up to date Then No node should match URL "/david-nodenborough" @@ -184,11 +185,12 @@ Feature: Routing behavior of removed, disabled and re-enabled nodes | nodeAggregateId | "sir-david-nodenborough" | | coveredDimensionSpacePoint | {} | | nodeVariantSelectionStrategy | "allVariants" | - And the event NodeAggregateWasDisabled was published with payload: + And the event SubtreeWasTagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "earl-o-documentbourgh" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | And the graph projection is fully up to date And The documenturipath projection is up to date Then No node should match URL "/david-nodenborough" @@ -200,11 +202,12 @@ Feature: Routing behavior of removed, disabled and re-enabled nodes | nodeAggregateId | "sir-david-nodenborough" | | coveredDimensionSpacePoint | {} | | nodeVariantSelectionStrategy | "allVariants" | - And the event NodeAggregateWasEnabled was published with payload: + And the event SubtreeWasUntagged was published with payload: | Key | Value | | contentStreamId | "cs-identifier" | | nodeAggregateId | "sir-david-nodenborough" | | affectedDimensionSpacePoints | [{}] | + | tag | "disabled" | And the graph projection is fully up to date And The documenturipath projection is up to date When I am on URL "/david-nodenborough" diff --git a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php index 203a0013ef8..9952d6a776b 100644 --- a/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php +++ b/Neos.Neos/Tests/Functional/Fusion/NodeHelperTest.php @@ -22,6 +22,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphIdentity; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\PropertyCollection; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; @@ -170,6 +171,7 @@ protected function setUp(): void $textNodeType, $textNodeProperties, null, + NodeTags::createEmpty(), Timestamps::create($now, $now, null, null) ); } diff --git a/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php b/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php index 7e3ae091573..5e9db5af825 100644 --- a/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php +++ b/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php @@ -13,6 +13,7 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Domain\Model\Workspace; use Neos\ContentRepository\Domain\Service\Context; use Neos\Flow\Tests\UnitTestCase; @@ -210,6 +211,7 @@ private function createNode(NodeAggregateId $nodeAggregateId): Node null, new PropertyCollection(SerializedPropertyValues::fromArray([]), new PropertyConverter(new Serializer([], []))), null, + NodeTags::createEmpty(), Timestamps::create($now, $now, null, null) ); } diff --git a/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php b/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php index ea41073c453..2abe83952b8 100644 --- a/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php +++ b/Neos.TimeableNodeVisibility/Classes/Service/TimeableNodeVisibilityService.php @@ -2,11 +2,11 @@ namespace Neos\TimeableNodeVisibility\Service; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\Flow\Annotations as Flow; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateFinder; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter; @@ -46,17 +46,16 @@ public function handleExceededNodeDates(ContentRepositoryId $contentRepositoryId if ($liveWorkspace === null) { throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); } - $nodeHiddenStateFinder = $contentRepository->projectionState(NodeHiddenStateFinder::class); $now = new \DateTimeImmutable(); $nodes = $this->getNodesWithExceededDates($contentRepository, $liveWorkspace, $now); $results = []; + /** @var Node $node */ foreach ($nodes as $node) { - /** @var Node $node */ - $nodeIsHidden = $this->isHidden($node, $nodeHiddenStateFinder); - if ($this->needsEnabling($node, $now) && $nodeIsHidden) { + $nodeIsDisabled = $node->tags->contain(SubtreeTag::disabled()); + if ($this->needsEnabling($node, $now) && $nodeIsDisabled) { $contentRepository->handle( EnableNodeAggregate::create( $liveWorkspace->workspaceName, @@ -70,7 +69,7 @@ public function handleExceededNodeDates(ContentRepositoryId $contentRepositoryId $this->logResult($result); } - if ($this->needsDisabling($node, $now) && !$nodeIsHidden) { + if ($this->needsDisabling($node, $now) && !$nodeIsDisabled) { $contentRepository->handle( DisableNodeAggregate::create( $liveWorkspace->workspaceName, @@ -135,15 +134,6 @@ private function getNodesWithExceededDates(ContentRepository $contentRepository, } } - private function isHidden(Node $node, NodeHiddenStateFinder $nodeHiddenStateFinder): bool - { - return $nodeHiddenStateFinder->findHiddenState( - $node->subgraphIdentity->contentStreamId, - $node->subgraphIdentity->dimensionSpacePoint, - $node->nodeAggregateId - )->isHidden; - } - private function needsEnabling(Node $node, \DateTimeImmutable $now): bool { return $node->hasProperty('enableAfterDateTime') diff --git a/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php b/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php index 64020879f1b..caf29cebe1d 100644 --- a/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php +++ b/Neos.TimeableNodeVisibility/Tests/Behavior/Bootstrap/FeatureContext.php @@ -10,11 +10,12 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteTrait; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\TimeableNodeVisibility\Service\TimeableNodeVisibilityService; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateFinder; use PHPUnit\Framework\Assert; /** @@ -54,23 +55,35 @@ public function iHandleExceededNodeDates(): void } /** - * @Then I expect this node to be :state + * @Then I expect this node to be enabled */ - public function iExpectThisNodeToBeState(string $state): void + public function iExpectThisNodeToBeEnabled(): void { - $hiddenState = $this->currentContentRepository->projectionState(NodeHiddenStateFinder::class) - ->findHiddenState( - $this->currentContentStreamId, - $this->currentDimensionSpacePoint, - $this->currentNode->nodeAggregateId - ); - if ($hiddenState->isHidden === false && $state != "enabled" - || - $hiddenState->isHidden === true && $state != "disabled" - ) { - Assert::fail('Node has not the expected state'); - } + Assert::assertNotNull($this->currentNode, 'No current node selected'); + $subgraph = $this->currentContentRepository->getContentGraph()->getSubgraph( + $this->currentContentStreamId, + $this->currentDimensionSpacePoint, + VisibilityConstraints::withoutRestrictions(), + ); + $currentNode = $subgraph->findNodeById($this->currentNode->nodeAggregateId); + Assert::assertNotNull($currentNode, sprintf('Failed to find node with id "%s" in subgraph %s', $this->currentNode->nodeAggregateId->value, json_encode($subgraph))); + Assert::assertFalse($currentNode->tags->contain(SubtreeTag::disabled()), sprintf('Node "%s" was expected to be enabled, but it is not', $this->currentNode->nodeAggregateId->value)); + } + /** + * @Then I expect this node to be disabled + */ + public function iExpectThisNodeToBeDisabled(): void + { + Assert::assertNotNull($this->currentNode, 'No current node selected'); + $subgraph = $this->currentContentRepository->getContentGraph()->getSubgraph( + $this->currentContentStreamId, + $this->currentDimensionSpacePoint, + VisibilityConstraints::withoutRestrictions(), + ); + $currentNode = $subgraph->findNodeById($this->currentNode->nodeAggregateId); + Assert::assertNotNull($currentNode, sprintf('Failed to find node with id "%s" in subgraph %s', $this->currentNode->nodeAggregateId->value, json_encode($subgraph))); + Assert::assertTrue($currentNode->tags->contain(SubtreeTag::disabled()), sprintf('Node "%s" was expected to be disabled, but it is not', $this->currentNode->nodeAggregateId->value)); } protected function getContentRepositoryService(